Programação & DevProgramação & Dev · 27 jul 2026
Microfrontends com Angular e Nx: o que ninguém conta
Todo tutorial ensina a configurar microfrontends em minutos, mas poucos revelam o caos arquitetural que surge após meses de deploy em produção.
Desafios reais de Microfrontends com Angular
Microfrontends frequentemente levam três meses para implementar e nove meses para estabilizar devido a problemas de produção não previstos.
O compartilhamento de estado global entre remotes anula a independência dos deploys, tornando o sistema acoplado e difícil de manter.
Conflitos de CSS entre remotes são problemas não determinísticos que exigem o uso de namespaces rigorosos para evitar vazamento de estilos.
O uso do Nx com configurações estritas de versionamento é essencial para evitar falhas silenciosas causadas por incompatibilidades de bibliotecas.
Manter versões diferentes do Angular em um mesmo monorepo Nx é inviável, sendo necessário realizar migrações globais em todo o workspace.
Todo tutorial mostra como configurar o Module Federation em 20 minutos. Nenhum mostra o que acontece 6 meses depois em produção — o CSS que vaza entre remotes, a versão do Angular que derruba o shell, o estado compartilhado que tornou o deploy independente impossível, e o custo organizacional que o diagrama arquitetural não captura.
A realidade que o tutorial não mostra
O tutorial termina onde o problema começa.
A maioria das equipes que implanta microfrontends chega a produção em 3 meses e gasta os próximos 9 meses resolvendo problemas que nenhum tutorial mencionou.
O modelo é sedutor: times independentes, deploys independentes, autonomia de tecnologia. Na prática, a independência prometida tem um custo que só aparece quando um time tenta subir uma versão do Angular enquanto outro ainda está no ciclo anterior. Ou quando o CSS do remote de checkout começa a afetar o layout do shell. Ou quando você descobre que o estado compartilhado que colocou em um serviço global tornou o deploy independente tecnicamente impossível.
A arquitetura de microfrontends não falha no código. Falha nas fronteiras que você não definiu.
→ O número que importa Em uma pesquisa com 20+ projetos de microfrontend de 2021-2023, o padrão era consistente: 3 meses para implementar, 9 meses para estabilizar. Os problemas de state compartilhado, CSS leaking e version mismatch não aparecem em demos — aparecem quando os times começam a trabalhar de forma genuinamente independente.
A arquitetura de microfrontends não falha no código. Falha nas fronteiras que você não definiu.
Os 5 problemas de produção
O que acontece 6 meses depois do deploy.
Problema 01 — Version mismatch — o crash silencioso: Time A usa Angular 21. Time B faz upgrade para Angular 22 com a nova API de Signal Forms. O shell carrega os dois. O Module Federation tenta reconciliar as versões com singleton: true. Às vezes funciona. Às vezes o remote B crasha sem mensagem de erro óbvia.
Problema 02 — CSS leaking — o estilo que não tem fronteiras: O remote de checkout define .btn { background: red } sem escopo. O shell tem .btn { background: blue }. Dependendo da ordem de carregamento, um sobrescreve o outro. Em produção, a ordem muda com caching. O bug é não determinístico.
Problema 03 — Estado global — o deploy que não é independente: Para compartilhar o usuário autenticado, alguém colocou o estado em um NgRx Store compartilhado entre todos os remotes. Agora, qualquer mudança no shape do estado exige deploy coordenado de todos os remotes. O deploy "independente" deixou de ser independente.
Problema 04 — DX em desenvolvimento — 4 terminais para 1 feature: Para rodar o feature de checkout localmente, o dev precisa iniciar shell, catalog-mfe, admin-mfe e checkout-mfe. O tempo de setup local triplicou. A autonomia do time veio com um overhead de DX que ninguém contabilizou.
Problema 05 — Bundle size — o compartilhamento que não compartilhou: Cada remote empacotou sua própria cópia do Angular Material porque a configuração de shared não incluiu as entradas secundárias (@angular/material/button, @angular/material/dialog). O bundle total ficou 3x maior do que o monolito original.
Solução 01 — Version mismatch
Governança de versões: o Nx como árbitro
O Module Federation permite versões diferentes. O Nx, por padrão, não permite — e essa restrição é um feature, não um bug.
// ❌ Configuração que causa crashes silenciosos
// module-federation.config.ts (remote checkout)
export const config: ModuleFederationConfig = {
name: 'checkout',
exposes: {
'./Module': './src/app/remote-entry/entry.module.ts',
},
shared: {
// ❌ singleton sem requiredVersion = aceita qualquer versão
// Se shell tem Angular 21 e remote tem Angular 22,
// o Module Federation carrega a versão do shell silenciosamente
'@angular/core': { singleton: true },
'@angular/common': { singleton: true },
},
};
// ✅ Governança de versão explícita com Nx
// libs/shared-mf-config/base.config.ts
import { shareAll } from '@nx/angular/module-federation';
export const baseConfig = {
shared: {
// ✅ requiredVersion: 'auto' lê do package.json do workspace
// strictVersion: true → falha se versão incompatível
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto', // ← Nx resolve do root package.json
},
'@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/forms': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
}
};
# Ver quais apps precisam ser (re)deployados após mudança em libs/auth
nx affected --target=build --base=main
# Output:
# Affected projects:
# shell ← host sempre afetado quando auth muda
# checkout-mfe ← usa AuthGuard da libs/auth
# admin-mfe ← usa AuthService da libs/auth
# NOT affected:
# catalog-mfe ← não depende de libs/auth
# Quando você ATUALIZA o Angular (major version):
nx migrate @angular/core@22
nx migrate --run-migrations
# Aplica migrações em TODOS os apps do workspace de uma vez
⚠️ A regra do Angular em monorepo Nx Quando você faz upgrade do Angular em um workspace Nx, todos os apps do workspace sobem juntos. Isso é intencional — a alternativa é o inferno de versões mistas. Se dois times precisam de versões diferentes de Angular, eles precisam de repositórios separados — e aí perdem as vantagens do monorepo Nx.
Solução 02 — CSS leaking
Isolamento de estilos: ViewEncapsulation não é suficiente
O ViewEncapsulation do Angular isola estilos de componentes mas não resolve os estilos globais que cada remote define.
/* ❌ Sem namespace — vaza para o shell e outros remotes */
.card { background: #1a1a2e; border-radius: 8px; }
.btn { background: #e94560; color: white; }
.overlay { z-index: 1000; } /* ← conflita com overlays do shell */
/* ✅ Solução 1: Namespace SCSS por remote */
/* checkout-mfe/src/styles.scss */
.checkout-mfe {
.card { background: #1a1a2e; border-radius: 8px; }
.btn { background: #e94560; color: white; }
.overlay { z-index: 100; }
}
/* No componente raiz do remote: */
/* <div class="checkout-mfe"><router-outlet /></div> */
/* ✅ Solução 2: CSS Custom Properties do Design System no shell */
/* O shell define as variáveis. Os remotes consomem. Nunca definem. */
:root {
--ds-color-primary: #dd0031;
--ds-color-surface: #161b22;
--ds-radius-card: 8px;
--ds-z-overlay: 200;
--ds-z-modal: 300;
--ds-z-toast: 400;
}
/* Remotes APENAS consomem variáveis — nunca definem novos z-index absolutos */
✅ A regra de ouro de CSS em MFEs O shell é o único dono dos estilos globais. Remotes consomem CSS Custom Properties definidas pelo shell. Nenhum remote define valores absolutos de z-index — esse é o bug mais difícil de debugar em produção, porque z-index depende do contexto de empilhamento e o shell não controla o contexto do remote.
Solução 03 — Estado compartilhado
O estado que destrói o deploy independente
⚠️ O anti-padrão mais comum em MFEs Colocar o estado do usuário autenticado em um NgRx Store compartilhado via Module Federation entre todos os remotes. Parece certo — "é estado global, deve ficar no NgRx global". O problema é que qualquer mudança no shape desse estado exige que todos os remotes sejam atualizados e deployados ao mesmo tempo. O deploy "independente" deixa de ser independente.
// libs/core/src/lib/event-bus.service.ts
// ✅ O Event Bus é o contrato entre remotes — não o shape do estado
import { Injectable } from '@angular/core';
import { Subject, filter, map } from 'rxjs';
export interface MfeEvent {
type: string;
payload: unknown;
source: string;
}
export type UserAuthenticatedEvent = MfeEvent & {
type: 'user.authenticated';
payload: {
userId: string;
tenantId: string;
// ✅ Apenas os campos que outros remotes REALMENTE precisam
};
};
export type CartUpdatedEvent = MfeEvent & {
type: 'cart.updated';
payload: { itemCount: number; total: number };
};
@Injectable({ providedIn: 'root' })
export class EventBusService {
private bus$ = new Subject<MfeEvent>();
emit(event: MfeEvent): void {
this.bus$.next(event);
}
on<T extends MfeEvent>(type: T['type']) {
return this.bus$.pipe(
filter(e => e.type === type),
map(e => e as T)
);
}
}
// ✅ No shell — emite após autenticação
// eventBus.emit({ type: 'user.authenticated', payload: { userId, tenantId }, source: 'shell' });
// ✅ No checkout-mfe — escuta sem saber nada do shell
// eventBus.on<UserAuthenticatedEvent>('user.authenticated').subscribe(...)
// Quando o shape do usuário muda, você versiona o evento:
// 'user.authenticated.v2' — backward compatible, remotes migram no seu ritmo
// ✅ Se você PRECISA de estado compartilhado, compartilhe o mínimo
export const AUTH_CONTEXT = new InjectionToken<AuthContext>('AUTH_CONTEXT');
export interface AuthContext {
// Apenas o que é genuinamente necessário para TODOS os remotes
userId: string | null;
tenantId: string | null;
isAuthenticated: boolean;
// ❌ NÃO coloque: permissions, roles, profile, preferences
}
Solução 04 — Developer Experience
Desenvolvimento local sem 4 terminais abertos
// shell/src/assets/module-federation.manifest.json
// ✅ Dynamic Module Federation — a URL do remote é resolvida em runtime
{
"checkout": "http://localhost:4201",
"catalog": "http://localhost:4202",
"admin": "http://localhost:4203"
}
// shell/src/assets/module-federation.manifest.staging.json
{
"checkout": "https://checkout.staging.example.com",
"catalog": "https://catalog.staging.example.com",
"admin": "https://admin.staging.example.com"
}
# ✅ Inicia o shell + checkout em modo live
# catalog e admin são servidos como static (sem live reload)
nx serve shell --devRemotes=checkout
# Inicia o shell + dois remotes em modo live
nx serve shell --devRemotes=checkout,catalog
# Com Dynamic Module Federation — aponta para staging:
# O dev desenvolve checkout localmente, catalog e admin vêm do staging
cp src/assets/module-federation.manifest.staging.json \
src/assets/module-federation.manifest.json
→ O padrão Nx recomendado para DX Configure um target
serve-with-staging-remotesnoproject.jsondo shell. O dev inicia um único terminal, o shell carrega o remote local que está desenvolvendo e os outros remotes do staging. Nenhum mock, nenhum stub — ambiente real com um remote vivo.
Solução 05 — Bundle size
O compartilhamento que não compartilhou nada
// ❌ Shared incompleto — cada remote tem sua própria cópia do Material
shared: {
'@angular/material': { singleton: true, strictVersion: true },
// Problema: @angular/material/button é uma entrada separada
// → Cada remote vai empacotar sua própria cópia desses módulos
// → Bundle total: shell(mat/button) + checkout(mat/button) = 3x
}
// ✅ shareAll com entradas secundárias — a configuração correta
import { shareAll } from '@nx/angular/module-federation';
// shareAll compartilha AUTOMATICAMENTE todos os pacotes do package.json
// Incluindo entradas secundárias como @angular/material/button
export const sharedDeps = shareAll({
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
});
// module-federation.config.ts de cada remote:
export const config: ModuleFederationConfig = {
name: 'checkout',
exposes: { './Module': './src/app/remote-entry/entry.module.ts' },
shared: {
...sharedDeps,
},
};
// Resultado com shareAll:
// Bundle antes: shell(300KB) + checkout(280KB) + catalog(275KB) = 855KB
// Bundle depois: shell(300KB) + checkout(80KB) + catalog(75KB) = 455KB
// Redução de ~47% no total transferido
✅ Nx bundle analysis integrada Rode
nx build checkout --analyzepara abrir o Webpack Bundle Analyzer e identificar quais pacotes estão sendo duplicados entre o shell e os remotes. Faça essa análise antes do primeiro deploy em produção — é muito mais difícil corrigir depois que o cache do CDN está propagado.
Quando microfrontends não são a resposta
Critério | Microfrontends | Monolito modular com Nx |
|---|---|---|
Tamanho do time | ✅ 5+ times paralelos | ✅ 1-3 times — custo de MFE não compensa |
Cadência de deploy | ✅ Times com cadências diferentes | ✅ Mesma cadência — deploy unificado é mais simples |
Domínios de negócio | ✅ Domínios genuinamente independentes | ❌ Muita interdependência — MFE cria fronteiras artificiais |
Bundle inicial | ❌ Maior — overhead de Module Federation | ✅ Menor |
Complexidade de config | ❌ Alta — versões, CSS, estado, DX | ✅ Baixa |
DX local | ⚠️ Degradada sem configuração cuidadosa | ✅ Simples — |
→ A recomendação do Nx A documentação oficial do Nx é direta: "Se você quer otimizar builds e não precisa de deploys independentes, use nosso guia de Faster Builds com Module Federation." Você pode ter builds incrementais, cache inteligente e repositório único sem a complexidade de remotes deployados independentemente. Essa é a resposta certa para a maioria dos times de até 30 pessoas.
O checklist antes de adotar microfrontends
✅ 5+ times que precisam deployar com cadências genuinamente diferentes
✅ Domínios de negócio claros que não compartilham estado além de autenticação
✅ SLAs independentes — uma falha no checkout não deve afetar o catalog
✅ Design system estabelecido via CSS Custom Properties antes de começar
✅ Governança de versões — política clara para upgrades do Angular
❌ Time único que "vai crescer" — antecipe com módulos lazy, não com MFE
❌ Complexidade técnica como objetivo — MFE é para problemas organizacionais
⚠️ Estado compartilhado extenso entre os remotes — redesenhe os limites antes
⚠️ SEO crítico — MFEs com Module Federation são CSR por padrão
Setup correto do zero
Cada passo abaixo mitiga um risco real de produção. Pular um não gera erro imediato — gera o problema que você vai debugar em produção 6 meses depois.
Passo 1 — Criar o workspace Nx com preset Angular:
npx create-nx-workspace@latest minha-org --preset=angular-monorepo
Risco mitigado: sem o preset correto, o workspace não configura o @nx/enforce-module-boundaries automaticamente. Sem essa regra de lint, remotes vão importar uns dos outros diretamente — criando acoplamento que torna o deploy independente impossível. Você só vai descobrir o problema quando tentar deployar o checkout sem o catalog e o build quebrar.
Passo 2 — Gerar o shell com Dynamic Module Federation:
nx g @nx/angular:host shell --remotes=checkout,catalog,admin --dynamic=true
Risco mitigado: sem --dynamic=true, a URL de cada remote fica hardcoded no bundle do shell em tempo de build. Mudar a URL de um remote em produção exige rebuild e redeploy do shell inteiro — destruindo a independência de deploy. Com o manifest JSON dinâmico, você muda a URL em runtime sem tocar no shell.
Passo 3 — Criar as libs compartilhadas antes de qualquer feature:
nx g @nx/angular:lib libs/ui
nx g @nx/angular:lib libs/auth
nx g @nx/angular:lib libs/core
Risco mitigado: criar libs depois que os remotes já existem significa que cada remote já tem sua própria versão de componentes e serviços. Unificar depois é uma refatoração dolorosa que os times resistem por meses. Criar antes força a disciplina de "código compartilhado vai para libs" desde o primeiro commit — o acoplamento nunca existe para ser removido.
Passo 4 — Configurar a base de shared config em libs/shared-mf-config:
// shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' })
Risco mitigado: sem uma config base centralizada, cada remote define seu próprio shared — e invariavelmente alguém esquece de incluir as entradas secundárias do Angular Material ou do CDK. O resultado é o Problema 05: bundle 3x maior. Com a config base no monorepo, um único PR corrige todos os remotes ao mesmo tempo.
Passo 5 — Estabelecer o Design System no shell antes do primeiro remote: Defina todas as CSS Custom Properties em shell/src/styles.scss e documente a convenção de namespace (ex: .checkout-mfe { ... }).
Risco mitigado: sem tokens CSS centralizados, cada remote define seus próprios valores de cor, espaçamento e z-index. Em 3 meses você tem 4 sistemas de design divergentes e CSS leaking não determinístico (Problema 02). Refatorar isso depois significa convencer 4 times a parar de entregar features para alinhar variáveis CSS — conversa que nunca acontece.
Passo 6 — Implementar o EventBusService antes do primeiro fluxo cross-remote: libs/core/src/lib/event-bus.service.ts com tipos de evento em libs/core/src/lib/events.ts.
Risco mitigado: sem um contrato de comunicação estabelecido, o primeiro dev que precisar passar dados entre remotes vai colocar estado no NgRx global — criando o Problema 03. O EventBus força a equipe a pensar em contratos versionados antes de criar a dependência de estado. É muito mais fácil estabelecer o padrão certo antes do primeiro fluxo do que remover estado global depois de 3 times dependerem dele.
# Estrutura de workspace que aguenta 2 anos de produção
minha-org/
├── apps/
│ ├── shell/ # Host — roteamento e layout global
│ ├── checkout-mfe/ # Remote — domínio de pagamento
│ ├── catalog-mfe/ # Remote — domínio de catálogo
│ └── admin-mfe/ # Remote — domínio administrativo
├── libs/
│ ├── ui/ # Design System — components, tokens CSS
│ ├── auth/ # AuthGuard, AuthContext, EventBus types
│ ├── core/ # EventBus, interceptors, error handler
│ ├── shared-mf-config/ # Base config de Module Federation
│ └── data-access/ # APIs compartilhadas (opcional)
├── module-federation.manifest.json # dev
├── module-federation.manifest.staging.json # staging
├── module-federation.manifest.prod.json # produção
└── nx.json
O que realmente importa
O Module Federation resolve problemas técnicos de carregamento federado. O Nx resolve problemas técnicos de build e dependência. Nenhum dos dois resolve o problema real: microfrontends são uma solução para o Conway's Law, não para performance de build.
Module Federation foca no carregamento federado, enquanto Nx gerencia build e dependências. Nenhuma ferramenta resolve a complexidade intrínseca dos microfrontends.
Microfrontends são eficazes para equipes que precisam trabalhar de forma independente, desde que os domínios de negócio estejam bem definidos.
Sem essa clareza, o projeto sofre com a complexidade distribuída e o acoplamento típico de monolitos, prejudicando o resultado final.
Se o seu problema é que dois times não conseguem trabalhar no mesmo código sem pisar um no outro, microfrontends podem ajudar — mas apenas se os domínios de negócio tiverem fronteiras claras o suficiente para que os remotes sejam genuinamente independentes.
A maioria dos projetos que adota microfrontends sem essa clareza de domínio acaba com o pior dos dois mundos: a complexidade da arquitetura distribuída com o acoplamento do monolito.
A política em uma frase Microfrontends são para times, não para código. Se o driver é "precisamos que times diferentes deployem com cadências diferentes em domínios de negócio claros" — use MFE. Se o driver é "nossa aplicação ficou grande" — use módulos lazy do Angular com Nx incremental builds. A segunda opção resolve 80% dos problemas com 20% da complexidade.
A independência que os microfrontends prometem custa mais do que o tutorial mostra. Só vale pagar quando você sabe exatamente o que está comprando.
Fontes
nx.dev/docs/technologies/module-federation — Arquitetura oficial de MFE com Nx: quando usar, version mismatch, deploys afetados, estratégia de shared libs.
blog.angular.dev — Manfred Steyer (fev/2025) — "Micro Frontends with Angular and Native Federation" — Module Federation vs Native Federation, singleton sharing, strictVersion, ESM + import maps.
DEV Community — Vitalii Petrenko (mai/2025) — "Microfrontends in 2025: A Reality Check from the Trenches" — CSS isolation, state management cross-MFE, version governance.
infinum.com (dez/2025) — "Implementing Micro Frontends — What to Look Out For" — shared packages, atualização de Angular em monorepo.
angulararchitects.io — "Multi-Framework and -Version Micro Frontends with Module Federation" — requiredVersion strategies, bootstrap assíncrono.
javascript-conference.com (jul/2024) — "Microfrontends in the Monorepo" — nx affected:apps para deploy seletivo, enforce-module-boundaries.