Article Guide
Senior15 min readJune 12, 2026

Communication Between Micro Frontends

Learn how micro frontends communicate safely using URL state, backend APIs, custom events, event buses, shared contracts, and why large global stores often create tight coupling.

Tags:
Micro FrontendsFrontend ArchitectureState ManagementSystem DesignInterview Prep

Communication is one of the hardest parts of micro frontend architecture.

It is also one of the most common senior frontend interview topics.

Many developers understand the basic idea of micro frontends:

Split a large frontend into independently owned frontend apps.

But the real challenge starts when those apps need to talk to each other.

Example:

Product Details Remote adds item to cart. Cart Remote owns cart state. Shell Header shows cart count. Checkout Remote needs latest cart information. Analytics needs to track the event.

If communication is designed badly, micro frontends quickly become a distributed monolith.

This article explains how micro frontends should communicate in production systems, what patterns to use, what to avoid, and how to explain these decisions in interviews.

1. Why Communication Is Hard in Micro Frontends

Micro frontends are designed to be independently owned and independently deployed.

That means each micro app should have its own boundary.

But real products are connected.

In an e-commerce system:

Catalog affects Product Details. Product Details affects Cart. Cart affects Checkout. Checkout affects Orders. Profile affects Checkout address.

The challenge is:

How do we let apps collaborate without making them tightly coupled?

Bad communication creates:

  • Hidden dependencies
  • Runtime breakages
  • Shared state chaos
  • Hard debugging
  • Unclear ownership
  • Circular dependencies
  • Deployment coupling

Good communication creates:

  • Clear contracts
  • Low coupling
  • Independent deployment
  • Predictable data flow
  • Easier testing
  • Better ownership

2. Core Rule

The most important rule:

Micro frontends should communicate as little as possible.

Communication should be intentional, documented, and contract-driven.

If two micro frontends communicate constantly, one of these may be true:

  • The boundary is wrong.
  • The domains are too tightly related.
  • The split is too granular.
  • The state ownership is unclear.

Strong interview phrase:

If two micro frontends need constant communication, the boundary is probably wrong.

3. Communication Pattern Overview

Common communication patterns:

PatternBest ForRisk
URL stateRoute-level state, filters, searchLimited for complex state
Backend as source of truthBusiness-critical stateMore API dependency
Custom browser eventsSimple cross-app notificationsHarder to trace at scale
Event busPub-sub communicationCan become hidden coupling
Shell-mediated communicationGlobal UI updatesShell can become too smart
Shared client storeTruly global stateTight coupling
Browser storageSimple persistenceSync/security issues
Direct remote importsRare stable contractsRuntime coupling risk

Recommended order:

  • Prefer URL state when the state belongs in the URL.
  • Prefer backend APIs for business-critical state.
  • Use explicit events for small notifications.
  • Use an event bus only with strong governance.
  • Avoid large shared global stores.
  • Avoid direct dependency on another remote’s internals.

4. URL-Based Communication

URL state is one of the cleanest communication mechanisms.

Example:

/search?q=shoes /categories/men?page=2&sort=price-low-to-high /products/123?variant=blue

This works well for:

  • Search query
  • Filters
  • Sorting
  • Pagination
  • Selected category
  • Selected tab
  • Shareable page state

Benefits:

  • Refresh-safe
  • Bookmarkable
  • Shareable
  • Easy to debug
  • Works across remotes
  • Does not require shared memory

Example:

Shell routes user to: /categories/shoes?sort=price&page=2 Catalog Remote reads: category = shoes sort = price page = 2

The shell does not need to know the internal filter state.

The Catalog Remote owns the interpretation of that URL.

5. When URL State Is Not Enough

URL state is not suitable for everything.

Avoid putting this in the URL:

  • Sensitive data
  • Large objects
  • Payment details
  • Auth tokens
  • Full cart contents
  • Large form state
  • Private user data

Bad:

/checkout?cardNumber=4111111111111111

Good:

/checkout/payment

The checkout state should be stored securely through backend/session APIs, not exposed in the URL.

6. Backend as Source of Truth

For business-critical state, backend APIs should usually be the source of truth.

Examples:

  • Cart
  • Checkout session
  • User profile
  • Order history
  • Payment status
  • Inventory
  • Pricing
  • Promotions

Why?

Because this state must be:

  • Consistent
  • Secure
  • Recoverable
  • Auditable
  • Shared across devices
  • Validated by backend rules

Example cart flow:

flow diagram
Product Details Remote
      │
      │ Add item to cart
      ▼
Cart API
      │
      │ Updated cart saved
      ▼
Cart Updated Event
      │
      ▼
Shell Header updates cart count

The frontend event is only a notification.

The real cart state lives in the backend.

Strong interview phrase:

Business-critical state like cart and checkout should be backend-first, not hidden inside a shared frontend store.

7. Custom Browser Events

Custom events are useful for lightweight communication.

Example:

Cart Remote updates cart count. Shell Header needs to update badge.

Event:

cart:updated

Payload:

jsonEditor
{
  "cartId": "cart_123",
  "itemCount": 4
}

Flow:

flow diagram
Product Details Remote
      │
      │ dispatches cart:updated
      ▼
Shell Header
      │
      │ updates cart badge
      ▼
User sees new cart count

Custom events are good when:

  • The message is small.
  • The payload is stable.
  • The receiving app does not need internal sender details.
  • The event represents a domain-level fact.

Example good events:

  • cart:updated
  • user:logged-out
  • wishlist:item-added
  • search:submitted
  • checkout:completed

Bad events:

  • cart:setInternalReducerState
  • product:updatePrivateHookValue
  • checkout:mutateStepComponent

Events should describe business/domain facts, not internal implementation details.

8. Event Contract Design

Events should have explicit contracts.

A good event contract includes:

  • Event name
  • Owner
  • Payload shape
  • Version
  • Description
  • Consumers
  • Backward compatibility rules

Example:

Event: cart:updated Owner: Cart Team Version: 1 Description: Emitted when cart item count changes. Payload: { cartId: string; itemCount: number; } Consumers: - Shell Header - Analytics

This avoids hidden coupling.

If the Cart Team changes the payload, contract tests should catch it.

9. Event Naming Rules

Use predictable event names.

Good naming:

  • domain:event
  • cart:updated
  • cart:item-added
  • user:logged-out
  • checkout:completed
  • search:submitted

Avoid vague names:

  • update
  • change
  • data
  • notify
  • refresh

Avoid implementation-based names:

  • redux:dispatch
  • component:set-state
  • cartReducer:update

Events should describe business/domain facts, not internal implementation details.

10. Event Bus Pattern

An event bus provides publish/subscribe communication.

Concept:

Remote A publishes event. Remote B subscribes to event.

Example:

Cart Remote publishes cart:updated Shell Header subscribes to cart:updated Analytics subscribes to cart:updated

Architecture:

architecture diagram
                    ┌────────────────┐
                    │   Event Bus     │
                    └───────┬────────┘
                            │
          ┌─────────────────┼─────────────────┐
          │                 │                 │
          ▼                 ▼                 ▼
   Shell Header        Analytics        Recommendations

Benefits:

  • Loose coupling
  • Multiple consumers
  • Simple notification flow

Risks:

  • Hidden dependencies
  • Hard debugging
  • No clear ownership
  • Event storms
  • Undocumented payloads
  • Runtime-only failures

Use an event bus carefully.

It should not become a global dumping ground.

11. Shell-Mediated Communication

Sometimes the shell can coordinate communication.

Example:

Cart Remote emits cart count update. Shell owns Header. Shell updates cart badge.

This is reasonable because the shell owns global layout.

But the shell should not contain domain logic.

Good shell role:

  • Listen for cart count updates.
  • Update header badge.
  • Route to checkout.
  • Show global notification.

Bad shell role:

  • Calculate cart totals.
  • Apply promo codes.
  • Validate payment rules.
  • Manage product filters.
  • Own checkout step logic.

Strong rule:

The shell can coordinate global UI, but domain logic should stay inside remotes or backend services.

12. Shared Store Pattern

A shared client-side store means multiple micro frontends read and write the same frontend state.

Example:

All remotes use one shared Redux store.

This is tempting, but risky.

Problems:

  • Tight coupling
  • Shared release assumptions
  • Hard versioning
  • Hidden dependencies
  • Conflicting updates
  • Difficult testing
  • Reduced team autonomy

Bad architecture:

Catalog Remote writes to global store. Cart Remote reads catalog slice. Checkout Remote mutates cart slice. Profile Remote depends on checkout state.

This can become worse than a monolith because dependencies are now distributed at runtime.

13. When Shared Store Is Acceptable

A shared store is not always wrong.

It can be acceptable for small, stable, platform-level state.

Examples:

  • Theme
  • Locale
  • Feature flags
  • Auth identity summary
  • Global notification state

But even here, keep it minimal.

Avoid storing:

  • Full cart state
  • Checkout form state
  • Product listing state
  • Search result state
  • Payment state
  • Domain business rules

Decision table:

StateRecommended Owner
ThemeShell/platform
LocaleShell/platform
Auth identity summaryShell/auth provider
CartBackend + Cart Remote
CheckoutBackend/session + Checkout Remote
Product filtersURL + Catalog Remote
Search queryURL + Search Remote
OrdersOrders Remote + Orders API

14. Browser Storage

Browser storage includes:

  • localStorage
  • sessionStorage
  • IndexedDB
  • Cookies

It can be useful for:

  • Theme preference
  • Recently viewed items
  • Non-sensitive draft state
  • Experiment assignment

But it has risks:

  • Security issues
  • Sync issues
  • Stale data
  • No clear ownership
  • Race conditions
  • Hard cleanup

Avoid using browser storage as a secret communication channel between remotes.

Bad:

Cart Remote writes private cart state to localStorage. Checkout Remote reads it secretly.

Good:

Cart Remote saves non-sensitive UI preference. Checkout uses backend session for real checkout state.

15. Direct Imports Between Remotes

Directly importing one remote’s internal code is usually a bad idea.

Bad:

Checkout Remote imports Cart Remote's internal reducer. Catalog Remote imports Search Remote's private hook. Shell imports utility functions from every remote.

Why this is bad:

  • Breaks encapsulation
  • Creates deployment coupling
  • Makes internal changes risky
  • Blurs ownership
  • Creates runtime compatibility issues

If something must be shared, expose a stable public contract.

Better:

Cart Remote exposes CartSummaryWidget. Cart API exposes cart data. Cart emits cart:updated event.

16. Communication in E-commerce Example

Scenario:

User views product details. User clicks Add to Cart. Header cart count updates. Cart page shows updated item. Checkout uses latest cart.

Recommended design:

flow diagram
Product Details Remote
      │
      │ POST /cart/items
      ▼
Cart API
      │
      │ returns updated cart summary
      ▼
Product Details Remote emits cart:updated
      │
      ▼
Shell Header updates cart badge
      │
      ▼
Cart Remote fetches latest cart from Cart API when opened
      │
      ▼
Checkout Remote creates checkout session from backend cart

Important:

  • The event updates the UI badge.
  • The backend owns the real cart.
  • The Cart Remote owns cart page behavior.
  • The Checkout Remote owns checkout session behavior.

This is clean because ownership is clear.

17. Sequence Diagram: Add to Cart

sequence diagram
User
 │
 │ clicks Add to Cart
 ▼
Product Details Remote
 │
 │ POST /cart/items
 ▼
Cart API
 │
 │ returns updated itemCount
 ▼
Product Details Remote
 │
 │ emits cart:updated
 ▼
Shell Header
 │
 │ updates cart badge
 ▼
Analytics
 │
 │ tracks add_to_cart

This sequence avoids direct coupling between Product Details, Cart, Shell, and Analytics.

They communicate through backend APIs and explicit events.

18. Communication in Search and Catalog

Search and catalog can communicate through URL state.

Example:

/search?q=laptop&sort=price

Flow:

Search Remote updates URL query params. Catalog/Search results read query params. User refreshes page. Same state is restored from URL.

Benefits:

  • Bookmarkable
  • Shareable
  • Refresh-safe
  • No shared store needed

Good for:

  • Search query
  • Filters
  • Sort option
  • Pagination
  • Category slug

19. Communication in Auth

Authentication is usually shell-owned.

Flow:

Shell authenticates user. Shell provides identity context. Remotes consume identity summary. Remotes call domain APIs with valid auth context. Backend enforces authorization.

Identity context may include:

  • userId
  • displayName
  • roles summary
  • locale
  • logged-in status

Do not expose:

  • Secrets
  • Raw tokens in events
  • Sensitive user data
  • Payment data

Important:

Frontend auth context improves UX. Backend authorization protects the system.

20. Communication in Analytics

Analytics often crosses all domains.

Recommended approach:

Shell initializes analytics SDK. Remotes emit domain analytics events. Analytics layer normalizes and sends data.

Example:

catalog:product-clicked cart:item-added checkout:payment-submitted order:placed

Avoid each remote implementing analytics differently.

Governance should define:

  • Event naming
  • Payload standards
  • PII rules
  • Required fields
  • Versioning
  • Testing

21. Contract Testing for Communication

Communication contracts should be tested.

Test:

  • Event names
  • Payload shapes
  • Required fields
  • Optional fields
  • Backward compatibility
  • Route contracts
  • Shared context contracts

Example contract test:

When cart item is added, cart:updated event must include: - cartId - itemCount

If a team removes itemCount, the test should fail before deployment.

Without contract tests, independent deployment becomes dangerous.

22. Observability for Communication

You should be able to debug cross-app communication.

Track:

  • Event name
  • Publisher remote
  • Consumer remote
  • Payload version
  • Route
  • Shell version
  • Remote version
  • Timestamp
  • Error/failure

Example log:

jsonEditor
{
  "event": "cart:updated",
  "publisher": "productDetailsRemote",
  "consumer": "shellHeader",
  "payloadVersion": "1",
  "route": "/product/123",
  "productDetailsVersion": "2.4.1",
  "shellVersion": "3.1.0"
}

This helps answer:

  • Which app published the event?
  • Which app consumed it?
  • Did the payload change?
  • Did the issue start after a deployment?

23. Security Considerations

Communication between micro frontends happens in the browser, so be careful.

Rules:

  • Do not pass secrets through events.
  • Do not pass tokens through custom events.
  • Do not store sensitive state in localStorage.
  • Validate event payloads.
  • Do not trust client-side authorization.
  • Avoid exposing private user data.
  • Keep payment data out of frontend communication channels.

Bad event payload:

jsonEditor
{
  "cardNumber": "4111111111111111",
  "cvv": "123"
}

Good event payload:

jsonEditor
{
  "checkoutStep": "payment-submitted",
  "status": "pending"
}

24. Performance Considerations

Communication can affect performance.

Risks:

  • Too many events
  • Large payloads
  • Event loops
  • Repeated API calls
  • Unnecessary re-renders
  • Global store updates triggering many apps

Best practices:

  • Keep event payloads small.
  • Avoid high-frequency global events.
  • Debounce where needed.
  • Use URL state for route-level state.
  • Avoid global store updates for local UI changes.
  • Monitor event-driven re-render impact.

Bad:

Catalog emits event on every mouse move. All remotes subscribe and re-render.

Good:

Catalog emits event only when user applies filters.

25. Communication Decision Table

NeedRecommended Pattern
Product filtersURL state
Search queryURL state
Cart dataBackend API
Cart badge updateCustom event
Checkout sessionBackend/session API
User identity summaryShell auth context
ThemeShared platform context
LocaleShared platform context
AnalyticsShared analytics event contract
Large form stateRemote-owned or backend session
Payment stateBackend/payment provider
Global notificationShell-mediated event
Recently viewedBrowser storage or backend profile

26. Common Anti-Patterns

Anti-PatternWhy It Is Bad
One global Redux store for all remotesTight coupling
Passing sensitive data through eventsSecurity risk
Using localStorage as hidden APIHard to debug and insecure
Directly importing another remote’s internalsBreaks ownership
Too many chatty eventsIndicates wrong boundary
Undocumented event payloadsRuntime breakage
Shell owns all stateShell becomes a monolith
Every remote listens to every eventHidden dependency chaos
No contract testsUnsafe independent deployment
No observabilityDifficult production debugging

27. Interview Questions

Q1. How do micro frontends communicate?

Micro frontends can communicate using URL state, backend APIs, custom events, event buses, shell-mediated context, or shared state. I prefer URL state for route-level data, backend APIs for business-critical state, and small explicit events for simple notifications. I avoid large shared global stores because they create tight coupling.

Q2. Why not use one global Redux store?

A global Redux store couples independently deployed applications to one shared state shape. If one team changes the state structure, other remotes may break at runtime. It also reduces team autonomy and makes testing/versioning harder. I would only use shared state for small, stable platform-level state like theme, locale, or identity summary.

Q3. How would you handle cart state across micro frontends?

I would make the Cart API the source of truth. The Cart Remote owns cart UI. Product Details can call the Cart API to add an item and emit a small cart:updated event with item count. The Shell Header can update the badge from that event. Checkout should fetch or create checkout state from backend cart/session APIs.

Q4. When should you use URL state?

Use URL state for shareable, refresh-safe state like search query, filters, sorting, pagination, selected category, and selected tab. Do not use it for sensitive or large private data.

Q5. What is a good event contract?

A good event contract defines the event name, owner, payload shape, version, consumers, and backward compatibility rules. For example, cart:updated may include cartId and itemCount, owned by the Cart Team.

Q6. What is the biggest communication anti-pattern?

The biggest anti-pattern is using a shared global store or event bus as a dumping ground for all cross-app communication. This creates hidden coupling and turns micro frontends into a distributed monolith.

28. Strong Senior Answer

If an interviewer asks:

“How would you design communication between micro frontends in an e-commerce app?”

A strong answer:

I would first minimize cross-app communication by choosing clear domain boundaries. For route-level state like search query, filters, sorting, and pagination, I would use URL state so the page remains shareable and refresh-safe. For business-critical state like cart, checkout, profile, and orders, I would use backend APIs as the source of truth. For example, Product Details can call the Cart API when a user adds an item. After the API succeeds, it can emit a small cart:updated event with item count so the Shell Header can update the badge. I would avoid a giant shared Redux store because it couples independently deployed remotes to one shared state shape. I would only share small platform-level state like theme, locale, auth summary, and feature flags. For custom events, I would define explicit contracts with event names, payload shapes, versioning, owners, and consumers. I would also add contract tests and observability so we can detect payload changes and production failures. The goal is low coupling, clear ownership, and predictable data flow.

29. Final Checklist

  • Is the communication really needed?
  • Can this be solved through URL state?
  • Should the backend be the source of truth?
  • Is the event payload small and stable?
  • Is the event contract documented?
  • Is there an owning team for the event?
  • Are consumers known?
  • Are contract tests in place?
  • Is sensitive data excluded?
  • Is communication observable?
  • Are we avoiding global shared state?
  • Does frequent communication indicate a wrong boundary?

30. Summary

Communication between micro frontends should be minimal, explicit, and contract-driven.

Recommended strategy:

URL state for route-level state Backend APIs for business-critical state Custom events for small notifications Shell context for platform-level state Shared stores only for rare, stable global state

Avoid:

Large global stores Hidden localStorage contracts Direct imports between remotes Undocumented event payloads Chatty event buses Sensitive data in browser events

The strongest interview takeaway:

Micro frontend communication should preserve independence. If communication creates tight coupling, the architecture is failing.

References

  • Micro Frontends — Martin Fowler (https://martinfowler.com/articles/micro-frontends.html)
  • Micro Frontends (https://micro-frontends.org)
  • AWS Prescriptive Guidance: Micro-frontends (https://docs.aws.amazon.com/prescriptive-guidance/latest/micro-frontends-aws/introduction.html)
  • webpack Module Federation Documentation (https://webpack.js.org/concepts/module-federation/)
  • Module Federation Official Site (https://module-federation.io)