This content is only available in Portuguese.
Not translated yet for this language.
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.
Os três pilares de uma arquitetura micro-frontend
Shell + Remotos + Shared = micro-frontends que funcionam como um app só pro usuário final.
🏠
Shell
A aplicação host que orquestra tudo. Cuida do roteamento global e carrega os micro-frontends dinamicamente.
📦
Remotos
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.
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 Shell
const 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 produtos
const 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 singleton: true nas dependências compartilhadas é crítico. Sem isso, cada micro-frontend carregaria sua própria cópia do Angular — duplicando código, quebrando injeção de dependências e inflando o bundle. Com singleton: true, todos compartilham a mesma instância.
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 — Shell
import { 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-frontends
interface 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])
);
}
}O EventPayloadMap é o segredo. Ele funciona como um contrato: todo micro-frontend sabe quais eventos existem e qual é o formato de cada um. Se alguém muda o payload de CART_UPDATED, o TypeScript avisa em todos os lugares que consomem esse evento. Sem surpresas em runtime.
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());
}
}Regra prática: se seu app fica mais lento quanto mais o usuário navega entre seções, provavelmente existe um memory leak no carregamento dinâmico. Use o Chrome DevTools → Memory → Heap Snapshot antes e depois de navegar pra identificar o vazamento.
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 inteiro
import { MaterialModule } from './material.module'; // tudo junto
// ✅ Faça assim — só o que o micro-frontend precisa
import { 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.
Use micro-frontends quando: múltiplas equipes trabalham em domínios distintos da mesma aplicação e os conflitos de merge, build time e deploy estão atrasando entregas. Não use quando: o time é pequeno, o app é gerenciável, e o problema real é organização de código — que se resolve com lazy loading e boa modularização, sem precisar de Module Federation.
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.
O que levar deste artigo
- Micro-frontends dividem um app monolítico em apps independentes com build, deploy e repositório próprios.
- Module Federation (Webpack 5) permite que builds separados compartilhem módulos em runtime — o Shell carrega os Remotos sob demanda.
- Comunicação via event bus tipado — cada micro-frontend conhece os eventos, nunca o outro micro-frontend diretamente.
- Sempre implemente fallbacks — se um micro-frontend cair, o resto do app deve continuar funcionando.
- Não compartilhe estado global — cada micro-frontend mantém seu estado e comunica via eventos.
- Limpe referências e subscriptions ao descarregar micro-frontends — memory leaks são o bug silencioso mais comum.
- Micro-frontends adicionam complexidade real. Só use quando múltiplas equipes em domínios distintos justificam essa complexidade.
- Migre incrementalmente — rotas legadas e federadas coexistem até o novo micro-frontend estar estável.



