Skip to content

Build1 publisher3 min readPublished

A versioned shell contract catches micro frontend mismatches at load time

Engineers on a government events platform cut their React release cycle about 40% by splitting it into remotes behind a versioned contract. The author credits the end of release queuing, and says the hard work was keeping separately built remotes compatible.

The Engineer · Build desk

Illustration accompanying A versioned shell contract catches micro frontend mismatches at load time

What happened

  • Once the shell loads remotes at runtime, a mismatch stops being a build error and shows up as a blank panel, a library initialised twice, or a hook bound to an unknown React copy.
  • The shell checks those ranges before mounting and refuses any remote that falls outside them, rendering a defined fallback in its place.
  • React and the design system are shared with explicit version ranges, and the author advises keeping that list short because each entry is another coupling to version.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Teams adopting federation have to choose a failure behaviour for an unloadable remote up front, because leaving it unset ships a blank rectangle to users.
  • exposure Without a load-time check, a version mismatch reaches users mid-session, and the first report of it comes from a user days later.
  • cost A frontend owned by one team pays the coordination cost of a distributed system while keeping a monolith's release cadence, so the 40% gain has nothing to come from there.
  • constraint An unwritten contract lives as tribal knowledge, which makes a new engineer's first remote the deploy most likely to break the shell.

The shell and a remote are built at different times, by different pipelines, possibly weeks apart, and the shell first meets the remote's code when it loads it at runtime [3]. No build step ever compiles both halves together [3]. All three failure modes in the post start from that gap [4].

The author is precise about what Module Federation covers. "The config is what makes loading possible. It says nothing about what is safe to load," the author wrote [7]. By the post's account, adding the plugin and naming exposes and remotes takes an afternoon [2].

The team's fix puts a check at the one point that sees both sides: the moment before mount. As the post describes it:

1. Each remote declares the props of its exposed module, the shape of the events it emits, and the range of shared dependencies it was built against [5]. 2. The shell checks that range before it mounts anything [6]. 3. A remote outside the range is refused, and the shell renders a defined fallback in its place [6]. 4. The failure is reported at load, in one place, with a message naming the remote and the expected range [8].

This is good engineering. It treats a remote as input from another team's pipeline and validates it at the boundary [5][6]. The alternative is a component that throws halfway through a user's session and turns up as a broken screen someone reports two days later [6][8]. The post does not include the declaration format or the checking code. The text says the shell checks the dependency range, so it is not clear whether prop and event shapes are also verified at runtime [6].

The author wants the failure behaviour chosen in advance, whether that is a shell fallback, an error boundary or a feature flag [17]. "The default is a blank rectangle, and the default is the one you get if you do not choose," the author wrote [14].

The shared list is where duplicated libraries come from. In this setup React and the design system are declared shared with explicit ranges [9]. The author's rule is to keep that list short, because every shared dependency is a coupling that has to be versioned. A date library probably does not belong on it [10].

The 40% figure describes one frontend. The release cycle came down by about that much, and the author attributes most of it to waiting that disappeared, with build time a small part [11]. Before the split, every team waited for the slowest team in a single release [1]. After it, a team ships when its remote satisfies the contract [15]. For the number to carry over to another codebase, its releases have to be gated on several teams in the same way [1][11]. The author says a frontend owned by one team gets the coordination cost of a distributed system and keeps the release cadence of a monolith [13]. Seams should follow ownership, and smaller bundles are available from plain code splitting [12].

I think the ordering advice is the part that carries over to other codebases most easily. The author says to write the contract before touching the build config. The config can be reversed in an afternoon, while an unwritten contract becomes tribal knowledge that breaks when someone new ships a first remote [16].

What to watch

  • Whether the author publishes the declaration format or checking code, which would show if prop and event shapes are validated at runtime or only dependency ranges.
  • A breakdown of the roughly 40% release-cycle cut separating queue time from build time, which would show how much depends on the multi-team setup.
  • Whether the shared list stays at React and the design system as more teams ship remotes, since each addition is another range the shell must police.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories