A host Module Federation config shares React as a singleton with requiredVersion ^18.2.0 while another team ships React 19.0.0. With strictVersion false the page runs 19.0.0 and only warns; with strictVersion true it throws on first load. Versions are examples.

Micro frontends team autonomy governance after the split: tokens, Module Federation contracts and one trace

Splitting a frontend gives each team its own deploy. Keeping those teams from drifting apart takes three shared layers, each held in place by a setting you can read and a signal you can check in your own repository.

What does micro frontends team autonomy governance need after the split?#

After the split, teams stay independent only while three layers stay shared and versioned: design tokens and components, the host and remote contract, and one trace per user action. Here the host is the shell app that loads the others, and each remote is a separately built app it loads. Everything else can differ by team. That includes the state library, the test runner and the release day.

The split itself does not decide which layers are shared. Amazon Web Services (AWS) Prescriptive Guidance, in its page on balancing autonomy with alignment, says leaders must "distinguish between areas that can support flexibility" and areas that need a standard. Its list is short. It names release templates, a shared logging library and one way to raise alerts.

So the three layers map onto that list. First, tokens and components keep the screen consistent. Second, the host and remote contract keeps the runtime consistent, through the shared hints in the plugin for webpack, the JavaScript bundler. Third, one telemetry SDK keeps observability consistent across teams. That matters because the micro frontend guide from Honeycomb, a tracing vendor, updated 11 March 2026, says OpenTelemetry "must be initialized only once per page load".

Each layer below gets the same treatment. You get what to version, which setting enforces it, and which signal in your own code shows it slipping.

The three shared layers: what to version, the setting that enforces it, and the signal that shows drift. Source: AWS Prescriptive Guidance, webpack and module-federation.io docs, Honeycomb guide (11 March 2026), read October 2026.

LayerWhat to versionSetting that enforces itDrift signal
Tokens and componentsToken and component packages under semverA renamed or removed token is a major release; DTCG aliases and $deprecatedTwo majors of the token package in the lockfile
Host and remote contractModule Federation shared hints and generated remote typessingleton, requiredVersion, strictVersion; abortOnError trueA shared entry with no requiredVersion; remote types older than the remote
TelemetryOne OpenTelemetry SDK, @opentelemetry/api as a singletonThe host starts the SDK once; a module attribute on every spanSpans with no module attribute

What does drift cost when nobody owns the shared layers?#

One remote shipping a new React major into a host that never tested it either runs silently with a console warning or throws on first load, and one flag decides which. That flag is strictVersion. In many setups its value was never chosen on purpose.

Take an illustrative case with example version numbers. A host shares React, the user interface (UI) library, as a singleton with requiredVersion ^18.2.0 and ships 18.3.1. Then a checkout team upgrades its remote to React 19.0.0 and deploys on a Tuesday. The host team changed no line of its own code.

If strictVersion is left false, the page keeps one copy of React. It picks the highest version in the shared scope, the pool of modules the builds offer each other, which is the remote's 19.0.0, and the console prints a warning. So the host now runs a major version its tests never saw, in production, because of another team's deploy. But if strictVersion is true, the same deploy throws a runtime error on the first load. As a result, the checkout team finds out before customers do.

Neither outcome is wrong in every case. However, the choice belongs to someone, in writing, with a reason. Otherwise the shared config is a production policy that changes whenever any team edits its own package.json. That is the real cost of unowned shared layers. It is not a slow build but a silent change to code other teams own.

Which layers stay shared, and who owns them?#

Share UI primitives, design tokens, the remote contract and the telemetry SDK; keep domain logic, routes and data inside each team, with a custodian guarding each shared layer. The line between the two is about logic, not about reuse.

Cam Jackson's 2019 article on martinfowler.com draws it plainly. Shared components should "contain only UI logic, and no business or domain logic". So a date picker belongs in the shared library, but a discount rule does not, even when two teams both need it. Once domain models are shared, the same article warns, a breaking change needs a coordinated upgrade across teams.

Diagramwhat each team owns, and the three layers every team shares. Sources: martinfowler.com 2019, webpack ModuleFederationPlugin docs and Honeycomb micro frontend guide, read October 2026

Ownership is the harder part, and the same article names two ways it fails. If "everyone" owns the shared library, in practice no one does, and it drifts into mixed code. But if one central team writes all of it, the people who build components lose touch with the people who use them.

Instead, the article describes a middle model. Anyone can contribute, and a custodian, a person or a team, is responsible for the quality and consistency of what comes in. Apply that model per layer. For example, the design system team can guard the token package, a platform group the shared config, and whoever runs tracing the telemetry setup. Each custodian reviews changes, and no custodian writes every change.

How should design tokens and the component library be versioned?#

Publish tokens and components as versioned packages, treat a renamed or removed token as a major release, and let each remote adopt new versions on its own schedule. A token file is an API like any other, because remotes build against its names.

Semantic Versioning 2.0.0, the version scheme known as semver, sets the rule. You bump the "MAJOR version when you make incompatible API changes". For tokens, that means a renamed token, a removed token or a changed type. So a new token is a minor release, and a fixed colour value under the same name is a patch.

The token format from the Design Tokens Community Group (DTCG), a community group final report dated 28 October 2025, gives two tools that make majors rarer. First, it supports aliases: "The same value can have multiple names or aliases". So a renamed token can keep its old name pointing at the new one for a release or two. Second, it defines a $deprecated property that marks a token as on its way out, with a reason. Together, they turn most renames into a minor release now and a major one later.

Which token change is a major, minor or patch release. Source: Semantic Versioning 2.0.0; DTCG format, W3C community group final report, 28 October 2025.

Token changeRelease
Renamed tokenMajor (an alias keeps the old name working for a release or two)
Removed tokenMajor
Changed typeMajor
New tokenMinor
Fixed colour value under the same namePatch

Steve Kinney's course notes on design systems across micro frontends argue for versioned packages first and runtime sharing second. In his view, tokens and primitives compile into each remote at an explicit version. Meanwhile, the shell owns theme choice and the first paint of token variables. That way, a token change never reaches a remote that has not chosen to take it.

What does Module Federation do when two teams need different versions?#

With singleton set and strictVersion left false, Module Federation loads the highest version in the shared scope and only warns; with strictVersion true, the mismatch throws. Without singleton, two versions can live on the page side by side.

webpack's ModuleFederationPlugin page, read in October 2026, defines each hint. The singleton hint "only allows a single version of the shared module in the shared scope". When several versions are on offer, the highest one wins. Next, strictVersion rejects a shared module whose version does not satisfy requiredVersion, and it "Throws a runtime error if the required version is not found". Its default is true only when a local fallback exists and the module is not a singleton. So for a singleton, it is false.

The module-federation.io shared page, read the same month, describes the same runtime from the other side. In singleton mode "a higher version will be loaded if the versions are inconsistent", and the side with the lower version gets a warning. Without singleton mode, each side loads its own copy.

So the hints give three outcomes for one clash:

  • singleton, strictVersion false: one copy, the highest version, a console warning.
  • singleton, strictVersion true: a runtime error on load.
  • no singleton: each build that cannot use the shared copy loads its own fallback, so the page ships both.

The third outcome is fine for a plain utility library. But it breaks React, which keeps global state, as the webpack page notes. Therefore the policy for React and React DOM, its browser renderer, is singleton plus a requiredVersion, and the only open choice is strictVersion. In short, Module Federation shared dependency versioning is a choice per library, not one global switch.

Try it
Host's requiredVersion
Version the host ships
Version a remote ships
singleton
strictVersion

For a singleton, strictVersion defaults to false.

19.0.0
Yes
No
1

The page runs 19.0.0 and only warns

Under singleton the highest version in the shared scope wins, even outside ^18.2.0. The host now runs a version its tests never saw.

One remote ships 19.0.0 into a host on 18.3.1 with requiredVersion ^18.2.0 (example versions)
HintsThe page runsWhat you see
singleton, strictVersion falseone copy, the highest versiona console warning
singleton, strictVersion truenothinga runtime error on load
no singletonboth versionsa second copy downloaded

Outcomes follow webpack's ModuleFederationPlugin page and module-federation.io's shared page, read October 2026. Modelled, not measured.

outcomes follow the hint definitions on webpack's ModuleFederationPlugin page and module-federation.io's shared page, read October 2026; version numbers are examples. Modelled, not measured.

How does a host get a typed contract for a remote?#

Module Federation's dts option makes each remote emit a zipped type bundle at build time and makes the host download it, so a breaking prop change fails the host's type check. The name comes from the .d.ts type files that TypeScript reads.

On the module-federation.io dts page, read in October 2026, the producer "will automatically generate a compressed type file" called @mf-types.zip during the build. Then the consumer pulls each remote's file and unpacks it into a local @mf-types folder. Both generateTypes and consumeTypes default to true. So a host that imports a remote's component sees its real prop types, not a stub someone typed by hand.

But the defaults hide breakage. When generateTypes or consumeTypes is on, abortOnError is false, and the consumer retries a failed download up to maxRetries, which defaults to 3. In other words, a remote whose types fail to build still deploys. And a host that cannot fetch them still builds against whatever it had before. So the contract exists, but nothing enforces it.

dts settings: the default and what each one does. Source: module-federation.io dts docs, read October 2026.

SettingDefaultWhat it does
generateTypestrueThe remote emits @mf-types.zip during the build
consumeTypestrueThe host pulls each remote's file into a local @mf-types folder
abortOnErrorfalseSet true on both sides in CI so a broken contract fails the build
maxRetries3Retries for a failed type download

Set abortOnError to true on both sides in continuous integration (CI), the build that runs on every merge. Then treat the exposed modules as the remote's public API under semver. Removing an exposed module or a required prop is a major release. In short, types are the build-time half of the contract, and the shared hints are the runtime half.

How do traces stay attributable to the team that owns each remote?#

Initialise one OpenTelemetry SDK in the host, share its API as a singleton, and tag every span with the owning remote, so one user action reads as one trace across teams. A span is one timed unit of work inside a trace.

Micro frontends observability across teams usually breaks at startup. Honeycomb's micro frontend guide, updated 11 March 2026, names the failure. OpenTelemetry libraries "must be initialized only once per page load". Module Federation setups hit this most, because every module tries to start its own copy. So the guide's fix is to start the SDK in one central module and list @opentelemetry/api as a singleton in the shared config. Then every remote creates spans through the same API instance.

The Trace Context Recommendation from the W3C, the web's standards body, dated 23 November 2021, defines the trace-id. It is "used to uniquely identify a distributed trace through a system". So one trace id per user action lets a search, a cart update and a payment call read as one story.

Show data table
What each remote's span adds to the shared trace: the trace-id is shared by every span in one user action, the parent-id is new for every span. Source: W3C Trace Context Recommendation, 23 November 2021
ItemValue
trace-id16 bytes
parent-id8 bytes

Every span in one user action carries the same 16-byte trace-id, and each span adds its own 8-byte parent-id.

Trace contextWhat each remote's span adds to the shared trace: the trace-id is shared by every span in one user action, the parent-id is new for every span. Source: W3C Trace Context Recommendation, 23 November 2021W3C, Trace Context Recommendation, 23 November 2021

Still, one trace is not enough on its own, because it has to say which team did what. Honeycomb's guide shows a module attribute set on each span, so an error report points to the remote it came from. Make that attribute a required field in the shared logging schema, which AWS's alignment page also says to standardise.

Which documentation pages do you build these rules from?#

Build the rules from the official pages in this order: the webpack shared hints, the Module Federation shared and dts settings, then the OpenTelemetry browser setup. Each page holds one part of the policy.

  1. webpack ModuleFederationPlugin: the singleton, requiredVersion and strictVersion hints, and their defaults.
  2. module-federation.io shared: what singleton and requiredVersion do at runtime when versions differ.
  3. module-federation.io dts: generateTypes, consumeTypes and abortOnError for typed remotes.
  4. OpenTelemetry browser getting started, updated 16 October 2026: the TracerProvider, the ZoneContextManager and the W3C trace context propagator.

What is the smallest host config that enforces the shared rules?#

A short block of host configuration declares React and the OpenTelemetry API as strict singletons, turns on remote types, and leaves telemetry setup to the host alone. Your bundler's Module Federation build plugin reads the first file, and the host's entry imports the second file once.

ts
// module-federation.config.ts (host). Version ranges are examples.
import { createModuleFederationConfig } from '@module-federation/enhanced';
export default createModuleFederationConfig({
  name: 'host',
  remotes: { checkout: 'checkout@https://example.com/checkout/remoteEntry.js' },
  shared: {
    // 1. One React, and a mismatch throws instead of warning
    react: { singleton: true, requiredVersion: '^18.2.0', strictVersion: true },
    'react-dom': { singleton: true, requiredVersion: '^18.2.0', strictVersion: true },
    // 2. Every remote creates spans through the host's API instance
    '@opentelemetry/api': { singleton: true },
  },
  // 3. A broken remote contract fails the build
  dts: {
    generateTypes: { abortOnError: true },
    consumeTypes: { abortOnError: true },
  },
});
// telemetry.ts (host only), step 4: start the SDK once per page load
import { context, propagation, trace } from '@opentelemetry/api';
import { ConsoleSpanExporter, SimpleSpanProcessor, TracerProvider } from '@opentelemetry/sdk-trace';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { W3CTraceContextPropagator } from '@opentelemetry/core';
const tracerProvider = new TracerProvider({
  spanProcessors: [new SimpleSpanProcessor({ exporter: new ConsoleSpanExporter() })],
});
trace.setGlobalTracerProvider(tracerProvider);
context.setGlobalContextManager(new ZoneContextManager().enable());
propagation.setGlobalPropagator(new W3CTraceContextPropagator());

Each remote repeats the shared block with the same keys and turns on generateTypes, and no remote imports telemetry.ts. The console exporter comes from the OpenTelemetry page, so swap it for your tracing backend's exporter. Also, the ZoneContextManager keeps the active trace attached across async callbacks in the browser.

Which signals in your own repository show drift has started?#

Drift shows in the repository before users see it: a shared package installed twice in the lockfile, a shared entry without requiredVersion, stale remote types, spans with no owner. None of these needs a count of teams, and each one checks a line of the config above.

  • A shared package installed twice. The lockfile pins every installed version. If it holds two majors of React or of the token package across host and remotes, the shared scope is already picking winners.
  • A shared entry with no requiredVersion. webpack's plugin page says the version is inferred from package.json, with a warning when it cannot be found. So an unpinned entry means the policy is whatever package.json says today.
  • Remote types older than the remote. If the host's @mf-types folder predates the remote's last release, the build used a stale contract. That is what abortOnError false allows.
  • Spans with no module attribute. If some spans in a trace carry no owner, a remote started its own SDK or skipped the schema.

The last signal has a second form. OpenTelemetry's Tracing SDK specification, read in October 2026, sets the batch processor's maxQueueSize default to 2048 spans. "After the size is reached, spans are dropped". Because every remote on the page fills the same queue, one noisy remote can drop another team's spans.

Show data table
One queue per page, shared by every remote: maxQueueSize spans are held before new ones are dropped, maxExportBatchSize spans are sent per export. Source: OpenTelemetry Tracing SDK specification, read October 2026
ItemValue
maxQueueSize2,048 spans
maxExportBatchSize512 spans

Every remote on the page fills one 2048-span queue, exported 512 spans at a time.

Batch queueOne queue per page, shared by every remote: maxQueueSize spans are held before new ones are dropped, maxExportBatchSize spans are sent per export. Source: OpenTelemetry Tracing SDK specification, read October 2026OpenTelemetry, Tracing SDK specification, accessed 29 October 2026

The timing defaults show why a busy page fills that queue. OpenTelemetry's specification, read in October 2026, sets scheduledDelayMillis to 5000 milliseconds between exports. It also sets exportTimeoutMillis to 30000 before an export is cancelled. So a page that creates spans fast can hit the limit between two exports. When a trace has gaps, check the queue before you blame the remote.

Show data table
How long spans wait in the shared queue: scheduledDelayMillis is the longest wait between exports, exportTimeoutMillis cancels an export. Source: OpenTelemetry Tracing SDK specification, read October 2026
ItemValue
scheduledDelayMillis5,000 ms
exportTimeoutMillis30,000 ms

Spans can wait up to 5000 ms between exports, and an export is cancelled after 30000 ms.

Export timingHow long spans wait in the shared queue: scheduledDelayMillis is the longest wait between exports, exportTimeoutMillis cancels an export. Source: OpenTelemetry Tracing SDK specification, read October 2026OpenTelemetry, Tracing SDK specification, accessed 29 October 2026

Run the four checks this week. If one hits, it names the layer to fix and the custodian who owns it.

When is this governance the wrong fit for your teams?#

If every remote deploys together or one team owns them all, build-time packages in one repository give the same consistency without runtime version negotiation. The setup above exists for one reason: separate builds that deploy on separate days.

The webpack Module Federation concepts page frames containers as separate builds, where each page "can be separately deployed". When that is not true, simpler tools do the job:

When the shared machinery is the wrong fit, and the simpler tool. Source: webpack Module Federation concepts docs, read October 2026; martinfowler.com (2019).

SituationSimpler tool
Remotes always ship together.A monorepo, one repository holding every app, with workspace packages gives one lockfile and one React version, checked at build time. So no shared scope is needed.
One team owns every remote.Plain TypeScript project references give typed boundaries without zipped type bundles or download retries.
Two remotes share domain models.No setting fixes that. Jackson's 2019 article says breaking changes to such links need a coordinated upgrade across teams, so merge them or move the boundary.

And if the real question is whether to split at all, that choice comes first. It is covered in whether micro frontends are worth the complexity.

Where to go next#

Start with the join itself in the Module Federation integration post, then the working host and remote, the strangler migration and the per-remote performance budget. Each one picks up a question these rules leave open.

If you want a shared token and component library built and owned, see design systems. If you want people to set up shared dependency policy and typed contracts, see frontend software engineers. Still, the official pages linked above are enough to do all of it yourself.

Questions this post answers

What is micro frontends team autonomy governance?
It is the small set of shared, versioned layers that keep independently deployed micro frontends consistent. In practice that means token and component packages, the Module Federation contract and one telemetry SDK, each with a custodian.
Should Module Federation shared dependencies use strictVersion?
For singletons such as React, setting strictVersion to true turns a silent version change into a loud runtime error. webpack's documentation, read October 2026, defaults it to false for a singleton, so it has to be set on purpose.
How do micro frontend teams share design tokens without a coordinated release?
Publish tokens as a semantically versioned package and let each remote upgrade on its own schedule. Under the DTCG format of 28 October 2025, aliases and the $deprecated property keep old names working while teams move.

Keep reading