2018 – 2024 · Senior to Principal Software Engineer · 7 min read
Micro-Frontend Architecture
Three-phase evolution of OneTrust's frontend from per-product SPAs, through an Angular Elements host, to a Module Federation platform hosting roughly 50 remotes owned by dozens of product teams.
- Micro-Frontends
- Module Federation
- Angular
- Angular Elements
- Webpack 5
- Cloudflare
Three-phase evolution of OneTrust’s frontend architecture, from per-product SPAs, through an Angular Elements host with manifest-based loading, to a Module Federation platform. What began as over fifty product codebases blocking each other on a shared release contract ended as a platform where dozens of product teams shipped roughly 50 remotes on their own cadence against a single host.
The problem
Frontend delivery at OneTrust had outgrown its release contract. A single repository, a single framework version, and a single release train had carried the estate through the early product catalog, but by the late 2010s they were the binding constraint. Every pressure on the platform was a different face of the same underlying problem: there was no way for a product team to change its shape without asking the rest of the org for permission.
Serialized releases were the first visible face. Every team waited on the train, and any team’s failing feature held the rest. The second was build and test time. The local dev loop and the CI pipeline had run out of headroom, and every incremental speedup was eaten within a quarter. The third was framework lock-in. A single Angular version constrained every product, so upgrades and framework experiments required org-wide coordination. The fourth was ownership and blast radius. With no clean team boundary, a bug in one area broke unrelated products, and the customer experience of that leakage was corrosive.
Each pressure on its own could have been answered by something smaller: a better build system, a stricter release discipline, a coordinated upgrade plan. Taken together they named a different need. What the portfolio actually required was an architecture where team autonomy was the default shape, not a negotiated exception.
The approach
That architecture did not arrive in one step. It evolved through three phases, and each was forced by the specific failure of the one before it.
The first phase was straightforward. Every product owned a standalone single-page app, and in narrow terms it worked: teams could ship without coordinating. The problems showed up only when the product catalog grew large enough to make the approach’s gaps structural. Shared chrome (nav, header, notifications, theming) drifted visibly product to product. Cross-product composition became impossible without copy-paste or embedded iframes. Session, tenancy, feature flags, and i18n were independently rebuilt across over fifty codebases, so any change to those primitives fanned out into as many pull requests.
Those three failures were what the second phase had to answer. A host shell was introduced that loaded product UIs as Angular Elements, custom elements wrapping Angular components, resolved through a manifest the shell fetched at runtime. Shared chrome moved to the host. Composition across products became possible. Product teams kept their independent deploy paths, because the manifest handled the indirection between shell and remote bundle.
The model worked until it did not. Four ceilings arrived at once. Each remote shipped its own Angular, zone.js, and RxJS runtime, so bundle size and memory ballooned and users paid the cost on every navigation. Sharing Angular DI, routing state, and services across the custom-element boundary was awkward, and the glue code accumulating around it grew fragile. Remotes drifted across Angular versions, and custom-element upgrade and teardown races surfaced as runtime bugs that were expensive to reproduce. And although the manifest resolved which JavaScript to load, there was no true code-sharing contract between host and remote, only shape coupling.
Each of those four ceilings pointed the same way. The web-component boundary was the wrong boundary. What the portfolio needed next was a code-sharing contract, not a DOM-sharing one.
The third phase delivered that. Module Federation on Webpack 5 replaced the web-component boundary with a code-sharing surface. Shared singletons such as Angular, RxJS, the internal design system, and i18n were declared, version-ranged, and deduped at runtime. The host exposed a small typed interface for auth, routing, events, and theming, and remotes consumed it through imports rather than DOM events. Remotes kept their independent deploys and stopped shipping their own framework.
The tax that replaced duplicate shipping, and never went away, was shared-dependency governance. Deciding which dependencies became singletons, setting their version ranges, and enforcing them across dozens of product teams was not a one-time design decision. It was a standing coordination practice, and it is the part of Module Federation that does not appear on the architecture diagram.
The outcome
With that tax accepted and paid, the operational picture changed. Dozens of product teams shipped against a single host contract, and roughly fifty remotes deployed independently on their own cadence. The shared release train retired. Cross-product composition, a workflow touching two modules or a shared header surfacing notifications from three, became a normal product capability rather than an integration project. Framework upgrades, the worst coordination problem under the monolith, became a per-team rollout governed by the shared-dependency ranges rather than an org-wide event.
The shape of the frontend org followed the shape of the architecture. Product teams owned their remote end to end. A platform team owned the shell, the host contract, and the singleton-dependency policy. The surface between those two groups became the place where most architectural decisions now lived, and the release calendar stopped being the place where product teams met.
What I’d change
One principle carries the lesson: micro-frontends are an org pattern disguised as an architecture. They are the right shape when the dominant pain is that teams block teams. They are the wrong shape when the pain is only technical, when a stronger build system, aggressive code-splitting, or cleaner module boundaries would deliver most of the value at a fraction of the ongoing cost. The shared-dependency governance tax is real, it is permanent, and it consumes the time of whoever owns the platform.
Narrower than the principle, two investments would have pulled the curve forward. Shared-dependency governance should have been stood up as a named discipline at the start of Phase 3, with the platform team owning it explicitly, rather than being allowed to emerge from team-to-team negotiation. And contract testing between host and remotes should have existed before the first Module Federation remote shipped. Compatibility issues surfaced in production as runtime errors instead of in CI, and chasing them cost more than building the test layer up front would have.