dhruvaldarji — portfolio
projects

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

Micro-frontends are an org pattern wearing an architecture costume. I learned that over three phases of OneTrust’s frontend, moving from per-product SPAs, through an Angular Elements host with manifest-based loading, to a Module Federation platform. What started 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. One repository, one framework version, and one release train had carried the estate through the early product catalog, and by the late 2010s they were the binding constraint. Every pressure on the platform was a different face of the same problem: a product team could change its own shape only by asking the rest of the org for permission.

Serialized releases were the visible face. Every team waited on the train, and one team’s failing feature held everybody. Underneath that, the local dev loop and CI had run out of headroom, and every speedup we bought got eaten within a quarter. A single Angular version constrained every product, so an upgrade or an experiment needed org-wide coordination. And with no clean team boundary, a bug in one area broke unrelated products, which customers felt.

Each pressure on its own had a smaller answer available: a better build system, stricter release discipline, a coordinated upgrade plan. Taken together they named something else. The portfolio needed an architecture where team autonomy was the default shape rather than a thing you negotiated for.

The approach

That architecture arrived in three phases, and each one was forced by the specific failure of the phase before it.

The first phase was simple. Every product owned a standalone single-page app, and in narrow terms it worked, since teams could ship without coordinating. The gaps turned structural once the product catalog grew. Shared chrome (nav, header, notifications, theming) drifted visibly product to product. Cross-product composition required copy-paste or embedded iframes. Session, tenancy, feature flags, and i18n got independently rebuilt across over fifty codebases, so any change to those primitives fanned out into as many pull requests.

Phase two had to answer those three failures. I introduced a host shell 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.

That model held for a while, then hit four ceilings at once. Each remote shipped its own Angular, zone.js, and RxJS runtime, so bundle size and memory ballooned and users paid on every navigation. Sharing Angular DI, routing state, and services across the custom-element boundary was awkward, and the glue code piling up around it grew fragile. Remotes drifted across Angular versions, and custom-element upgrade and teardown races showed up as runtime bugs that were expensive to reproduce. And while the manifest resolved which JavaScript to load, host and remote shared only a shape, with no real code-sharing contract between them.

All four ceilings pointed the same direction. The web-component boundary was the wrong boundary, and what the portfolio needed next was a code-sharing contract.

Phase three delivered it. 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 instead of 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 a standing coordination practice rather than a one-time design decision. It is the part of Module Federation that stays off 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 50 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 instead of an integration project. Framework upgrades, the worst coordination problem under the monolith, turned into a per-team rollout governed by the shared-dependency ranges.

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 where most architectural decisions now live, and the release calendar stopped being the place where product teams met.

What I’d change

One principle carries the lesson. Micro-frontends are the right shape when the dominant pain is that teams block teams. They are the wrong shape when the pain is purely technical, where 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 a named discipline from the start of Phase 3, owned explicitly by the platform team instead of emerging out of 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 when CI should have caught them, and chasing them cost more than building the test layer up front would have.