The micro-frontend decision record: why the composition model you chose determines your shared dependency conflict surface and your independent deployment failure rate
Micro-frontend decisions are made in three founding sessions that never document the consequences — the "how do we split the frontend?" session that picks Module Federation or iframes without specifying the shared dependency version negotiation protocol, the "how do we share the design system?" session that enables singleton React without specifying what happens when teams upgrade at different rates, and the "how do we handle routing?" session that wires route-based composition without specifying the cross-app state synchronization contract. What none of these sessions produce is the composition model specification, the shared dependency conflict resolution policy, the deployment compatibility contract between independently-deployed teams, or the cross-app navigation state protocol that determines whether the user's session stays coherent when they move between independently-operated frontend applications.
A 45-person e-commerce engineering organization decomposed their monolithic React frontend into four independently-deployable micro-frontends using Webpack 5 Module Federation. The shell application owned the global navigation, the session, and the route-to-micro-frontend mapping. The product catalog team owned /products/*, rendering browseable category pages, product detail pages, and the image carousel component that drove the highest conversion rate on mobile. The checkout team owned /checkout/*, rendering the multi-step payment flow. The account team owned /account/*, rendering the user's profile and order history. All four micro-frontends shared React as a Module Federation singleton — the shell's federation configuration declared React as a shared library with singleton: true and strictVersion: false, which meant the first micro-frontend to load would claim the singleton slot and all subsequent micro-frontends would use whatever React version it loaded, regardless of what version they had declared as a requirement.
For fourteen months, the shared singleton caused no problems. All four teams had converged on React 17 during the initial migration. Each team's Module Federation remote bundle loaded React 17 from their own node_modules during their CI build, but at runtime, the singleton mechanism ensured that only one copy of React 17 ran in the browser — whichever loaded first. The shell loaded first, its React 17 took the singleton slot, and the other three micro-frontends used the shell's React instance, exactly as designed. The catalog team's image carousel worked correctly because it had been built against React 17 and it ran under React 17.
In month fifteen, the checkout team decided to upgrade their micro-frontend to React 18 to access the concurrent rendering features they needed for their payment form animation — specifically, the ability to use startTransition to mark the form validation state updates as non-urgent, keeping the payment form responsive during heavy input validation. The checkout team's CI build updated their node_modules to React 18, and their remote bundle began shipping React 18. They tested their checkout micro-frontend in isolation against a mock shell and it worked correctly. They deployed it to production on a Tuesday afternoon.
That evening, the on-call engineer received an alert: the product catalog's conversion rate had dropped 18% on mobile. No code had been deployed to the catalog micro-frontend. The catalog's image carousel — the highest-conversion component in the product — was not updating when users swiped between product images. The carousel's state was changing (the selectedIndex state variable was updating), but the displayed image was not re-rendering to match the new index. There were no JavaScript errors in the console. The carousel appeared to be working — it responded to user input — but the visual output was stale.
The root cause took three days to diagnose. The checkout team's new bundle, when loaded on a product page (because the shell loaded all micro-frontend remote entries at initialization, not only the one for the current route), claimed the singleton React slot before the shell's React — the checkout remote entry loaded slightly faster than the shell's own React bundle on some network conditions. React 18's automatic batching behavior — a behavioral change from React 17 — changed the semantics of state updates made inside setTimeout callbacks. In React 17, state updates inside setTimeout were not batched — each setState call triggered a synchronous re-render. In React 18, all state updates are automatically batched, including those inside setTimeout, and a single re-render is scheduled after all updates complete. The catalog team's carousel used a pattern inherited from the original monolith: a setTimeout(() => { setSelectedImage(nextImage); setCurrentIndex(nextIndex); }, 0) call for each swipe gesture, where the two separate setState calls were expected to produce two re-renders — one for the image, one for the index indicator. Under React 17 batching, this produced two renders as expected. Under React 18 automatic batching, both updates were batched into one render, and the render timing changed: the image update and the index update now happened in the same frame rather than in two consecutive frames. The carousel's animation logic, which depended on the two-frame sequence to compute the swipe direction from the intermediate state, received only the final state and produced a stale visual output with no error — the component rendered, but with missing animation state that the carousel used to determine which image to display.
The fix was to add strictVersion: true to the shell's shared React configuration. This would have caused a loud JavaScript exception at load time when the checkout team deployed React 18 — "Module Federation: Singleton React version 18.2.0 conflicts with required version ^17.0.0" — rather than a silent behavioral divergence. The exception would have caught the incompatibility at the moment of deployment rather than 14 hours later via a conversion rate alert. Instead, the fix required a full React 18 migration across all four micro-frontends simultaneously — a three-week coordinated effort across three teams — because the strictVersion: false behavior meant the checkout team's React 18 was already in production serving product catalog pages, and rolling the checkout team back to React 17 would have reintroduced the layout difference the checkout team was trying to eliminate. None of this was in the founding Module Federation session that enabled singleton mode with the defaults.
A 32-person enterprise SaaS company built their customer-facing portal as a shell-and-iframes micro-frontend architecture. Three business units each owned one iframe: the CRM team owned the customer contact management view, the analytics team owned the reporting dashboard, and the billing team owned the subscription and invoice management view. The shell owned the global navigation bar, the user's profile menu, and the URL routing that determined which iframe was shown. Communication between the shell and the iframes used the browser's postMessage API — the shell sent navigation events to iframes, and iframes sent user action notifications to the shell (the user has clicked a customer record; display the analytics for that customer in the analytics iframe).
The team chose iframes precisely to avoid the shared dependency conflict surface problem. Each business unit had different technology preferences — the CRM team used Vue 3, the analytics team used React 18 with Recharts, the billing team used Svelte — and iframes provided perfect isolation. The shell was written in vanilla JavaScript with no framework. Each iframe loaded its own dependencies independently, and a Vue 3 upgrade in the CRM iframe could not affect the analytics iframe's React components. The architecture worked as designed for the first eighteen months.
In month nineteen, the CRM team submitted a bug report that their senior customer success managers had been complaining about for weeks: when navigating from the CRM view to the analytics view, the analytics dashboard showed empty charts and no data, but only for users who had been active in the CRM view for more than four hours before switching. Users who had been using the portal for less than four hours did not experience the problem. Support attributed it to a network issue. The on-call engineer attributed it to the analytics service being slow for heavy query loads. It persisted for six weeks before an engineer on the analytics team ran the browser developer tools with network logging during the bug reproduction and saw the API calls returning 401 Unauthorized.
The root cause was auth token divergence between the iframe contexts. The shell managed the user's session by storing the JWT auth token in memory and distributing it to each iframe via postMessage at application load time. Each iframe stored the token in its own memory and used it for API calls. The shell refreshed the token every 55 minutes (5 minutes before the token's 60-minute expiry) by calling the auth service, receiving a new token, storing it in the shell's memory, and broadcasting it to each iframe via postMessage. This worked correctly — as long as all three iframes were mounted in the browser and their postMessage event listeners were active.
The problem emerged from the shell's iframe rendering strategy: to preserve the analytics team's complex chart state (which required 8–12 seconds to fully render on initial load), the shell did not unmount the analytics iframe when the user navigated to the CRM view. It set the analytics iframe's CSS display to none to hide it. When an iframe is hidden with display: none, its JavaScript continues to execute — the event listener was still active — but there was an undocumented behavior in the company's specific browser-security configuration: the enterprise IT policy deployed a browser extension that throttled postMessage delivery to non-visible iframes, treating them as background frames. On some users' machines, postMessage messages sent to a display: none iframe were delivered with up to a 5-minute delay. On those machines, the shell's token refresh broadcast — sent 55 minutes after login — was received by the analytics iframe up to 5 minutes after delivery. The analytics iframe then used the new token for subsequent API calls. But if the user had been in the CRM view for more than 60 minutes — more than the token's lifetime — and the analytics iframe's delayed postMessage delivery meant the new token had not yet been received when the user switched views, the analytics iframe made API calls with the expired token and received 401 errors.
The fix required building an explicit token acknowledgment protocol: each iframe, upon receiving a new token, must send a postMessage back to the shell confirming receipt. The shell must detect iframes that have not acknowledged a token refresh within 10 seconds and re-send the token. The shell must also respond to a "request current token" message from any iframe that mounted or became visible after the last distribution — the iframe's onload handler or visibility change listener must request the current token rather than waiting for the next scheduled distribution. Building, testing, and deploying this protocol required eight weeks of engineering effort across the shell team and all three iframe teams — coordinated testing was required because the protocol involves all four components. None of this coordination cost was in the founding "we'll use iframes for isolation" session. The isolation benefit was real, but the cross-frame communication surface — every piece of state that must be shared between contexts — was a hidden architectural cost that the founding session did not enumerate.
Structural properties set by the micro-frontend design decision
Three structural properties are determined when a team decides how to decompose the frontend into independently-deployable units. None appear explicitly in the founding session that picks a composition mechanism — they are the operational consequences of choices made under the pressure of enabling team independence before the first cross-team deployment.
Property 1: The composition model and the shared dependency conflict surface. The composition model is the mechanism by which the shell assembles micro-frontend fragments into a unified user experience. The model determines what can be shared between fragments, what must be isolated, and what happens when the shared and isolated boundaries change over time.
In Module Federation client-side composition, the shared dependency conflict surface is the set of library versions that are declared as singletons in the federation configuration. A singleton library — React, Vue, a design system, a state management library — loads once per page, and all micro-frontends use the same instance. The conflict surface emerges when different micro-frontends declare different version requirements for a singleton: Module Federation must choose one version at runtime, and the chosen version may not behave identically to the version that each micro-frontend was built and tested against. The strictVersion: false default silences the conflict at the cost of making the conflict invisible — the wrong version loads without any error, and the behavioral difference manifests as a UI bug or a performance regression that has no obvious connection to the version mismatch. The strictVersion: true setting makes the conflict loud — an exception at load time — but requires all micro-frontends to agree on a version before any can deploy an upgrade, which can block a team's deployment on a peer team's upgrade readiness.
The shared dependency conflict surface is not only the framework library. It includes every library declared as shared: the date formatting library (moment.js, date-fns) that different teams may use at different versions; the icon library (which may add or remove icons between minor versions); the analytics client (which may change the event schema between minor versions); the internationalization library (which may change locale handling between minor versions). The micro-frontend ADR must enumerate every shared singleton by name and declare its version negotiation policy — whether it uses strictVersion: true (version must match exactly, deployment is blocked until all teams agree), strictVersion: false, singleton: true (first loaded wins, silent conflict), or singleton: false (each micro-frontend loads its own copy, no conflict surface but bundle size increases by one copy per micro-frontend per page). The frontend framework decision record documents the chosen framework for each micro-frontend; the service discovery ADR must reference the framework decision record for each singleton that is a framework library, because a framework upgrade in one micro-frontend changes the singleton version for all.
In iframe composition, the shared dependency conflict surface is near-zero: each iframe has its own JavaScript context and loads its own copies of all libraries. A React 18 upgrade in the checkout iframe cannot affect the Vue 3 catalog iframe. The cost of eliminating the conflict surface is that nothing can be shared — the design system must be duplicated in each iframe's bundle, the analytics client must be instantiated separately in each iframe, and any state that must be coordinated between iframes (auth tokens, shopping cart contents, active user preferences) must be communicated through the postMessage API, which adds a cross-frame communication surface that grows with every piece of state that must cross the iframe boundary.
Web component composition sits between Module Federation and iframes: each micro-frontend's custom elements are registered in the global custom elements registry and use Shadow DOM for CSS encapsulation, but all custom elements share the same JavaScript context. The shared dependency conflict surface exists for any library used inside a custom element — if two teams implement their custom elements using React, one using React 17 and one using React 18, both React versions will be loaded into the same page (the custom elements registry does not deduplicate them), and both instances will run, each managing the components rendered inside their custom element's shadow root. This avoids the singleton conflict (each instance manages its own component tree independently) but doubles the React download and runtime cost per page. The performance optimization decision record documents the bundle size and loading performance targets; the composition model choice has a direct budget implication for bundle size and initial page load time that must be calculated as part of the micro-frontend ADR.
Property 2: The deployment independence model and the runtime compatibility contract. Deployment independence is the primary motivation for micro-frontends: each team can deploy their micro-frontend to production without coordinating with other teams or requiring other micro-frontends to rebuild. Module Federation achieves deployment independence by loading remote bundles from URLs at runtime — the shell fetches the catalog team's latest bundle URL each time the user visits a product page, so the catalog team can deploy a new bundle version and it is live for all subsequent page visits without any action by the shell team. This is the core value proposition of Module Federation.
The runtime compatibility contract is the specification of which behaviors each micro-frontend guarantees will be stable across deployments, which changes require coordination across teams, and what the rollback procedure is when a deployment breaks a peer micro-frontend's assumptions. Without a documented runtime compatibility contract, a micro-frontend's deployment can change a shared singleton library's version (breaking peer micro-frontends that assumed the previous version), change an exported component's prop interface (breaking the shell's usage of that component), change a consumed shared module's API (breaking if the shell's version of the shared module has a different interface), or change the payload format of a custom event emitted by the micro-frontend (breaking any peer that listens for that event). All of these are breaking changes that affect deployed peers even though the deploying team ran all their own tests successfully in isolation.
The runtime compatibility contract must specify the following: the interface versioning policy for exported components (semantic versioning on the exports, with the deploying team responsible for maintaining backward compatibility within a major version); the shared singleton version negotiation protocol (which team may update each singleton, the testing and coordination procedure, and whether strictVersion: true is enforced); the cross-app event schema versioning policy (events must carry a version field, consumers must handle events from all supported versions, new fields are additive, removed fields require a deprecation period); and the canary deployment procedure (new versions of a micro-frontend should be loadable alongside the current version for a subset of traffic to detect compatibility problems before full rollout). The CI/CD pipeline decision record documents the deployment pipeline for each micro-frontend; the pipeline must include a compatibility check — loading the new micro-frontend version against the current versions of all peer micro-frontends in a staging environment — as a deployment gate, because compatibility failures that are not caught in staging become production incidents. The feature flag decision record documents the progressive rollout mechanism; micro-frontend deployments should be gated behind feature flags that allow the platform team to route a percentage of traffic to the new version before full deployment, giving the team visibility into compatibility problems at production scale before committing to the full rollout.
Property 3: The navigation model and the cross-app state synchronization surface. The navigation model specifies how URL changes are coordinated when a user moves between micro-frontends, which component owns the browser's history API, and how each micro-frontend participates in route transitions. In the simplest model — a single-page application shell with route-based micro-frontend assignment — the shell owns the history API and renders different micro-frontends for different URL prefixes. When the user navigates from /products/42 to /checkout, the shell unmounts the catalog micro-frontend and mounts the checkout micro-frontend. State that the checkout micro-frontend needs from the catalog session — the selected product, the quantity, the chosen configuration — must be in the URL or in a cross-app state store, because the catalog micro-frontend's internal state is lost when it unmounts.
The cross-app state synchronization surface is the set of state that must be visible to multiple micro-frontends simultaneously or must survive navigation between micro-frontends. The surface grows over product lifetime as new features require state sharing. At launch, the surface may be just the auth token. Three months later, the analytics team needs the current user's experiment assignment to correlate with events. Six months later, the checkout team needs the product catalog's last-viewed product to populate a "continue where you left off" feature. A year in, the account team needs the checkout team's last-incomplete order to surface an "abandoned cart" notification. Each new state-sharing requirement adds to the cross-app state synchronization surface, and each addition must be implemented using the mechanism specified in the micro-frontend ADR — or, if no mechanism was specified, must be implemented ad hoc, producing a heterogeneous mix of localStorage, custom events, URL parameters, and shared Module Federation modules that different teams chose independently.
The three viable mechanisms for cross-app state synchronization in Module Federation are: a shared state module exposed by the shell (the shell's federation configuration exposes a reactive state store — a Zustand store, a Redux store, a custom EventEmitter — that all micro-frontends import and subscribe to; the shell is the authority for all shared state; the cost is that the shared state module becomes a coupling point between the shell and all micro-frontends); the URL as state carrier (shared state is encoded in URL parameters and path segments, making it shareable via links and persistent across refreshes; the cost is that only serializable state can be shared this way, and complex state like a multi-step form's intermediate state cannot be stored in the URL); browser storage (localStorage is synchronous and available to all same-origin micro-frontends, sessionStorage is scoped to the tab and cleared on close; the cost is that localStorage has no built-in change notification, so a micro-frontend that writes to localStorage must also dispatch a storage event or a custom event to notify peers that the state has changed, adding a notification protocol requirement on top of the storage mechanism). The micro-frontend ADR must specify one primary mechanism and document when each secondary mechanism is acceptable for cases where the primary mechanism is insufficient.
What the founding session records and what it omits
Micro-frontend architecture is implemented across multiple phases: an initial phase when the team decides to split the frontend and picks a composition mechanism, a second phase when the team implements shared dependencies and realizes they need a version negotiation protocol, and a third phase when the team adds cross-app navigation and discovers that state sharing requires an explicit synchronization mechanism. None of these phases produces a micro-frontend ADR. Each phase addresses the immediate problem — how do we make it work? — without documenting the structural properties that determine whether it will keep working as teams evolve independently.
Three types of AI chat sessions generate these gaps:
The "how do we split the frontend into independent teams?" session. The trigger is organizational: two or more teams are working on the same frontend codebase, merge conflicts are expensive, deployment coordination is blocking teams, and an engineering leader or architect proposes decomposing the frontend to give each team an independent deployment target. The engineer asks the AI to explain how to implement micro-frontends — what options are available, what Module Federation is, how iframes differ, whether web components are the right choice. The session explains the composition mechanisms, may walk through a Module Federation tutorial, and helps the engineer understand how to configure a shell and two or three remote applications. What the session does not ask or answer: which teams will own which micro-frontends, and how the ownership boundaries map to the product's navigation structure — whether the cart checkout flow is owned by the checkout team even when the cart is initiated from a product detail page owned by the catalog team, and how the product detail page passes cart state to the checkout micro-frontend at the navigation boundary; what the shared dependency policy will be — which libraries will be shared as singletons, what the version negotiation protocol will be when one team needs to upgrade a shared singleton, and whether strictVersion: true will be enforced from the start; what the deployment compatibility testing procedure will be — how the shell team and the remote teams will verify that a new micro-frontend version is compatible with all current peer versions before production deployment; and what the rollback strategy will be if a micro-frontend deployment breaks a peer — whether the rollback procedure is to roll back the deploying team's bundle or to pin the peer's federation config to the previous URL. The monorepo vs. polyrepo decision record documents the repository structure; the choice between a monorepo (all micro-frontends in one repository, shared configuration and lint rules, harder to enforce deployment independence) and polyrepo (each micro-frontend in its own repository, true deployment independence, but shared configuration must be distributed across repositories and synchronized manually) must be documented in the micro-frontend ADR as the governance model for cross-team changes to shared configuration, because the repository structure determines how the version negotiation protocol is enforced.
The "how do we share the design system and avoid loading React twice?" session. The trigger is a bundle size audit or a visual inconsistency: the team runs a bundle analysis and discovers that each micro-frontend is bundling its own copy of React (200 KB each, four micro-frontends = 800 KB of React before any application code), or the team notices that the button component in the checkout micro-frontend is 2 pixels taller than the button in the catalog micro-frontend because both teams updated their design system dependency at different times. The engineer asks how to share React and the design system across micro-frontends without bundling multiple copies. The session explains Module Federation's shared configuration, explains the singleton: true option, and shows how to configure sharing for React, ReactDOM, and a design system package. What the session does not ask or answer: what happens to an existing micro-frontend when a peer micro-frontend deploys a new version of a shared singleton — specifically, that with singleton: true, strictVersion: false, the first-loaded version wins and all other micro-frontends use it, regardless of their declared version requirements, and that this can produce silent behavioral differences between a micro-frontend's test environment (where it is tested in isolation against its declared React version) and its production environment (where it runs against whatever React version the first-loaded micro-frontend claims); how the team will coordinate a version upgrade across micro-frontends when one team needs a new version of a shared singleton — the protocol for requesting an upgrade, the testing procedure, the coordinated deployment sequence, and the fallback if one team cannot upgrade on the required timeline; what the version negotiation policy is for shared libraries that are not framework singletons — the analytics client, the error tracking library, the internationalization library — all of which have minor-version behavioral changes that are not always documented in changelogs; and whether requiredVersion should be specified for each shared library to ensure that a micro-frontend refuses to run with an incompatible version of a peer's shared module rather than silently using an incompatible version. The build system decision record documents the bundler and build toolchain; Module Federation is a Webpack 5 feature, and teams using Vite, esbuild, or other bundlers need to use compatibility adapters (vite-plugin-federation for Vite) that may have different version negotiation behavior than Webpack 5's native Module Federation — the micro-frontend ADR must specify which bundler is authoritative for the federation configuration and what the adaptation strategy is for teams that use non-Webpack bundlers.
The "how do we handle navigation across micro-frontends?" session. The trigger is the first feature that requires the user to move from one micro-frontend to another while carrying state: the user adds a product to the cart in the catalog micro-frontend, then navigates to the checkout micro-frontend, which needs to display the cart contents. Or the user views a customer record in the CRM micro-frontend, then switches to the analytics micro-frontend, which needs to display analytics for that specific customer. The engineer asks how to pass state between micro-frontends during navigation. The session may explain URL parameters, localStorage, Module Federation shared modules, or custom events as options. What the session does not ask or answer: which state will need to cross micro-frontend boundaries over the product's two-to-three-year lifetime — not just the immediate feature (the cart), but the analytics team's future need for experiment assignment, the notification system's future need for the user's read state across micro-frontends, the mobile app's future need for the same session state that the web micro-frontends share; how the chosen state synchronization mechanism will be enforced across teams — whether the team uses a central shared state module that all micro-frontends must import, or whether different teams can choose different synchronization mechanisms independently; what the state persistence contract is — whether shared state survives a page refresh (localStorage persists, module-level singletons do not), a tab duplication (localStorage is shared between tabs of the same origin, module-level singletons are per-tab), or a browser session (sessionStorage clears on tab close, localStorage persists until explicitly cleared or expired); and what the cross-app navigation transition model is — whether the shell performs a hard navigation (full page reload, all micro-frontends reinitialize, shared module state is lost) or a soft navigation (micro-frontend modules are swapped in memory, shared module state persists) for route transitions, and whether the transition model is the same for all route changes or varies by the source and destination micro-frontend pair. The authentication strategy decision record documents the session and token management model; the cross-app state synchronization surface must include the auth token distribution mechanism, because the auth token is the first piece of state that every micro-frontend needs and the first place where cross-app synchronization failures cause visible user-facing errors — 401 Unauthorized responses that produce empty views or error states.
The WhyChose extractor surfaces the "how do we split the frontend," "how do we share dependencies," and "how do we handle navigation" sessions from AI chat history. The micro-frontend ADR converts the implicit choices in those sessions — enabling singleton: true, strictVersion: false to avoid configuration friction during initial setup, using localStorage because it was the first option that worked for a specific state-sharing case, not specifying a version negotiation protocol because the first upgrade hadn't happened yet — into a documented composition model specification that names the mechanism chosen and its shared dependency conflict surface, a documented version negotiation protocol that specifies who can upgrade each shared singleton and the testing procedure for compatibility, a documented runtime compatibility contract that specifies what each micro-frontend guarantees to its peers across deployments, and a documented cross-app state synchronization policy that specifies the authoritative mechanism for each category of shared state.
The five sections of a micro-frontend ADR
Section 1: Composition model and shell architecture. Document the composition model as a specific choice from the set of available options, with the reasoning for that choice over the alternatives. The choice options are: client-side composition via Module Federation (chosen when teams share a technology stack, bundle size optimization via singleton sharing is required, and runtime performance of framework initialization matters more than strict isolation); client-side composition via web components (chosen when teams use different frameworks, standard browser APIs are preferred over proprietary bundler extensions, and Shadow DOM CSS encapsulation is sufficient without JavaScript context isolation); iframe composition (chosen when teams genuinely require JavaScript context isolation, technology heterogeneity is a long-term requirement, or the content being embedded is third-party or security-sensitive); server-side composition via edge includes or SSI (chosen when initial page load performance is the primary constraint, micro-frontends need to be indexable by search engines without JavaScript, and the composition logic can be expressed as URL-based fragment assembly without dynamic per-user logic); build-time composition via npm packages (chosen when deployment independence is a secondary concern and the primary goal is codebase separation, not independent deployment cadence).
For each composition model considered and rejected, document the specific reason for rejection. "We rejected iframes because postMessage communication would have required rebuilding our existing component-level communication patterns" is a documentable reason that explains a future-engineer's question about why iframes were not chosen. "We rejected server-side composition because our personalized pricing requires per-user rendering that edge caches cannot serve" is a documentable reason that a future team can evaluate when the pricing model changes. Without documented rejections, the composition model appears to have been the only option considered, and future engineers re-evaluate the model from scratch rather than building on the previous evaluation.
Document the shell architecture: what the shell owns (navigation, auth token, route-to-micro-frontend mapping, shared state module, analytics initialization), what the shell does not own (any product feature, business logic, application-specific state), and the rule for adding to the shell's responsibilities. The shell's scope tends to expand over time as cross-cutting concerns accumulate — the notification center, the search overlay, the user preference modal — and each expansion increases the coordination cost for all teams because the shell is the one component that all micro-frontends depend on. The ADR must specify the criteria for including a feature in the shell vs. assigning it to a dedicated micro-frontend: features that require rendering on every page are shell candidates; features that require state from multiple micro-frontends are shell candidates (because the shell is the central state authority); features that could reasonably be owned by one team and rendered only in their route prefix should be in a micro-frontend, not the shell. The CDN decision record documents the content delivery infrastructure; in Module Federation, each micro-frontend's remote bundle is served from a CDN URL, and the CDN configuration determines the bundle's cache TTL — the time between a micro-frontend team's deployment and when new users receive the new bundle. The CDN TTL must be documented in the micro-frontend ADR because it determines the deployment propagation latency: a 10-minute CDN TTL means a new micro-frontend version takes up to 10 minutes to be served to all users after deployment, and a cache invalidation event must be triggered on deployment to reduce this to near-zero.
Section 2: Shared dependency management and version negotiation protocol. Document every library declared as a Module Federation shared module, with its version negotiation policy, the team responsible for version governance, and the procedure for upgrading to a new version. For each shared singleton, the document must specify: the current version in production (a specific version string, not a range); the strictVersion setting and the justification; the requiredVersion range that each micro-frontend must declare in its own federation configuration (this range specifies the minimum compatible version and the maximum version that does not introduce breaking changes, allowing the singleton to be shared across micro-frontends that have different patch versions while rejecting major or minor version incompatibilities that would change behavior); and the semantic versioning policy for the shared library (what constitutes a breaking change that requires a major version bump and coordinated deployment vs. what constitutes a compatible change that can be deployed by any team independently).
The version upgrade protocol must be documented as a procedure with named steps and responsible parties. A minimum viable protocol: (1) the team requesting an upgrade submits a proposal to the platform team specifying the library, the current version, the proposed version, and the reason for the upgrade; (2) the platform team reviews the changelog between the current and proposed versions for breaking changes affecting the shared module interface; (3) each micro-frontend team tests their micro-frontend against the proposed version in a staging environment that loads their remote bundle against the shell's proposed module federation configuration; (4) the platform team schedules a coordinated deployment window during which the shell's shared module configuration is updated to the new version, each micro-frontend team's requiredVersion configuration is updated to accept the new version, and all bundles are deployed within a short window; (5) the platform team monitors error rates for 30 minutes after the coordinated deployment and triggers the rollback procedure if error rates exceed the threshold. The rollback procedure must specify which team is responsible for reverting which deployment artifact, and must be practiced during a non-production coordinated deployment at least once before the first production coordinated upgrade.
The API contract testing decision record documents the contract testing strategy for backend services; the same contract testing principles apply to micro-frontend shared modules. Each shared module should have a contract test that specifies the interface it exposes — the exported function signatures, the event payload schemas, the reactive state shapes — and verifies that the module's published version matches the contract. When a team proposes a shared module change, the contract test must be updated to reflect the new interface, and all consumers must verify they can satisfy the updated contract before the change is deployed. Without contract tests on shared modules, breaking interface changes are caught only in production when a consuming micro-frontend fails to mount or renders incorrectly.
Section 3: Deployment independence model and runtime compatibility contract. Document the deployment independence model: what each team can deploy without coordinating with other teams, and what requires coordination. The typical model: a micro-frontend team can deploy a new version of their bundle without coordination if the new version does not change the shared singleton version requirements, does not change the interface of any module they expose, and does not change the payload format of any custom events they emit. Changes that require coordination: shared singleton version upgrades (requires the version negotiation protocol from section 2); interface changes to exposed modules (requires notifying consuming teams, running compatibility tests, and deploying the new interface behind a version flag that consuming teams can opt into before the old interface is deprecated); changes to event payload formats (requires a deprecation period during which both the old and new formats are emitted, allowing consuming teams to update their consumers before the old format is removed).
The runtime compatibility contract must specify the testing procedure that each team's CI pipeline must run before deployment. The pipeline must load the new micro-frontend version against the current versions of all peer micro-frontends in an integration environment — not just against mock stubs or test doubles, but against the actual running versions that will be in production at the time of the deployment. This requires the integration environment to reflect the production configuration: the shell's module federation manifest, which points to each remote's CDN URL, must be updated to point to the new candidate bundle for the micro-frontend under test, while all peer remotes point to their current production bundle URLs. The compatibility test must exercise the user flows that cross micro-frontend boundaries — navigation from one micro-frontend to another, shared state reads and writes, cross-app events — not just the functionality within the micro-frontend under test. The observability strategy decision record documents the monitoring and alerting infrastructure; micro-frontend deployments must emit deployment markers to the monitoring system so that error rate spikes after a deployment can be attributed to the specific micro-frontend version that changed, not to the collective behavior of the production system. Without deployment markers, a post-deployment error spike is visible as a spike in the global error rate, but the attribution to the specific micro-frontend is manual — requiring the on-call engineer to correlate the alert timestamp with the deployment logs from each team's CI system.
Section 4: Navigation model and cross-app state synchronization. Document the navigation model: which component owns the browser's history API, how route transitions are initiated (whether micro-frontends push to the shell's history via a shared module, via a custom event, or via a direct import of the shell's navigation module), how the shell determines which micro-frontend to render for a given URL, and whether route transitions are implemented as hard navigations (full page reload) or soft navigations (micro-frontend swap in memory). Document the transition behavior during a soft navigation: what micro-frontend state is preserved (the unmounted micro-frontend's module-level singletons, if any), what is reset (the unmounted micro-frontend's React component tree state), and what is expected by the remounted micro-frontend (whether it should restore its previous state from a shared store, initialize fresh from the URL, or receive initial state from the navigation event).
Document the cross-app state synchronization mechanism for each category of shared state: authentication state (who is the authority, how is the current token distributed to micro-frontends, what is the token staleness tolerance, what is the behavior when a micro-frontend receives a 401 because its token is stale before the distribution arrives); user preferences (theme, language, timezone — whether stored in the user's profile API and loaded by the shell on session start, or stored in localStorage and loaded by each micro-frontend independently); in-progress task state (the shopping cart, the incomplete form, the unsaved draft — whether stored in the shell's shared state module, in localStorage with a defined key namespace and change event protocol, or in the URL as query parameters); notification state (unread notification counts, notification badges — whether the shell subscribes to a notification service and distributes badge counts to all micro-frontends, or each micro-frontend subscribes independently). For each category, document the authority (who writes the state), the distribution mechanism (how changes are communicated to consumers), the staleness tolerance (how long a consumer can hold stale state before it must re-fetch or re-sync), and the persistence contract (whether the state survives a page refresh, a tab duplication, or a browser restart).
Section 5: Cross-team communication protocol and inter-micro-frontend API contracts. Document the communication protocol for interactions between micro-frontends that are not mediated by the shell. Two micro-frontends that need to communicate directly — the analytics micro-frontend displaying data for the customer record that is open in the CRM micro-frontend — must do so through an explicit interface, not through implicit coupling (shared global variables, direct module imports from peer micro-frontends, shared DOM mutations). The viable communication mechanisms between peer micro-frontends are: custom events on the window object (the CRM micro-frontend dispatches a CustomerSelected event when the user opens a customer record; the analytics micro-frontend listens for CustomerSelected events and updates its data query; the event payload must be versioned and backward-compatible, and the analytics team must document which events it subscribes to); shared state via a shell-exposed module (the CRM micro-frontend writes the selected customer ID to the shell's shared state store; the analytics micro-frontend subscribes to changes in the selected customer ID; the shell's shared state store is the intermediary, and neither micro-frontend depends on the other directly); URL parameters (the shell encodes the selected customer ID in the URL when navigating to the analytics view; the analytics micro-frontend reads the customer ID from the URL on mount; no real-time synchronization is required because navigation is the synchronization event).
The inter-micro-frontend API contract must specify: which micro-frontends publish events or shared state (the publishers), which micro-frontends consume events or shared state (the consumers), the payload schema for each event or state shape, the version of the payload schema currently in production, the deprecation policy for old payload schemas, and the testing approach for verifying that publishers and consumers agree on the schema. The inter-micro-frontend API contract is subject to the same version negotiation problems as the shared dependency contract — a consumer that expects a customerId field in the CustomerSelected event payload will fail silently if the CRM team renames the field to customer_id in a new deployment, because neither team tests the combined behavior in their individual CI pipelines. The solution is the same as for backend API contracts: consumer-driven contract tests, where each consuming micro-frontend defines the payload shape it expects, and each publishing micro-frontend runs those consumer contract tests in its CI pipeline before deploying a new payload format. The feature flag decision record documents the progressive rollout mechanism; new inter-micro-frontend event payload formats should be deployed behind a feature flag that allows the consuming team to test the new format in production for a subset of traffic before the publisher removes the old format — this provides a rollback path without requiring a coordinated rollback of both the publisher and consumer, which is the most common source of extended downtime during a rollback of a breaking cross-app interface change.
Further reading
- Frontend framework decision record — the choice of React, Vue, or other frameworks that determines what can be shared as a Module Federation singleton across micro-frontends.
- Monorepo vs. polyrepo decision record — the repository structure that determines how shared module federation configuration is governed and synchronized across teams.
- Build system decision record — the bundler and build toolchain, since Module Federation is a Webpack 5 feature and teams using Vite or esbuild need compatibility adapters with different behavior.
- CI/CD pipeline decision record — the deployment pipeline that must include cross-micro-frontend compatibility testing as a gate before production deployment.
- Performance optimization decision record — the bundle size and loading performance targets that the composition model choice directly affects through singleton sharing vs. duplication.
- CDN decision record — the content delivery infrastructure that serves each micro-frontend's remote bundle and whose cache TTL determines deployment propagation latency.
- Authentication strategy decision record — the session and token management model that the cross-app state synchronization surface must accommodate for auth token distribution.
- API contract testing decision record — the contract testing strategy whose principles apply to inter-micro-frontend shared module interfaces and event payload schemas.
- Feature flag decision record — the progressive rollout mechanism for deploying new micro-frontend versions and new inter-micro-frontend event payload formats without full-commit deployments.
- WhyChose extractor — surface the "how do we split the frontend," "how do we share dependencies," and "how do we handle navigation" sessions from your AI chat history and convert them into a micro-frontend ADR.