Angular Micro-frontends: Arquitetura Distribuída
Quando um app Angular cresce demais, o monolítico falha. Este guia mostra como usar Module Federation para criar uma arquitetura distribuída, dividindo o build entre equipes e eliminando conflitos de merge sem sacrificar performance.

Seu app Angular começou pequeno. Uns 15 componentes, duas rotas, um módulo de autenticação. O build levava segundos. Os merges eram tranquilos. A vida era boa.
Seis meses depois, o projeto tem 200 componentes. Três squads trabalhando no mesmo repositório. O ng build leva 4 minutos. Cada merge vira uma negociação diplomática. E o pior: quando o time de produtos quebra algo no checkout, o time de catálogo descobre na sexta à noite.
Esse não é um problema de código ruim. É um problema de arquitetura. Quando três equipes compartilham o mesmo build, o mesmo deploy e o mesmo bundle — qualquer mudança de uma afeta todas as outras.
Micro-frontends existem pra resolver exatamente isso.
O que são micro-frontends (sem o jargão)
Se você já ouviu falar de microsserviços no backend, micro-frontends são a mesma ideia aplicada ao frontend: em vez de um app monolítico gigante, você divide a aplicação em pedaços menores e independentes. Cada pedaço tem seu próprio repositório, seu próprio build e seu próprio deploy.
No Angular, isso significa que o módulo de produtos pode ser um app Angular separado. O módulo de pedidos, outro. O módulo de autenticação, outro. Cada um desenvolvido, testado e publicado por equipes diferentes — sem pisar no pé de ninguém.
Uma aplicação principal (o Shell) orquestra tudo: ela carrega os micro-frontends sob demanda, cuida do roteamento global e garante que o usuário final veja uma experiência única e coesa — sem perceber que por baixo existem vários apps independentes.
A aplicação host que orquestra tudo. Cuida do roteamento global e carrega os micro-frontends dinamicamente.
Os micro-frontends independentes. Cada um é um app Angular completo, com seu próprio build e deploy.
Dependências compartilhadas entre todos — Angular core, RxJS, design system. Carregadas uma vez, usadas por todos.
Shell + Remotos + Shared = micro-frontends que funcionam como um app só pro usuário final.
Module Federation: a tecnologia que faz tudo funcionar
A mágica por trás dos micro-frontends no Angular tem nome: Module Federation. É uma funcionalidade do Webpack 5 que permite que builds diferentes compartilhem módulos em tempo de execução. Em termos práticos: o app de produtos pode importar código do app de pedidos sem que os dois tenham sido compilados juntos.
Vamos ver como isso se monta na prática. Primeiro, a configuração do Shell — o app que vai orquestrar tudo:
// 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 } } }) ]};Agora, a configuração do micro-frontend de produtos — o app que vai ser carregado remotamente:
// 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 } } }) ]};O roteamento que conecta tudo
Com o Module Federation configurado, o próximo passo é conectar os micro-frontends ao roteador do Angular. É aqui que a coisa fica elegante: o Shell carrega cada micro-frontend sob demanda, quando o usuário navega pra aquela rota.
// 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' }];Repara: loadRemoteModule funciona como o loadChildren que você já conhece do lazy loading — mas em vez de carregar um módulo local, ele busca um módulo de outro servidor. Pro Angular Router, é transparente. Pro usuário, é invisível.
Carregamento dinâmico: não carregue o que o usuário não precisa
O roteamento lazy já resolve parte do problema de performance. Mas em apps grandes, você precisa de mais controle. Um serviço de carregamento dinâmico evita que o mesmo micro-frontend seja baixado duas vezes e lida com falhas de rede:
@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 } }}Comunicação entre micro-frontends
Se cada micro-frontend é independente, como eles conversam entre si? O usuário faz login no módulo de autenticação — como o módulo de pedidos fica sabendo?
A resposta é um padrão que o backend já usa há anos: event bus. Um serviço central que publica e recebe eventos tipados. Nenhum micro-frontend conhece o outro diretamente — eles só conhecem os eventos.
// 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]) ); }}Os erros que todo mundo comete (e como evitar)
Micro-frontends resolvem problemas reais, mas criam novos se implementados sem cuidado. Esses são os três erros mais comuns — e os três eu já vi em produção.
1. Não ter fallback quando um micro-frontend falha
Se o servidor do micro-frontend de produtos cair, o que o usuário vê? Se a resposta for "uma tela branca", você tem um problema. Todo micro-frontend remoto precisa de um componente de fallback:
@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. Compartilhar estado global entre micro-frontends
O instinto natural é criar um store global que todos os micro-frontends acessam. Resista a esse instinto. Estado compartilhado global cria acoplamento forte — exatamente o que micro-frontends existem pra eliminar. Cada micro-frontend mantém seu próprio estado e se comunica via eventos:
@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 no carregamento dinâmico
Quando um micro-frontend é carregado e depois descarregado, ele precisa limpar tudo atrás de si — subscriptions, referências a componentes, timers. Se não limpar, o app acumula memória a cada navegação:
@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()); }}Performance: o que otimizar primeiro
Micro-frontends podem melhorar ou piorar a performance do seu app — depende de como você configura o compartilhamento de dependências e a estratégia de carregamento.
Importe só o que você usa
// ❌ 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 { }Pré-carregamento inteligente
Nem todo micro-frontend deve ser carregado sob demanda. O módulo de produtos num e-commerce vai ser acessado por 90% dos usuários — faz sentido pré-carregar. O módulo de relatórios? Só durante horário comercial. Você pode criar uma estratégia de preload baseada em contexto:
@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({...}) }];Quando NÃO usar micro-frontends
Essa é a seção mais importante do artigo.
Micro-frontends adicionam complexidade real: mais repositórios pra manter, mais pipelines de CI/CD pra configurar, mais pontos de falha em produção, mais overhead de comunicação entre módulos. Essa complexidade só se justifica quando o problema que ela resolve é maior que a dor que ela cria.
Migrando um monolito: por onde começar
Se você decidiu que micro-frontends fazem sentido pro seu contexto, não tente migrar tudo de uma vez. A abordagem que funciona é incremental: mantenha as rotas legadas funcionando enquanto adiciona as novas rotas federadas ao lado delas.
// 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 { }Quando o micro-frontend de produtos estiver estável em produção, você remove a rota legada. Um domínio por vez. Sem big bang.


