How micro-frontends communicate, and what to avoid
Well-cut micro-frontends have little to say to each other. For what remains, there are a few simple mechanisms, ordered here from the loosest coupling to the tightest.
Start by communicating less
The amount of communication between two micro-frontends measures how good the boundary between them is. If two areas exchange messages constantly, they are probably one area, and merging them is cheaper than building a protocol.
Most legitimate needs fall into three groups: open another area's screen, know something global such as the current user, and learn that something happened elsewhere.
1. The URL
Navigation with parameters is the default mechanism. Orders sends the user to /customers/42?tab=payments, and the customers area loads what it needs.
The URL survives reloads, can be shared and tested, and creates no code dependency between teams. The rules are in Routing across micro-frontends.
2. The backend as the source of truth
When two areas show the same business data, each one fetches it from the API. Nothing is shared in the browser. This costs an occasional duplicate request, which HTTP caching can reduce. In return, no area depends on another being loaded.
3. Shared services from the platform
A few things are truly global: the current user, the language, permissions, feature flags. These come from a small platform package, initialized by the host.
In Angular with federation, this is a service in a library that is shared as a singleton. Keep its API narrow and mostly read-only: observables or signals to read, a few methods to write. A shared service that grows a method for every feature has become global state under another name.
4. Custom events
To announce that something happened, the browser already has a mechanism. It works across frameworks, across framework versions and with web components.
// publisher: orders
window.dispatchEvent(new CustomEvent('orders:order-paid', {
detail: { orderId: '981' }
}));
// subscriber: notifications
window.addEventListener('orders:order-paid', (e) => {
refreshBadge(e.detail.orderId);
});
5. A small event bus
An event bus is a thin, typed wrapper around the same idea, published as a shared package: publish(name, payload) and subscribe(name, handler) that returns an unsubscribe function.
It adds three things over raw events: type checking of payloads, one catalog of all events, and a place to log traffic during development. In Angular, a singleton service with an RxJS Subject does the job, but only for remotes that share the same library instance. DOM events and BroadcastChannel also reach parts that do not.
Event design rules
- Name events as facts, with the owner as prefix.
orders:order-paidsays what happened. A command such ascart:refresh-nowtells another team's code what to do, which couples the two. - Send identifiers and small values.
- Treat events as a public API. Document them. Adding fields is safe. Renaming or removing them needs a deprecation period.
- Assume nobody may be listening. The subscriber's remote may not be loaded. The publisher must not depend on a reaction.
- Assume the event may have been missed. A remote loaded later must be able to get the current state from the API.
- Unsubscribe on destroy. Listeners on
windowoutlive components and leak. - Expect no reply. If you need an answer, you need a function call. Use a shared service or the API.
What to avoid
Shared mutable global state. A single store that every micro-frontend reads and writes, or objects on window used the same way, brings back the coupling of the monolith without its tooling. Any team can break any other. Release order starts to matter. Nobody can change the shape of the state, because nobody knows who depends on it.
Other patterns that cause trouble:
- Importing another remote's code directly. That is a build-time dependency between two teams that were meant to be independent.
- Using
localStorageas a message channel inside one page. It is synchronous, untyped and survives after it should have been cleared. - Putting tokens or personal data in events. Every script on the page can listen. Session handling is covered in Shared authentication.
Which mechanism for which need
| Need | Mechanism |
|---|---|
| Open a screen in another area | URL |
| Same business data in two areas | Each fetches from the API |
| Current user, language, permissions | Shared platform service |
| Announce that something happened | Custom event or event bus |
| Talk to a part in another framework or version | Custom events; attributes and properties on a custom element |
| Talk to content in an iframe | postMessage, with origin checks on both sides |
Checklist
- Is there a written catalog of cross-area events, with an owner for each?
- Does every remote still work when loaded alone, with no other remote present?
- Do any two areas exchange more than a handful of event types? Review the boundary, and see Team ownership and Conway's law.
More in this section
- Shared dependencies and versions in micro-frontends
- Keeping one look across micro-frontends: tokens, components and style isolation
- CI/CD for micro-frontends: independent pipelines, discovery and rollbacks
- Error isolation and resilience in micro-frontends
Planning a migration or a micro-frontend platform?
In a 20-minute call you describe your situation and I tell you how I'd approach it. If it isn't worth doing yet, I'll say so.
Message me on LinkedIn