blense.
HomeFrontendCodeAINewsGuidesCheatsheetsAbout

Technology decoded, always.

A curated read on AI, product, and the culture behind the code. No noise, no hype.

blense.

Technology with focus. Stories about what innovation really changes.

Sections

FrontendArtificial IntelligenceCode & DevTech News

Blense

AboutRSSPrivacy policyTermsCookies
© 2026 Blense · blense.fun
Frontend

Angular Micro-frontends: How to Scale Large Applications and Eliminate Monolithic Build Times with Module Federation

Discover how to break down large Angular monoliths using Module Federation to enable independent deployments and seamless team scaling.

Gby Genildo SouzaJun 2110 min read
Angular Micro-frontends: How to Scale Large Applications and Eliminate Monolithic Build Times with Module Federation
Angular Micro-frontends Architecture
  • 1Micro-frontends solve monolithic scaling issues by allowing independent development, deployment, and builds for different teams.
  • 2Module Federation enables Angular apps to share modules at runtime, allowing separate applications to function as a single cohesive unit.
  • 3The Shell application acts as an orchestrator that loads remote micro-frontends on demand via the Angular router.
  • 4Setting shared dependencies to singleton: true is critical to prevent code duplication and maintain a single instance of Angular.
  • 5Communication between independent micro-frontends should be managed through an event bus pattern to keep modules decoupled.

Your Angular app started small. About 15 components, two routes, an authentication module. The build took seconds. Merges were smooth. Life was good.

Six months later, the project has 200 components. Three squads working on the same repository. The ng build takes 4 minutes. Every merge becomes a diplomatic negotiation. And the worst part: when the product team breaks something in the checkout, the catalog team finds out on Friday night.

This isn't a bad code problem. It's an architecture problem. When three teams share the same build, the same deploy, and the same bundle — any change from one affects all the others.

Micro-frontends exist to solve exactly this.

What are micro-frontends (without the jargon)

If you've heard of microservices on the backend, micro-frontends are the same idea applied to the frontend: instead of one giant monolithic app, you split the application into smaller, independent pieces. Each piece has its own repository, its own build, and its own deploy.

The elite dev's arsenal.
A Guide to HTML Loading Strategies That Will Speed Up Your Load Times9 minMicrofrontends with Angular and Nx: what nobody tells you15 minStop Using useMemo Wrong: A Practical Guide to React Performance14 minAngular 22: Replace ControlValueAccessor with Signal Forms13 min

In Angular, this means the product module can be a separate Angular app. The orders module, another. The authentication module, another. Each one developed, tested, and published by different teams — without stepping on anyone's toes.

A main application (the Shell) orchestrates everything: it loads the micro-frontends on demand, handles global routing, and ensures the end user sees a unique and cohesive experience — without realizing that underneath there are several independent apps.

The three pillars of a micro-frontend architecture
🏠
Shell

A aplicação host que orquestra tudo. Cuida do roteamento global e carrega os micro-frontends dinamicamente.

📦
Remotes

Os micro-frontends independentes. Cada um é um app Angular completo, com seu próprio build e deploy.

🔗
Shared

Dependências compartilhadas entre todos — Angular core, RxJS, design system. Carregadas uma vez, usadas por todos.

Shell + Remotes + Shared = micro-frontends that work as a single app for the end user.

Module Federation: the technology that makes it all work

The magic behind micro-frontends in Angular has a name: Module Federation. It's a Webpack 5 feature that allows different builds to share modules at runtime. In practical terms: the product app can import code from the orders app without the two having been compiled together.

Let's see how this is set up in practice. First, the Shell configuration — the app that will orchestrate everything:

JAVASCRIPT
// webpack.config.js — configuração do Shellconst ModuleFederationPlugin = require("@module-federation/webpack");module.exports = {  plugins: [    new ModuleFederationPlugin({      name: "shell",      remotes: {        // Cada micro-frontend é registrado aqui        "mfe-products": "mfeProducts@http://localhost:4201/remoteEntry.js",        "mfe-orders": "mfeOrders@http://localhost:4202/remoteEntry.js"      },      shared: {        // Dependências que todos compartilham — carregadas uma vez só        "@angular/core": { singleton: true, strictVersion: true },        "@angular/common": { singleton: true, strictVersion: true },        "@angular/router": { singleton: true, strictVersion: true }      }    })  ]};

Now, the product micro-frontend configuration — the app that will be loaded remotely:

JAVASCRIPT
// webpack.config.js — configuração do micro-frontend de produtosconst ModuleFederationPlugin = require("@module-federation/webpack");module.exports = {  plugins: [    new ModuleFederationPlugin({      name: "mfeProducts",      filename: "remoteEntry.js", // o ponto de entrada que o Shell vai carregar      exposes: {        // O que esse micro-frontend disponibiliza pro mundo        "./ProductsModule": "./src/app/products/products.module.ts"      },      shared: {        "@angular/core": { singleton: true, strictVersion: true },        "@angular/common": { singleton: true, strictVersion: true }      }    })  ]};
ℹ️

The singleton: true in shared dependencies is critical. Without this, each micro-frontend would load its own copy of Angular — duplicating code, breaking dependency injection, and bloating the bundle. With singleton: true, everyone shares the same instance.

The routing that connects everything

With Module Federation configured, the next step is to connect the micro-frontends to the Angular router. This is where it gets elegant: the Shell loads each micro-frontend on demand, when the user navigates to that route.

TYPESCRIPT
// app-routing.module.ts — Shellimport { loadRemoteModule } from '@angular-architects/module-federation';const routes: Routes = [  {    path: 'products',    loadChildren: () => loadRemoteModule({      type: 'module',      remoteEntry: 'http://localhost:4201/remoteEntry.js',      exposedModule: './ProductsModule'    }).then(m => m.ProductsModule)  },  {    path: 'orders',    loadChildren: () => loadRemoteModule({      type: 'module',      remoteEntry: 'http://localhost:4202/remoteEntry.js',      exposedModule: './OrdersModule'    }).then(m => m.OrdersModule)  },  { path: '', redirectTo: '/products', pathMatch: 'full' }];

Notice: loadRemoteModule works like the loadChildren you already know from lazy loading — but instead of loading a local module, it fetches a module from another server. To the Angular Router, it's transparent. To the user, it's invisible.

Dynamic loading: don't load what the user doesn't need

Lazy routing already solves part of the performance problem. But in large apps, you need more control. A dynamic loading service prevents the same micro-frontend from being downloaded twice and handles network failures:

TYPESCRIPT
@Injectable({ providedIn: 'root' })export class DynamicModuleService {  private loadedModules = new Set<string>();  async loadModule(    remoteName: string,    exposedModule: string,    remoteEntry: string  ) {    const moduleKey = `${remoteName}-${exposedModule}`;    if (this.loadedModules.has(moduleKey)) {      return; // já carregou, não busca de novo    }    try {      await loadRemoteModule({ type: 'module', remoteEntry, exposedModule });      this.loadedModules.add(moduleKey);    } catch (error) {      console.error(`Falha ao carregar ${moduleKey}:`, error);      throw error; // deixa o componente de fallback cuidar    }  }}

Communication between micro-frontends

If each micro-frontend is independent, how do they talk to each other? The user logs in to the authentication module — how does the orders module find out?

The answer is a pattern the backend has been using for years: event bus. A central service that publishes and receives typed events. No micro-frontend knows about the other directly — they only know about the events.

TYPESCRIPT
// Tipos dos eventos — o contrato entre micro-frontendsinterface EventPayloadMap {  'USER_LOGGED_IN': { userId: string; role: string };  'CART_UPDATED': { itemCount: number; total: number };  'NAVIGATION_REQUESTED': { path: string; params?: any };}@Injectable({ providedIn: 'root' })export class MicroFrontendEventBus {  private bus = new Subject<{ type: string; payload: any; source: string }>();  // Publicar evento — type-safe  publish<T extends keyof EventPayloadMap>(    type: T,    payload: EventPayloadMap[T],    source: string  ): void {    this.bus.next({ type, payload, source });  }  // Ouvir evento — type-safe  on<T extends keyof EventPayloadMap>(    eventType: T  ): Observable<EventPayloadMap[T]> {    return this.bus.pipe(      filter(event => event.type === eventType),      map(event => event.payload as EventPayloadMap[T])    );  }}
💡

The EventPayloadMap is the secret. It works like a contract: every micro-frontend knows which events exist and what the format of each one is. If someone changes the payload of CART_UPDATED, TypeScript warns everywhere that consumes this event. No runtime surprises.

The mistakes everyone makes (and how to avoid them)

Micro-frontends solve real problems, but create new ones if implemented carelessly. These are the three most common mistakes — and all three I've seen in production.

1. Not having a fallback when a micro-frontend fails

If the product micro-frontend server goes down, what does the user see? If the answer is "a white screen," you have a problem. Every remote micro-frontend needs a fallback component:

TYPESCRIPT
@Component({  template: `    <div class="error-fallback">      <h3>Este módulo está temporariamente indisponível</h3>      <p>Estamos trabalhando pra restaurar. Tente novamente em alguns minutos.</p>      <button (click)="retry()">Tentar novamente</button>    </div>  `})export class MicroFrontendFallbackComponent {  retry() {    window.location.reload();  }}

2. Sharing global state between micro-frontends

The natural instinct is to create a global store that all micro-frontends access. Resist this instinct. Shared global state creates tight coupling — exactly what micro-frontends exist to eliminate. Each micro-frontend maintains its own state and communicates via events:

TYPESCRIPT
@Injectable()export class BoundedContextService {  // Cada micro-frontend mantém seu próprio estado  private localState = new BehaviorSubject(initialState);  // Comunica via eventos, nunca compartilha estado diretamente  notifyDomainEvent(event: DomainEvent): void {    this.eventBus.publish('DOMAIN_EVENT', {      context: this.contextName,      event: event,      timestamp: Date.now()    }, this.contextName);  }}

3. Memory leaks in dynamic loading

When a micro-frontend is loaded and then unloaded, it needs to clean up everything behind it — subscriptions, component references, timers. If it doesn't clean up, the app accumulates memory with every navigation:

TYPESCRIPT
@Injectable()export class MicroFrontendLifecycleService implements OnDestroy {  private subscriptions = new Map<string, Subscription>();  private loadedComponents = new Map<string, ComponentRef<any>>();  unloadMicroFrontend(name: string): void {    // Destruir componente    const component = this.loadedComponents.get(name);    if (component) {      component.destroy();      this.loadedComponents.delete(name);    }    // Cancelar subscriptions    const sub = this.subscriptions.get(name);    if (sub) {      sub.unsubscribe();      this.subscriptions.delete(name);    }  }  ngOnDestroy(): void {    // Na destruição do serviço, limpa tudo    this.loadedComponents.forEach(c => c.destroy());    this.subscriptions.forEach(s => s.unsubscribe());  }}
ℹ️

Rule of thumb: if your app gets slower the more the user navigates between sections, there is likely a memory leak in the dynamic loading. Use Chrome DevTools → Memory → Heap Snapshot before and after navigating to identify the leak.

Performance: what to optimize first

Micro-frontends can improve or worsen your app's performance — it depends on how you configure dependency sharing and the loading strategy.

Import only what you use

TYPESCRIPT
// ❌ Evite — importar o Angular Material inteiroimport { MaterialModule } from './material.module'; // tudo junto// ✅ Faça assim — só o que o micro-frontend precisaimport { MatButtonModule } from '@angular/material/button';import { MatCardModule } from '@angular/material/card';import { MatInputModule } from '@angular/material/input';@NgModule({  imports: [MatButtonModule, MatCardModule, MatInputModule]})export class ProductsModule { }

Intelligent preloading

Not every micro-frontend should be loaded on demand. The product module in an e-commerce site will be accessed by 90% of users — it makes sense to preload it. The reports module? Only during business hours. You can create a preload strategy based on context:

TYPESCRIPT
@Injectable()export class SmartPreloadingStrategy implements PreloadingStrategy {  preload(route: Route, load: () => Observable<any>): Observable<any> {    const priority = route.data?.['preload'];    if (priority === 'high') return load(); // sempre pré-carrega    if (priority === 'business-hours') {      const hour = new Date().getHours();      if (hour >= 9 && hour <= 17) return load();    }    return of(null); // carrega só quando o usuário navegar  }}// Nas rotas:const routes: Routes = [  {    path: 'products',    data: { preload: 'high' }, // sempre pré-carrega    loadChildren: () => loadRemoteModule({...})  },  {    path: 'reports',    data: { preload: 'business-hours' }, // só no horário comercial    loadChildren: () => loadRemoteModule({...})  }];

When NOT to use micro-frontends

This is the most important section of the article.

Micro-frontends add real complexity: more repositories to maintain, more CI/CD pipelines to configure, more points of failure in production, more overhead of communication between modules. This complexity is only justified when the problem it solves is greater than the pain it creates.

💡

Use micro-frontends when: multiple teams work on distinct domains of the same application and merge conflicts, build time, and deploy are delaying deliveries. Don't use when: the team is small, the app is manageable, and the real problem is code organization — which can be solved with lazy loading and good modularization, without needing Module Federation.

Migrating a monolith: where to start

If you've decided that micro-frontends make sense for your context, don't try to migrate everything at once. The approach that works is incremental: keep the legacy routes working while adding the new federated routes alongside them.

TYPESCRIPT
// Migração incremental — rotas legadas e federadas coexistem@NgModule({  imports: [RouterModule.forRoot([    // Rota legada — ainda funciona normalmente    {      path: 'legacy-products',      component: ProductsComponent    },    // Nova rota federada — micro-frontend independente    {      path: 'products',      loadChildren: () => loadRemoteModule({        type: 'module',        remoteEntry: 'http://localhost:4201/remoteEntry.js',        exposedModule: './ProductsModule'      }).then(m => m.ProductsModule)    }  ])]})export class AppRoutingModule { }

When the product micro-frontend is stable in production, you remove the legacy route. One domain at a time. No big bang.

Takeaways from this article
  • 1Micro-frontends split a monolithic app into independent apps with their own build, deploy, and repository.
  • 2Module Federation (Webpack 5) allows separate builds to share modules at runtime — the Shell loads the Remotes on demand.
  • 3Communication via typed event bus — each micro-frontend knows the events, never the other micro-frontend directly.
  • 4Always implement fallbacks — if one micro-frontend goes down, the rest of the app should keep working.
  • 5Don't share global state — each micro-frontend maintains its state and communicates via events.
  • 6Clean up references and subscriptions when unloading micro-frontends — memory leaks are the most common silent bug.
  • 7Micro-frontends add real complexity. Only use them when multiple teams in distinct domains justify this complexity.
  • 8Migrate incrementally — legacy and federated routes coexist until the new micro-frontend is stable.
Angular
Want to master modern Angular?

Signals, incremental hydration, new control flow — Angular has changed. Explore the other articles in the series to understand what changed and how to apply it.

Explorar

#JavaScript#TypeScript#Angular

The elite dev's arsenal.

Debugging CSS: The Forgotten Margin and Frontend Lessons
Frontend

Debugging CSS: The Forgotten Margin and Frontend Lessons

A forgotten CSS property turned a minor layout glitch into a two-week ordeal. Learn how to avoid emotional debugging and fix complex UI issues faster.

Genildo Souza · Aug 2 · 8 min
Stop Using useMemo Wrong: A Practical Guide to React Performance
Frontend

Stop Using useMemo Wrong: A Practical Guide to React Performance

Master React useMemo with this guide. Learn when to memoize, how to fix dependency array bugs, and why over-optimization can hurt your performance.

Genildo Souza · Jul 8 · 14 min
CSS Animations: How to Build Performant, Professional Interfaces Without Messy Code
Frontend

CSS Animations: How to Build Performant, Professional Interfaces Without Messy Code

Bring your web interfaces to life. Learn when to use CSS transitions versus keyframes and how to optimize your animations for peak performance.

Genildo Souza · Jul 4 · 17 min
In this article
  • What are micro-frontends (without the jargon)
  • Module Federation: the technology that makes it all work
  • The routing that connects everything
  • Dynamic loading: don't load what the user doesn't need
  • Communication between micro-frontends
  • The mistakes everyone makes (and how to avoid them)
  • 1. Not having a fallback when a micro-frontend fails
  • 2. Sharing global state between micro-frontends
  • 3. Memory leaks in dynamic loading
  • Performance: what to optimize first
  • Import only what you use
  • Intelligent preloading
  • When NOT to use micro-frontends
  • Migrating a monolith: where to start