Part 1 of a series. The build follows in subsequent posts.
Three frontend frameworks in the same business domain is the rule, not the exception. One team adopted Angular years ago. Another fell in love with React. The M&A team brought a Vue app along. The standard answer is Rewrite — years, millions, often failing.
There is another answer: let them live together. This post walks through the architectural spec for a small but real demo that does exactly that — Angular, React, and Svelte inside a single workspace, sharing one business context. Call it Frankenstein-Driven Architecture.
The Frankenstein Reality
Heterogeneity in enterprise frontends isn't a temporary mess to be cleaned up. It's a permanent condition. Acquisitions bring new stacks. Teams pick what they know. Industry tides shift; once-favored frameworks fall out of fashion long before the apps written in them stop earning money.
The rewrite-first culture treats this as a problem to be eliminated. Two years, ten engineers, one framework to rule them all. By the time the rewrite ships, the dominant framework has changed again, the original team has left, and the business questions whether any of it was worth doing.
The alternative is to design with the heterogeneity instead of against it. Stop asking how do we make everything one framework? and start asking how do we let multiple frameworks share one product?
The Architectural Principle
The whole spec rests on one sentence:
Remote owns capability. Host owns business context and persistence.
Each remote is responsible for what it does best — drawing, diagramming, reporting, scheduling. The host is responsible for what the business is about — meetings, customers, orders, claims. Remotes do not own state. They do not own routing. They do not own the user. They render a capability when handed a context, and they emit changes when the user does something.
In this demo, the Angular host owns the meeting context. The React remote owns whiteboarding via Excalidraw. The Svelte remote owns diagram editing via Mermaid. The principle scales: replace whiteboard with reporting, diagrams with scheduling, the structure stays the same.
Islands, Not Components
When the host runs Angular and a remote runs React, there is no shared component model, no shared hook system, no shared reactivity. There is no React-component-inside-Angular-template trick that survives contact with reality.
So each remote is a complete, self-contained application — an island. What it exposes to the host is not a React component or a Svelte component, but a Custom Element that wraps the entire app and boots it on mount.
class WhiteboardRemote extends HTMLElement {
connectedCallback() {
this.root = createRoot(this);
this.root.render(<App ... />);
}
disconnectedCallback() {
this.root?.unmount();
}
}
customElements.define('whiteboard-remote', WhiteboardRemote);
The host then consumes the remote like any other DOM element:
<whiteboard-remote></whiteboard-remote>
Web Components are the boundary because Web Components are a browser standard. Angular, React, Svelte, Vue — all four know how to render and listen to a Custom Element. The browser, not the framework, owns the integration contract.
One Channel, Four Events
If the host is the only orchestrator, communication runs through one channel: an event bus. No initial state via attributes. No properties that have to be set before mount. Remotes are dumb on mount — they know nothing until the bus tells them.
Four events cover the entire cross-framework communication:
-
context:request— Remote → Host, "I just mounted, what's the current context?" -
event:selected— Host → Remotes, "the user is now looking at meeting X, here's its data" -
drawing:changed— React → Host, "the whiteboard changed, here's the new payload" -
diagram:changed— Svelte → Host, "the diagram changed, here's the new source"
The bus itself is fifteen lines of TypeScript wrapping a globalThis-pinned EventTarget. No library. No broker. The wrapper provides typed emit and on so neither end has to remember which payload belongs to which event.
The flow for the most important interaction — the user clicks a meeting in the calendar and both remotes update — is one round-trip:
The host is the hub, the remotes are spokes. Spokes never talk to each other directly.
Native Federation as the Vehicle
The plumbing that lets the host actually load the React and Svelte bundles at runtime is Native Federation v4 — Manfred Steyer's framework-agnostic, ESM- and import-map-native successor to Webpack Module Federation.
Two adapters do the work. The Angular adapter (@angular-architects/native-federation-v4) wires the host: a dynamic-host schematic generates a two-phase bootstrap (init federation first, bootstrap Angular second), a federation.manifest.json listing remote URLs, and a builder that splits shared dependencies into separate chunks. The esbuild adapter (@softarc/native-federation-esbuild) builds the remotes: a small build.mjs script drives runEsBuildBuilder and produces a remoteEntry.json plus its bundle. No Vite involved — the official remote adapter is esbuild-based and framework-agnostic.
The runtime is the Orchestrator (@softarc/native-federation-orchestrator), v4's recommended replacement for the classic runtime. It does semver-aware version resolution for shared dependencies, caches remoteEntry.json data across reloads, and handles share scopes for multi-team setups.
The same machinery is what makes this pattern interesting for migration. A team running an Angular monolith can carve a new feature out as a federated remote in any framework — React, Svelte, whatever the team picks — without touching the existing app. Old code keeps shipping, new capabilities arrive as islands. There is no all-or-nothing rewrite gate.
The Demo
The demo is deliberately small. A meeting room app where the user picks a meeting from a calendar, and the meeting opens with two artifacts side by side: a whiteboard sketch (React + Excalidraw) and a sequence diagram (Svelte + Mermaid). Both are real, iconic open-source applications, embedded as full islands.
Three columns. The Angular calendar (Schedule-X) on the left. The two remotes stacked in the middle. Meeting details and a live event-bus log on the right. Click a meeting, both remotes load that meeting's data. Draw on the whiteboard, the host persists. Switch to a different meeting, both remotes follow the context. Open DevTools and the Network tab shows three frameworks loaded — Angular, React, Svelte — talking through one event bus.
The full spec is in the repo: https://github.com/lutzleonhardt/FrankensteinMeetingRoom/blob/main/specs/SPEC.md. Read it if you want the actual Meeting type, the MeetingService skeleton with stale-update guards, the bus.ts wrapper, the federation.config.mjs for both remotes, the workspace layout, and the milestones the build will follow.
Two Notes from the Spec'ing Process
A spec rarely arrives clean on the first pass. Two corrections from this one are worth sharing because they were genuine surprises during the design conversation, not lessons from a textbook.
The "Vite Adapter" doesn't exist. Going in, the assumption was that Vite-based remotes were the standard path — Vite is everyone's modern build tool, after all. Reading the actual Native Federation docs revealed that the official adapter is @softarc/native-federation-esbuild. Vite is not officially supported. The adapter is framework-agnostic and runs from a hand-written build.mjs, which initially feels backward but turns out to be cleaner: no Vite-Federation interop magic, no plugin ecosystem assumptions, just esbuild plus your framework's source-transform plugin.
One channel, not two. The first instinct was to send initial meeting data via Custom Element properties (the standard Web Components idiom) and use the bus only for ongoing changes. Two channels, two mental models, two places to look when something doesn't render. The spec collapsed this into a single channel: the bus carries everything, including the initial context that a freshly-mounted remote requests via context:request. The remotes become dumber, the architecture clearer, and the workshop pitch tightens to one line: "the only thing crossing the framework boundary is a bus event."
What's Next
This post is part 1. The repo will host the spec and the build, milestone by milestone:
-
M1 — Workspace scaffolded, Angular host with empty federation manifest running on
:4200 - M2 — Calendar, meeting service with persistence, three-column layout, event-bus log
- M3 — React Whiteboard remote — first federation stitching live
- M4 — Svelte Mermaid remote — both remotes federated, the money-shot becomes recordable
- M5 — Polish, README, optional CRUD niceties
Each milestone produces a usable artifact you can stop at and demo. The next post will follow M1 + M2 — the host shell, why the two-phase bootstrap matters, and what „the host is also a remoteEntry.json" actually means.
Repo: https://github.com/lutzleonhardt/FrankensteinMeetingRoom
Spec: https://github.com/lutzleonhardt/FrankensteinMeetingRoom/blob/main/specs/SPEC.md
If your enterprise frontend looks more like a museum than a monolith, this is the pattern that makes that a feature, not a problem.



Top comments (0)