Back to Blog
AngularAugust 22, 202611 min read

Micro-Frontends with Angular. Is It Worth It in the Real World?

DR

Doug Ribeiro

11 min read

Micro-frontends are one of those architectural patterns that sound compelling in conference talks and feel different once you are living with them in production. I have worked within a micro-frontend architecture on an enterprise Angular project at BMO, and I have a considered opinion about them now. It is not a simple answer, but it is an honest one.

The short version: micro-frontends solve real problems for teams of a specific size and organizational structure. For teams smaller than that threshold, they add complexity that produces no benefit. The challenge is that the threshold is higher than most architectural discussions suggest, and the costs are more concrete than the benefits.

What Problem Micro-Frontends Actually Solve

The honest pitch for micro-frontends is not about technology. It is about team topology. When multiple teams need to develop, deploy, and own different parts of a large frontend application independently, a monolithic Angular app creates coordination costs. Team A cannot deploy their changes without coordinating with Team B. A bug in Team C's code can block a release for all other teams. Ownership is blurry because everyone owns the same codebase.

Micro-frontends address this by drawing hard boundaries at the deployment and ownership level. Team A owns MFE-A. They can deploy it independently. Their bugs do not affect Team B's deployment. Their technology choices (within limits) do not constrain Team B. The monolith coordination problem goes away because there is no shared monolith.

If you have one team, two closely coordinated teams, or a codebase that deploys infrequently, you do not have this problem. Micro-frontends then solve nothing you actually have, and they introduce costs without the corresponding benefit.

Module Federation Setup in Angular

The most common path to micro-frontends in Angular is Module Federation via the community-maintained @angular-architects/module-federation package; Angular itself ships no official micro-frontend solution. This webpack plugin allows a host application to load remote JavaScript modules at runtime from separately deployed URLs. Each remote module is an independently deployed Angular application that exposes components, routes, or services.

The setup involves modifying webpack configuration for both the host and each remote. Module Federation is a webpack feature, so it needs Angular's webpack-based builder rather than the esbuild-based default that arrived in Angular 17 (Native Federation, from the same maintainers, covers the esbuild path). The plugin generates the webpack configs for you. In practice, the generated configs need customization for anything non-trivial.

javascript
// webpack.config.js for a remote MFE
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { share } = require('@angular-architects/module-federation/webpack');

module.exports = {
  output: {
    uniqueName: 'workspaceApp',
    publicPath: 'auto'
  },
  optimization: { runtimeChunk: false },
  experiments: { outputModule: true },
  plugins: [
    new ModuleFederationPlugin({
      library: { type: 'module' },
      name: 'workspaceApp',
      filename: 'remoteEntry.js',
      exposes: {
        './WorkspaceRoutes': './src/app/workspace/workspace.routes.ts'
      },
      shared: share({
        '@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        '@angular/common': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        '@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        'rxjs': { singleton: true, strictVersion: false, requiredVersion: 'auto' }
      })
    })
  ]
};
typescript
// app.routes.ts in the host
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/module-federation';
import { environment } from '../environments/environment';

export const routes: Routes = [
  {
    path: 'workspace',
    loadChildren: () =>
      loadRemoteModule({
        type: 'module',
        remoteEntry: environment.workspaceRemoteUrl + '/remoteEntry.js',
        exposedModule: './WorkspaceRoutes'
      }).then(m => m.WORKSPACE_ROUTES)
  }
];

One thing to be plain about, because this post is from August 2026: the webpack builder path above is the legacy one. Angular's default builder has been esbuild-based since v17, and the maintained option for it is @angular-architects/native-federation. The concepts carry over: remotes, a remote entry, shared singletons. What changes is the wiring. Native Federation uses a federation config file plus an initFederation call in main.ts against a JSON manifest, instead of a webpack config. Start a new micro-frontend there. I show the webpack config because it is what most existing enterprise setups still run, and because migrating off it is its own project.

Host Shell ApplicationRouting, authentication, global layout. Loads remote MFEs on demand 1 Remote MFEs (Feature Teams)Independently deployed Angular apps exposed via Module Federation 2 Shared DependenciesDesign system, auth state, shared store. Singleton instances to avoid v… 3 InfrastructureCDN per MFE, independent CI/CD pipelines, feature flags per remote 4

Communication Between MFEs: The Hardest Part

Once you have multiple independently deployed Angular applications that need to communicate, you discover that Angular's dependency injection system was not designed for this. Services in the host application are not automatically available to services in remote applications. You cannot inject a host service into a remote component directly.

There are three patterns for cross-MFE communication, each with significant tradeoffs.

The first pattern is shared singleton services. You expose a service from the host (or from a shared library) and configure Module Federation to treat it as a singleton shared dependency. Remote MFEs import and inject this service. It works, but it creates tight coupling. The remote now has a compile-time dependency on the host's service interface. Changes to the shared service require coordinating all MFEs that consume it.

The second pattern is custom events. MFEs communicate by dispatching and listening to browser CustomEvents on the window. This is fully decoupled. No shared imports. It works across any technology, not just Angular. The downside is that CustomEvents are stringly typed, have no TypeScript support by default, and debugging requires understanding an event chain that spans multiple application boundaries.

The third pattern is a shared state store. Typically a tiny framework-free store, plain TypeScript with no Angular imports, no NgRx and no signals, shared by reference through Module Federation's shared dependency mechanism. Both host and remotes read and write to this store. It is a global state management solution, which has all the complexity of global state at scale.

typescript
// shared-mfe-store.ts
// A framework-free store, shared as a singleton through Module Federation.
// No Angular imports live in this file.
import type { AppNotification, User, Workspace } from './shared-types';

export interface SharedState {
  currentUser: User | null;
  activeWorkspace: Workspace | null;
  notifications: AppNotification[];
}

export type SharedAction =
  | { type: 'SET_USER'; user: User | null }
  | { type: 'SET_WORKSPACE'; workspace: Workspace | null };

export class SharedMFEStore {
  private _state: SharedState = this.initialState();
  private _listeners = new Set<(state: SharedState) => void>();

  getState(): SharedState {
    return { ...this._state };
  }

  dispatch(action: SharedAction): void {
    this._state = this.reduce(this._state, action);
    this._listeners.forEach(listener => listener({ ...this._state }));
  }

  // Returns the unsubscribe function. Callers must keep it and call it.
  subscribe(listener: (state: SharedState) => void): () => void {
    this._listeners.add(listener);
    return () => {
      this._listeners.delete(listener);
    };
  }

  private initialState(): SharedState {
    return { currentUser: null, activeWorkspace: null, notifications: [] };
  }

  private reduce(state: SharedState, action: SharedAction): SharedState {
    switch (action.type) {
      case 'SET_USER': return { ...state, currentUser: action.user };
      case 'SET_WORKSPACE': return { ...state, activeWorkspace: action.workspace };
      default: return state;
    }
  }
}

export const sharedStore = new SharedMFEStore();

The store itself stays free of Angular. The bridge into Angular is a separate service, and it is where the teardown has to be handled: subscribe returns an unsubscribe function, and if nobody calls it the store keeps a reference to a component that is long gone.

typescript
// shared-state-bridge.service.ts
import { DestroyRef, Injectable, Signal, inject, signal } from '@angular/core';
import { sharedStore, SharedState } from 'shared/shared-mfe-store';

@Injectable({ providedIn: 'root' })
export class SharedStateBridge {
  private destroyRef = inject(DestroyRef);

  select<T>(selector: (state: SharedState) => T): Signal<T> {
    const sig = signal<T>(selector(sharedStore.getState()));
    const unsubscribe = sharedStore.subscribe(state => sig.set(selector(state)));
    this.destroyRef.onDestroy(unsubscribe);
    return sig.asReadonly();
  }
}

Shared State Challenges

The shared state challenge with MFEs is that the state you need to share is almost always the state that changes most frequently and has the most consumers. Authentication state, current user, global notifications. These are exactly the things that every MFE needs and that you most need to get right.

The problem compounds with Angular Signals. Signals only cross an MFE boundary when @angular/core is shared as a singleton, as in the config above, and the signal itself is handed over through a shared library or store. If a remote ends up with its own copy of Angular, the host's signals are invisible to it. You have to bridge between the Angular-native reactive system and whatever cross-boundary communication mechanism you chose. This bridging code is fragile, hard to test, and requires deep understanding of how Angular's change detection interacts with external state changes.

It is not unusual for a project to spend more engineering time on the MFE communication infrastructure than on the actual features those MFEs deliver. When the plumbing takes more effort than the features, you have probably over-architected.

Deployment Complexity

A monolithic Angular app has one deployment artifact: one build output that goes to one CDN. A micro-frontend architecture with five MFEs has five deployment artifacts, five CDN paths, version management across all five, and a host application that must reference remote URLs that work in every environment.

The URL configuration problem is genuinely hard. In development, remotes run on localhost:4201, 4202, etc. In staging, they live at different subdomains. In production, different ones again. The host application must know the correct URL for each remote in each environment. You need environment-aware configuration that is bundled with the host but references externally deployed remotes, which is a configuration problem that does not exist in a monolith.

Version compatibility between host and remotes is another real problem. If you update a shared library in the host, all remotes that declare that library as a shared dependency will receive the host's version at runtime. If the shared library has a breaking change and a remote was built against the old version, you have a runtime version conflict. The Angular version, RxJS version, and any shared design system library all become potential runtime conflict points.

Team Autonomy Tradeoffs: The Honest Assessment

The promised benefit of MFEs is team autonomy. Each team deploys independently, owns their MFE completely, and is not blocked by other teams. In practice, the autonomy is more limited than advertised, for two reasons.

First, shared dependencies constrain technology choices. If the host uses Angular 21, all remotes must use a compatible Angular version. You cannot have one team on Angular 19 and another on Angular 21 in the same MFE ecosystem. The singleton shared library constraint makes version divergence produce runtime errors. The teams are autonomous in some ways, but they are coupled at the dependency level.

Second, cross-cutting concerns require coordination. Authentication changes, global navigation changes and shared design system updates all need agreement between teams regardless of MFE architecture. The architectural boundary does not make the organizational communication go away. It moves it from code review to interface contracts and integration testing.

The autonomy benefit is real, but it is narrower than the pitch suggests. You get autonomous deployment of features that live entirely within one team's MFE boundary. For features that cross boundaries, MFEs do not help and may make things harder.

Where It Makes Sense

I want to be fair to the pattern. Picture a large organization where multiple development teams work on different products that happen to share a unified portal experience. Team A owns the onboarding MFE. Team B owns the account management MFE. Team C owns the reporting MFE. These teams have different release cadences, different product managers, and different on-call rotations. They should not be bottlenecked on each other's deployments. That is where MFEs make genuine sense.

The shared surface area between these MFEs is intentionally narrow: a design system, authentication state, and a global navigation frame. Everything within each product area is self-contained. The deployment boundary matches the organizational boundary. That alignment is the key. When the architecture matches the org structure, MFEs reduce real friction.

Where It Is Over-Engineering

A common misfire is MFE architecture applied to an application where a single small team owns every micro-frontend and makes every deployment decision. There is no deployment independence problem to solve when one team makes all deployment decisions. There is no team autonomy problem when everyone is in the same stand-up. In that setup, the MFE architecture adds months of setup complexity, ongoing maintenance overhead, and difficult debugging situations, all from trying to solve an organizational problem that does not exist.

  • Single team of fewer than 8 developers. A monorepo with strong internal boundaries is almost always better
  • Applications with fewer than 5 distinct product areas. The deployment unit overhead is not worth it
  • Teams that ship fewer than two releases a week. The deployment independence benefit is minimal
  • Greenfield projects without proven product-market fit. You will pivot the architecture before you stabilize the product
  • Projects where the shared surface area between MFEs is large. Tight coupling through shared state negates the independence

My Verdict

Micro-frontends with Angular Module Federation are a legitimate solution to a real problem. That problem is: multiple teams that need true deployment independence for features that have a small shared surface area. If that description fits your situation precisely, MFEs are worth the investment.

For my personal SaaS project, I would never use MFEs. Solo developer, one deployment, no team coordination problem. For a startup with one product and three developers, I would not use MFEs. For a large organization with truly independent product teams that happen to share a portal, they are the right call.

The mistake I see most often is applying MFEs because they sound mature and scalable, not because a concrete problem that MFEs solve is actually present. Architecture should be a response to real problems, not a pre-emptive optimization for problems you might have someday. If you do not have multiple teams blocked by each other's deployments right now, invest that engineering time in delivering features instead.

The right architecture is not the most sophisticated one. It is the simplest one that solves the problems you actually have, with enough structure to grow into the problems you are likely to encounter.

References

AngularMicro-FrontendsArchitectureEnterprise