A shell application loads three remote modules from other teams at runtime through module federation. What happens when one remote is unreachable at load time, and separately when a remote ships a change that breaks the shell's assumptions?
Show the full answer Hide the answer
Second by second: the unreachable remote
- t=0 The shell loads and renders. Its own bundle is fine.
- t+200 ms The remote entry request for module B fails - DNS, a bad deploy, an expired certificate, a CDN region issue.
- What happens next is entirely determined by code you may not have written. The default in most setups is an unhandled promise rejection inside a dynamic import. If the shell does not catch it, the error boundary that catches it is whichever one happens to be above that subtree - and if there is none, a blank page.
The important property: this is a runtime dependency with no build-time guarantee. Everything a normal
import verifies at build time - the module exists, its exports match, its version is compatible - is deferred
to the user's browser, on their network, at the moment they need it.
Where it amplifies
- Shared dependency resolution happens at runtime too. If the shell provides React 18 as a singleton and the remote was built against 19, the failure surfaces as a hook error inside the remote rather than as a build failure - and it appears only for users who reach that route.
- One slow remote delays everything composed with it, because the composition point waits. The user experiences the slowest team's deploy.
- Caching multiplies the blast radius in time. A broken remote entry cached at the edge keeps failing after the fix is deployed.
What the user sees, and what stops it
Without deliberate handling: a partial page, a spinner that never resolves, or nothing. With deliberate handling: the rest of the application, and a placeholder where module B would be.
The mechanisms that stop it are concrete, and none is monitoring:
- An error boundary per remote, with a designed fallback - not a generic "something went wrong" over the whole page.
- A timeout on the remote load, because unreachable is easier than slow and slow is what actually happens.
- A pinned, versioned remote entry URL rather than a floating
latest, so the shell chooses when to adopt a new version. This converts a runtime surprise into a deployment decision. - A contract test in the remote's own pipeline that loads it into a copy of the shell. Runtime composition removes the compiler's checks; something has to replace them.
- Strict singleton declarations with required versions, so an incompatible shared dependency fails loudly and early rather than inside a hook.
What would have to be true for it to self-heal
Almost nothing self-heals here, which is the honest answer. A retry with backoff handles a transient network blip. Nothing recovers from an incompatible remote except rolling that remote back - and the shell team cannot do that, because they do not own the deployment. That is the structural cost of runtime composition: you have distributed the release process without distributing the ability to roll back.
When this is the wrong architecture entirely
With fewer than about four independent teams, or when the teams release on a shared cadence anyway, build-time composition through published packages gives the same modularity, keeps compile-time verification, and makes rollback a single deploy. Runtime composition buys independent release timing and nothing else - if the organisation does not actually release independently, the cost is being paid for no benefit.