Voltar ao Blog
Angular22 de agosto de 202611 min de leitura

Micro-Frontends com Angular: Vale a Pena no Mundo Real?

DR

Doug Ribeiro

11 min de leitura

Micro-frontends são um daqueles padrões arquiteturais que parecem convincentes em palestras de conferência e se sentem diferentes quando você está convivendo com eles em produção. Trabalhei dentro de uma arquitetura de micro-frontends em um projeto Angular enterprise no BMO, e tenho uma opinião formada sobre eles agora. Não é uma resposta simples, mas é uma resposta honesta.

A versão curta: micro-frontends resolvem problemas reais para times de um tamanho e estrutura organizacional específicos. Para times menores que esse limiar, eles adicionam complexidade que não produz benefício nenhum. O desafio é que o limiar é mais alto do que a maioria das discussões arquiteturais sugere, e os custos são mais concretos do que os benefícios.

Qual Problema Micro-Frontends Realmente Resolvem

O pitch honesto para micro-frontends não é sobre tecnologia. É sobre topologia de times. Quando múltiplos times precisam desenvolver, fazer deploy e ser donos de partes diferentes de uma grande aplicação frontend de forma independente, uma app Angular monolítica cria custos de coordenação. O Time A não consegue deployar suas mudanças sem coordenar com o Time B. Um bug no código do Time C pode bloquear um release para todos os outros times. A propriedade é borrada porque todo mundo é dono do mesmo codebase.

Micro-frontends abordam isso traçando limites rígidos no nível de deploy e propriedade. O Time A é dono do MFE-A. Eles podem deployá-lo independentemente. Seus bugs não afetam o deploy do Time B. Suas escolhas tecnológicas (dentro de limites) não restringem o Time B. O problema de coordenação do monolito desaparece porque não há monolito compartilhado.

Se você não tem esse problema, se tem um time, ou dois times bem coordenados, ou um codebase com deploys infrequentes, micro-frontends não resolvem nada que você realmente tem. Eles introduzem custos sem o benefício correspondente.

Setup de Module Federation em Angular

O caminho mais comum para micro-frontends no Angular é Module Federation via o pacote @angular-architects/module-federation, mantido pela comunidade; o próprio Angular não entrega nenhuma solução oficial de micro-frontends. Esse plugin webpack permite que uma aplicação host carregue módulos JavaScript remotos em runtime de URLs deployadas separadamente. Cada módulo remoto é uma aplicação Angular deployada independentemente que expõe componentes, rotas ou serviços.

O setup envolve modificar a configuração webpack tanto para o host quanto para cada remoto. Module Federation é um recurso do webpack, então ele precisa do builder do Angular baseado em webpack em vez do builder padrão baseado em esbuild que chegou no Angular 17 (Native Federation, dos mesmos mantenedores, cobre o caminho do esbuild). O plugin gera as configs webpack pra você. Na prática, as configs geradas precisam de customização para qualquer coisa não-trivial.

javascript
// webpack.config.js for a remote MFE
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { share } = require('@angular-architects/module-federation/webpack');

module.exports = {
  output: {
    uniqueName: 'workspaceApp',
    publicPath: 'auto'
  },
  optimization: { runtimeChunk: false },
  experiments: { outputModule: true },
  plugins: [
    new ModuleFederationPlugin({
      library: { type: 'module' },
      name: 'workspaceApp',
      filename: 'remoteEntry.js',
      exposes: {
        './WorkspaceRoutes': './src/app/workspace/workspace.routes.ts'
      },
      shared: share({
        '@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        '@angular/common': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        '@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
        'rxjs': { singleton: true, strictVersion: false, requiredVersion: 'auto' }
      })
    })
  ]
};
typescript
// app.routes.ts in the host
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/module-federation';
import { environment } from '../environments/environment';

export const routes: Routes = [
  {
    path: 'workspace',
    loadChildren: () =>
      loadRemoteModule({
        type: 'module',
        remoteEntry: environment.workspaceRemoteUrl + '/remoteEntry.js',
        exposedModule: './WorkspaceRoutes'
      }).then(m => m.WORKSPACE_ROUTES)
  }
];

Uma coisa para deixar clara, porque este post é de agosto de 2026: o caminho do builder webpack acima é o legado. O builder padrão do Angular é baseado em esbuild desde a v17, e a opção mantida para ele é @angular-architects/native-federation. Os conceitos continuam valendo: remotos, um remote entry, singletons compartilhados. O que muda é a ligação. Native Federation usa um arquivo de configuração de federação mais uma chamada initFederation no main.ts apontando para um manifesto JSON, em vez de uma config webpack. Comece um micro-frontend novo por ali. Mostro a config webpack porque é o que a maioria dos setups enterprise existentes ainda roda, e porque migrar é um projeto por si só.

Aplicação host (shell)Roteamento, autenticação, layout global. Carrega os MFEs remotos sob de… 1 MFEs remotos (times de feature)Apps Angular com deploy independente, expostos via Module Federation 2 Dependências compartilhadasDesign system, estado de autenticação, store compartilhada. Instâncias … 3 InfraestruturaCDN por MFE, pipelines de CI/CD independentes, feature flags por remoto 4

Comunicação Entre MFEs: A Parte Mais Difícil

Uma vez que você tem múltiplas aplicações Angular deployadas independentemente que precisam se comunicar, você descobre que o sistema de injeção de dependência do Angular não foi projetado para isso. Serviços na aplicação host não estão automaticamente disponíveis para serviços em aplicações remotas. Você não pode injetar um serviço do host em um componente remoto diretamente.

Existem três padrões para comunicação entre MFEs, cada um com tradeoffs significativos.

O primeiro padrão é serviços singleton compartilhados. Você expõe um serviço do host (ou de uma biblioteca compartilhada) e configura Module Federation para tratá-lo como dependência singleton compartilhada. MFEs remotos importam e injetam esse serviço. Funciona, mas cria acoplamento forte. O remoto agora tem uma dependência de tempo de compilação na interface do serviço do host. Mudanças no serviço compartilhado exigem coordenar todos os MFEs que o consomem.

O segundo padrão é custom events. MFEs se comunicam disparando e ouvindo CustomEvents do browser no window. Isso é totalmente desacoplado, sem imports compartilhados. Funciona com qualquer tecnologia, não apenas Angular. A desvantagem é que CustomEvents são tipados por string, não têm suporte TypeScript por padrão, e debugar requer entender uma cadeia de eventos que abrange múltiplas fronteiras de aplicação.

O terceiro padrão é um store de estado compartilhado, tipicamente um store minúsculo sem framework, TypeScript puro sem nenhum import do Angular, sem NgRx e sem signals, compartilhado por referência através do mecanismo de dependência compartilhada do Module Federation. Tanto host quanto remotos leem e escrevem nesse store. É uma solução de gerenciamento de estado global, com toda a complexidade de estado global em escala.

typescript
// shared-mfe-store.ts
// A framework-free store, shared as a singleton through Module Federation.
// No Angular imports live in this file.
import type { AppNotification, User, Workspace } from './shared-types';

export interface SharedState {
  currentUser: User | null;
  activeWorkspace: Workspace | null;
  notifications: AppNotification[];
}

export type SharedAction =
  | { type: 'SET_USER'; user: User | null }
  | { type: 'SET_WORKSPACE'; workspace: Workspace | null };

export class SharedMFEStore {
  private _state: SharedState = this.initialState();
  private _listeners = new Set<(state: SharedState) => void>();

  getState(): SharedState {
    return { ...this._state };
  }

  dispatch(action: SharedAction): void {
    this._state = this.reduce(this._state, action);
    this._listeners.forEach(listener => listener({ ...this._state }));
  }

  // Returns the unsubscribe function. Callers must keep it and call it.
  subscribe(listener: (state: SharedState) => void): () => void {
    this._listeners.add(listener);
    return () => {
      this._listeners.delete(listener);
    };
  }

  private initialState(): SharedState {
    return { currentUser: null, activeWorkspace: null, notifications: [] };
  }

  private reduce(state: SharedState, action: SharedAction): SharedState {
    switch (action.type) {
      case 'SET_USER': return { ...state, currentUser: action.user };
      case 'SET_WORKSPACE': return { ...state, activeWorkspace: action.workspace };
      default: return state;
    }
  }
}

export const sharedStore = new SharedMFEStore();

O store em si fica livre de Angular. A ponte para o Angular é um serviço separado, e é onde o teardown precisa ser tratado: subscribe devolve uma função de unsubscribe, e se ninguém chamar, o store mantém uma referência para um componente que já foi embora.

typescript
// shared-state-bridge.service.ts
import { DestroyRef, Injectable, Signal, inject, signal } from '@angular/core';
import { sharedStore, SharedState } from 'shared/shared-mfe-store';

@Injectable({ providedIn: 'root' })
export class SharedStateBridge {
  private destroyRef = inject(DestroyRef);

  select<T>(selector: (state: SharedState) => T): Signal<T> {
    const sig = signal<T>(selector(sharedStore.getState()));
    const unsubscribe = sharedStore.subscribe(state => sig.set(selector(state)));
    this.destroyRef.onDestroy(unsubscribe);
    return sig.asReadonly();
  }
}

Desafios de Estado Compartilhado

O desafio de estado compartilhado com MFEs é que o estado que você precisa compartilhar é quase sempre o estado que muda com mais frequência e tem mais consumidores. Estado de autenticação, usuário atual, notificações globais: são exatamente as coisas que todo MFE precisa e que você mais precisa acertar.

O problema se agrava com Angular Signals. Signals só cruzam uma fronteira de MFE quando @angular/core é compartilhado como singleton, como na config acima, e quando o próprio signal é entregue através de uma biblioteca ou store compartilhado. Se um remoto acabar com a própria cópia do Angular, os signals do host ficam invisíveis para ele. Você precisa fazer bridge entre o sistema reativo nativo do Angular e qualquer mecanismo de comunicação cross-boundary que escolheu. Esse código de bridge é frágil, difícil de testar e requer entendimento profundo de como o change detection do Angular interage com mudanças de estado externo.

Não é incomum um projeto gastar mais tempo de engenharia na infraestrutura de comunicação de MFE do que nas features reais que esses MFEs entregam. Quando o encanamento toma mais esforço que as features, você provavelmente super-arquitetou.

Complexidade de Deploy

Uma app Angular monolítica tem um artefato de deploy: um output de build que vai para um CDN. Uma arquitetura micro-frontend com cinco MFEs tem cinco artefatos de deploy, cinco caminhos de CDN, gerenciamento de versão entre todos os cinco, e uma aplicação host que deve referenciar URLs remotas que funcionem em cada ambiente.

O problema de configuração de URL é genuinamente difícil. Em desenvolvimento, remotos rodam em localhost:4201, 4202, etc. Em staging, vivem em subdomínios diferentes. Em produção, diferentes novamente. A aplicação host precisa saber a URL correta para cada remoto em cada ambiente. Você precisa de configuração sensível ao ambiente que é bundled com o host mas referencia remotos deployados externamente: um problema de configuração que não existe em um monolito.

Compatibilidade de versão entre host e remotos é outro problema real. Se você atualiza uma biblioteca compartilhada no host, todos os remotos que declaram essa biblioteca como dependência compartilhada receberão a versão do host em runtime. Se a biblioteca compartilhada tem uma breaking change e um remoto foi buildado contra a versão antiga, você tem um conflito de versão em runtime. A versão do Angular, versão do RxJS, e qualquer biblioteca de design system compartilhada se tornam pontos potenciais de conflito em runtime.

Tradeoffs de Autonomia de Time: A Avaliação Honesta

O benefício prometido de MFEs é autonomia de time. Cada time deploya independentemente, é dono completo do seu MFE e não é bloqueado por outros times. Na prática, a autonomia é mais limitada do que anunciada, por duas razões.

Primeiro, dependências compartilhadas restringem escolhas tecnológicas. Se o host usa Angular 21, todos os remotos devem usar uma versão compatível de Angular. Você não pode ter um time no Angular 19 e outro no Angular 21 no mesmo ecossistema MFE: a restrição de biblioteca singleton compartilhada faz divergência de versão produzir erros em runtime. Os times são autônomos de certas formas, mas estão acoplados no nível de dependências.

Segundo, preocupações transversais exigem coordenação. Mudanças de autenticação, mudanças de navegação global, atualizações de design system compartilhado: exigem coordenação entre times independente da arquitetura MFE. A fronteira arquitetural não faz a comunicação organizacional desaparecer. Ela a move de code review para contratos de interface e testes de integração.

O benefício de autonomia é real, mas é mais estreito do que o pitch sugere. Você ganha deploy autônomo de features que vivem inteiramente dentro da fronteira do MFE de um time. Para features que cruzam fronteiras, MFEs não ajudam e podem tornar as coisas mais difíceis.

Onde Faz Sentido

Quero ser justo com o padrão. Imagine uma organização grande onde múltiplos times de desenvolvimento trabalham em produtos diferentes que compartilham uma experiência de portal unificada. O Time A é dono do MFE de onboarding. O Time B é dono do MFE de gestão de contas. O Time C é dono do MFE de relatórios. Esses times têm cadências de release diferentes, product managers diferentes e rotações de plantão diferentes. Não deveriam ser bloqueados nos deploys uns dos outros. É aí que MFEs fazem sentido genuíno.

A superfície compartilhada entre esses MFEs é intencionalmente estreita: um design system, estado de autenticação e um frame de navegação global. Tudo dentro de cada área de produto é auto-contido. A fronteira de deploy corresponde à fronteira organizacional. Esse alinhamento é a chave: quando a arquitetura corresponde à estrutura org, MFEs reduzem fricção real.

Onde É Over-Engineering

Um erro comum é aplicar arquitetura MFE a uma aplicação onde um único time pequeno é dono de todos os micro-frontends e toma todas as decisões de deploy. Não há problema de independência de deploy a resolver quando um time toma todas as decisões de deploy. Não há problema de autonomia de time quando todo mundo está no mesmo stand-up. Nesse cenário, a arquitetura MFE adiciona meses de complexidade de setup, overhead de manutenção contínua e situações de debug difíceis, tudo por tentar resolver um problema organizacional que não existe.

  • Time único com menos de 8 desenvolvedores: um monorepo com fronteiras internas fortes é quase sempre melhor
  • Aplicações com menos de 5 áreas de produto distintas: o overhead de unidade de deploy não vale a pena
  • Times que entregam menos de dois releases por semana: o benefício de independência de deploy é mínimo
  • Projetos greenfield sem product-market fit comprovado: você vai pivotar a arquitetura antes de estabilizar o produto
  • Projetos onde a superfície compartilhada entre MFEs é grande: acoplamento forte via estado compartilhado nega a independência

Meu Veredito

Micro-frontends com Angular Module Federation são uma solução legítima para um problema real. Esse problema é: múltiplos times que precisam de verdadeira independência de deploy para features que têm uma superfície compartilhada pequena. Se essa descrição se encaixa na sua situação precisamente, MFEs valem o investimento.

Para meu projeto SaaS pessoal, eu jamais usaria MFEs. Desenvolvedor solo, um deploy, nenhum problema de coordenação de times. Para uma organização grande com times de produto genuinamente independentes que compartilham um portal, é a decisão certa.

O erro que vejo mais frequentemente é aplicar MFEs porque parecem maduros e escaláveis, não porque um problema concreto que MFEs resolvem está realmente presente. Arquitetura deveria ser uma resposta a problemas reais, não uma otimização preventiva para problemas que você pode ter um dia. Se você não tem múltiplos times bloqueados nos deploys uns dos outros agora, invista esse tempo de engenharia entregando features.

A arquitetura certa não é a mais sofisticada. É a mais simples que resolve os problemas que você realmente tem, com estrutura suficiente para crescer nos problemas que provavelmente vai encontrar.

Referências

AngularMicro-FrontendsArchitectureEnterprise