Skip to content
arrow_back Back to writings
12 MIN READ · 495 words
Listen to Article
click play to listen

Building Micro-Frontends in Angular: A Case Study

An architectural review of transitioning a monolithic frontend to an Angular micro-frontend design. We dissect module federation, shared state, and deployment isolation.

ANGULAR ARCHITECTURE MICRO-FRONTENDS

As enterprise engineering organizations scale, single monolithic codebases often become bottlenecks. Build times skyrocket, deployment schedules collide, and team velocity slows.

This case study analyzes how we partitioned a large fintech platform into Angular Micro-Frontends using Webpack Module Federation, enabling multiple autonomous teams to deploy code independently.

The Architectural Goal

The objective was to separate a corporate banking dashboard into three independent deployables:

  1. Shell (Host): Responsible for layout, global navigation, session management, and routing.
  2. KYC & Onboarding (Remote): A secure wizard module for corporate verification workflows.
  3. Transactional Ledger (Remote): A high-performance transaction dashboard and charting panel.

1. Webpack Module Federation Configuration

Webpack Module Federation allows an application to dynamically load compiled code from a remote server at runtime. In Angular, we configure this using custom builders.

Shell (Host) Configuration

The Shell configuration declares the dynamic remotes and exposes shared libraries:

// webpack.config.js (Shell/Host)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      remotes: {
        "onboarding": "onboarding@http://localhost:4201/remoteEntry.js",
      },
      shared: {
        "@angular/core": { singleton: true, strictVersion: true },
        "@angular/common": { singleton: true, strictVersion: true },
        "@angular/router": { singleton: true, strictVersion: true }
      }
    })
  ]
};

Remote (Onboarding) Configuration

The remote module compiles its features and exposes them to the host:

// webpack.config.js (Remote/Onboarding)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "onboarding",
      filename: "remoteEntry.js",
      exposes: {
        "./Module": "./src/app/onboarding/onboarding.module.ts"
      },
      shared: {
        "@angular/core": { singleton: true, strictVersion: true },
        "@angular/common": { singleton: true, strictVersion: true },
        "@angular/router": { singleton: true, strictVersion: true }
      }
    })
  ]
};

2. Dynamic Routing in the Host

In the Shell router, we load the remote module dynamically using Webpack’s import statement, wrapped in Angular’s router load children hook:

import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/module-federation';

export const APP_ROUTES: Routes = [
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
  },
  {
    path: 'onboarding',
    loadChildren: () =>
      loadRemoteModule({
        type: 'module',
        remoteEntry: 'http://localhost:4201/remoteEntry.js',
        exposedModule: './Module'
      }).then(m => m.OnboardingModule)
  }
];

3. Resolving Shared State Challenges

One of the major pitfalls of micro-frontend designs is state pollution. Sharing store states directly across remotes tightly couples the deployments.

To solve this, we implemented a Message Bus architecture using RxJS. The shell exposes a thin event bus, and remotes communicate exclusively by publishing and subscribing to serializable events:

import { Injectable } from '@angular/core';
import { Subject, filter } from 'rxjs';

export interface AppEvent {
  type: string;
  payload: any;
}

@Injectable({ providedIn: 'root' })
export class EventBusService {
  private eventSubject = new Subject<AppEvent>();

  publish(event: AppEvent) {
    this.eventSubject.next(event);
  }

  on(eventType: string) {
    return this.eventSubject.asObservable().pipe(
      filter(event => event.type === eventType)
    );
  }
}

Conclusion

Transitioning to Angular Micro-Frontends allowed us to reduce build and deploy loops from 40 minutes to under 3 minutes per team. By defining strict API boundaries and using a lightweight event bus for state updates, we achieved complete deployment isolation without sacrificing performance.