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.
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:
| Pattern | Best For | Risk |
|---|---|---|
| URL state | Route-level state, filters, search | Limited for complex state |
| Backend as source of truth | Business-critical state | More API dependency |
| Custom browser events | Simple cross-app notifications | Harder to trace at scale |
| Event bus | Pub-sub communication | Can become hidden coupling |
| Shell-mediated communication | Global UI updates | Shell can become too smart |
| Shared client store | Truly global state | Tight coupling |
| Browser storage | Simple persistence | Sync/security issues |
| Direct remote imports | Rare stable contracts | Runtime 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:
Product Details Remote
│
│ Add item to cart
▼
Cart API
│
│ Updated cart saved
▼
Cart Updated Event
│
▼
Shell Header updates cart countThe 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:
{
"cartId": "cart_123",
"itemCount": 4
}Flow:
Product Details Remote
│
│ dispatches cart:updated
▼
Shell Header
│
│ updates cart badge
▼
User sees new cart countCustom 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:
┌────────────────┐
│ Event Bus │
└───────┬────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
Shell Header Analytics RecommendationsBenefits:
- 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:
| State | Recommended Owner |
|---|---|
| Theme | Shell/platform |
| Locale | Shell/platform |
| Auth identity summary | Shell/auth provider |
| Cart | Backend + Cart Remote |
| Checkout | Backend/session + Checkout Remote |
| Product filters | URL + Catalog Remote |
| Search query | URL + Search Remote |
| Orders | Orders 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:
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 cartImportant:
- 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
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:
{
"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:
{
"cardNumber": "4111111111111111",
"cvv": "123"
}Good event payload:
{
"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
| Need | Recommended Pattern |
|---|---|
| Product filters | URL state |
| Search query | URL state |
| Cart data | Backend API |
| Cart badge update | Custom event |
| Checkout session | Backend/session API |
| User identity summary | Shell auth context |
| Theme | Shared platform context |
| Locale | Shared platform context |
| Analytics | Shared analytics event contract |
| Large form state | Remote-owned or backend session |
| Payment state | Backend/payment provider |
| Global notification | Shell-mediated event |
| Recently viewed | Browser storage or backend profile |
26. Common Anti-Patterns
| Anti-Pattern | Why It Is Bad |
|---|---|
| One global Redux store for all remotes | Tight coupling |
| Passing sensitive data through events | Security risk |
| Using localStorage as hidden API | Hard to debug and insecure |
| Directly importing another remote’s internals | Breaks ownership |
| Too many chatty events | Indicates wrong boundary |
| Undocumented event payloads | Runtime breakage |
| Shell owns all state | Shell becomes a monolith |
| Every remote listens to every event | Hidden dependency chaos |
| No contract tests | Unsafe independent deployment |
| No observability | Difficult 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)