When a host application and several remote microfrontends each declare a required version range for a shared library (for example one remote needs '^17.0.0' and another needs '^18.2.0'), how does a module-federation-style runtime decide which actual version gets loaded on the page, and what options does it give you to control that decision?
answer
- semver range per remote
- requiredVersion + singleton + strictVersion
- loose = silent fallback duplicate
- strict = throw on mismatch
- eager skips negotiation
basics
~20 sThe runtime looks at every version range each app asked for, tries to find one already-loaded version that satisfies all of them, and uses that. If none satisfies everyone, or if you set stricter rules, it either loads a second separate copy for the mismatched app or throws an error, depending on your configuration.
solid answer
~40 sEach microfrontend declares a shared dependency with a semver range (`requiredVersion`) and flags like `singleton` and `strictVersion`. At runtime, the federation runtime collects every requested range for that package across every already-registered remote, and tries to satisfy them all with one loaded instance. If a remote's range is incompatible with what's already resolved: with default settings it silently falls back to bundling and loading that remote's own private copy (breaking singleton guarantees for stateful libraries); with `strictVersion: true` it throws a runtime error instead of silently degrading. `eager: true` skips the async negotiation and forces a version to load immediately rather than being resolved lazily.
go deeper
Should understand that each microfrontend declares a version range, not an exact version, and that the runtime tries to find one version that works for everyone.
Should name the concrete config knobs (requiredVersion, singleton, strictVersion) and describe the default silent-fallback behavior on mismatch.
Should explain the availability-vs-correctness trade-off between loose and strict resolution and describe eager loading's effect on negotiation order.
Should connect version negotiation policy to release process design -- e.g., deciding which libraries get strictVersion, and what compatibility contract teams must honor to keep loose resolution safe.
## What version negotiation is Version negotiation is the mechanism a federation runtime uses to decide, at page-load time, which single copy of a shared library actually gets used when multiple independently-built microfrontends each declare their own opinion about what version they need. The starting point is that every microfrontend is compiled separately, often months apart, by different teams, against whatever version of a library was current when they last touched their `package.json`. When they're all loaded onto the same page, their declared shared-dependency ranges may or may not overlap, and something has to reconcile that before a single instance can be handed to everyone who asked for `singleton: true`. ## What each microfrontend declares Concretely, in Webpack/Rspack Module Federation, each microfrontend's `shared` config for a package looks roughly like `{ requiredVersion: '^18.2.0', singleton: true, strictVersion: false }`. `requiredVersion` is a semver range, not a pinned version -- it says 'anything compatible with 18.2.0 under semver rules is fine for me.' ## How the resolver decides At runtime, as each remote is loaded (often lazily, only when the user navigates to a route that needs it), the federation runtime's shared-scope resolver checks what's already been provided for that package name. 1. **If nothing has been provided yet**, it provisions the requesting remote's own version and registers it as the shared instance going forward. 2. **If something is already provided**, the resolver checks whether the already-provided version satisfies the new remote's declared range. 3. **If yes**, the existing instance is reused -- this is the success path, and it's what makes the singleton pattern actually save bytes and avoid duplicate-instance bugs. 4. **If the existing version does NOT satisfy** the new remote's range, the runtime has to pick a strategy, and this is where `strictVersion` and the runtime's built-in fallback behavior matter. ## Loose mode against strict mode By default (loose mode), an unsatisfiable version mismatch does not crash the page. The runtime logs a console warning and falls back to letting the mismatched remote load and use its own privately bundled copy of the library instead of the shared one. This preserves availability -- nothing visibly breaks at load time -- but it silently defeats the whole point of marking the library `singleton: true`: you now have two instances of a supposedly-singleton library on the page, and for stateful libraries (React, a router, a Redux store) this reintroduces exactly the cross-instance bugs singleton sharing exists to prevent. Setting `strictVersion: true` changes this behavior: instead of silently falling back, the runtime throws a hard runtime error the moment the mismatch is detected, failing loudly rather than degrading silently. Most teams enable `strictVersion` specifically for their framework and any library where 'two instances' is worse than 'the page doesn't load.' ## The second axis: eager loading There's a second axis independent of version negotiation, called eager loading. Normally, shared modules are resolved asynchronously as part of a lazy, code-split load -- the runtime waits to see everyone's declared range before committing to a version, which is what makes cross-remote negotiation possible in the first place. - `eager: true` opts a package out of that: it's bundled synchronously into the initial chunk and provided to the shared scope immediately, without waiting to see what other remotes will ask for. - This is occasionally necessary (e.g., the host needs a library available before any remote has even loaded) but it forfeits negotiation -- whatever the eager package declares becomes the version everyone else is compared against, and it can't itself yield to a 'better' version discovered later. ## The trade-off The trade-off in all of this is between availability and correctness. | Resolution | What it preserves | What it gives up | |---|---|---| | **Loose/fallback resolution** | keeps the page rendering even when teams drift out of sync on versions | at the cost of quietly reintroducing duplicate instances that are hard to notice until a stateful bug appears in production weeks later | | **Strict resolution** | surfaces the incompatibility immediately as a build- or runtime-visible error | at the cost of an outage the moment one team ships a version bump the others haven't caught up to -- which is exactly the kind of tightly coupled release-coordination problem microfrontend architecture is usually adopted to avoid | ## A production failure mode A concrete production failure mode: team A owns the host and ships `react-router` at a newer minor version. Team B's remote, developed against an older baseline, still declares an older compatible range. Under loose resolution, if the newer version satisfies team B's range, it resolves cleanly and nobody notices. But if team A later bumps to a new major version, team B's range no longer overlaps at all -- under loose mode, team B's remote silently gets its own bundled router instance, and now there are two separate router histories on the page, so navigation inside team B's UI can desync from the browser's actual URL. Under strict mode, that same deploy would instead throw immediately, which is unpleasant but at least makes the incompatibility visible at the moment it's introduced rather than as a subtle desync bug discovered later.
- What's the practical difference between setting strictVersion to true versus leaving it false for a shared framework library?With strictVersion false, an incompatible range causes a silent fallback to a second bundled copy, which can reintroduce duplicate-instance bugs without any visible error. With strictVersion true, the same mismatch throws a runtime error immediately, trading a visible failure for the guarantee that a broken singleton is never silently tolerated.
- Why might a host application mark a shared dependency as eager rather than letting it negotiate normally?Eager loading is used when the library must be available synchronously before any remote has had a chance to load and register its own version -- for example, the host's own bootstrap code needs React immediately. The cost is that negotiation is skipped, so the eager version effectively becomes the baseline every later remote is checked against.
- If two remotes both request compatible ranges for a shared library but at slightly different patch versions, does the negotiation reload the library twice?No -- if both ranges overlap with an already-provided version, the runtime reuses that single loaded instance for both remotes rather than loading a second copy; that's the whole point of the negotiation succeeding, and it's the normal, silent, no-warning path.
It's like several people each phoning in a request for 'a car, any model compatible with a 2020-or-newer sedan.' The dispatcher tries to give everyone the same car if one already satisfies all requests. If a new request can't be satisfied by the car already dispatched, loose mode quietly sends that person a second, different car without telling anyone; strict mode refuses the ride entirely until the mismatch is resolved.
saying these in an interview costs you the question
- Thinks version ranges are pinned exact versions, not semver ranges
- Doesn't know what happens on an unsatisfiable version mismatch
- Confuses eager loading with strict version checking
- Assumes a version mismatch always crashes the page
- Can't explain why silent fallback is dangerous for stateful libraries