← Back to list

Micro Frontends divida para conquistar

_Como transformar seu frontend gigante em aplicações independentes, escaláveis e mantidas por times autônomos_

Davy de Souza Assunção · 2026-01-02 20:45 · 73 claps · 14.7 min read
#react-microfrontend #implement-microfrontend #frontend #architecture #micro-frontends
Open on Medium ↗
Wiki topics: GRW · Growth & Analytics 🌐 · Web Development 🏛️ · Architecture

Micro Frontends divida para conquistar

Como transformar seu frontend gigante em aplicações independentes, escaláveis e mantidas por times autônomos

A dor que ninguém conta nos tutoriais

Imagine a seguinte cena: segunda-feira, 9h da manhã. Daily do time de frontend.

Dev 1: “Não vou conseguir subir meu PR hoje porque o João ainda não mergeou o dele e vai dar conflito gigante no App.tsx.”

Dev 2: “Eu tô travado porque preciso de uma feature do módulo de pagamento, mas o time responsável tá em outra sprint.”

Dev 3: “Fiz uma refatoração no sistema de rotas e quebrei três features de outros times. Vou ter que reverter tudo.”

Tech Lead: “Pessoal, vamos parar de deployar por hoje. O build tá demorando 23 minutos e não sabemos qual commit quebrou a aplicação.”

Essa não é ficção. É o dia a dia de milhares de times que construíram um monolito frontend.

Enquanto o backend há anos se dividiu em microsserviços, o frontend continuou sendo tratado como um bloco único, indivisível e cada vez maior. É como tentar fazer uma festa para 500 pessoas num apartamento de 50m² — tecnicamente possível, mas absolutamente insustentável.

E aí surge a pergunta que muda tudo: e se cada time pudesse ter seu próprio frontend?

A origem: quando o Spotify disse “chega de dependência”

Em 2016, os engenheiros do Spotify enfrentavam um problema clássico de escala: centenas de desenvolvedores trabalhando no mesmo repositório frontend. Cada deploy era um evento traumático. Cada refatoração, uma negociação política entre times.

A solução que eles encontraram foi revolucionária na sua simplicidade: dividir o frontend em pedaços independentes, cada um pertencendo a um time específico.

Não foi o Spotify que inventou o conceito — empresas como Ikea, Zalando e ThoughtWorks já experimentavam ideias similares — mas foram eles que popularizaram o termo Micro Frontend e mostraram que era possível aplicar os mesmos princípios dos microsserviços no frontend.

A promessa era simples e poderosa:

“Cada time desenvolve, testa e deploya seu pedaço do frontend de forma completamente independente.”

O que é Micro Frontend na prática?

Micro Frontend é uma arquitetura onde você divide sua aplicação frontend em pequenos pedaços independentes, cada um:

  • Desenvolvido e mantido por um time específico
  • Com seu próprio repositório e pipeline de deploy
  • Usando potencialmente tecnologias diferentes
  • Integrado em runtime numa aplicação maior

É como transformar aquele apartamento de 50m² em um prédio com apartamentos independentes. Cada morador (time) tem sua porta, suas chaves, sua autonomia. Mas todos fazem parte do mesmo condomínio (aplicação).

A diferença entre modularização e Micro Frontend

Isso NÃO é Micro Frontend:

meu-app/
├── src/
│   ├── modules/
│   │   ├── products/     # Pasta de produtos
│   │   ├── checkout/     # Pasta de checkout
│   │   └── profile/      # Pasta de perfil
│   └── App.tsx           # Tudo compilado junto

Isso é apenas organização de pastas. Tudo ainda compila junto, deploya junto, quebra junto.

Isso SIM é Micro Frontend:

# Repositórios separados
products-mfe/        # Time A - Deploy independente
checkout-mfe/        # Time B - Deploy independente
profile-mfe/         # Time C - Deploy independente
shell-app/           # Container que orquestra tudo

Cada micro frontend é uma aplicação completa que vive sua própria vida.

Os tipos de Micro Frontend

Existem três abordagens principais para implementar micro frontends, cada uma com seus trade-offs:

  1. Build-Time Integration (Compile Time)

Micro frontends são integrados durante o build através de dependências npm.

// package.json do shell
{
  "dependencies": {
    "@mycompany/mfe-products": "^2.1.0",
    "@mycompany/mfe-checkout": "^1.5.3",
    "@mycompany/mfe-profile": "^3.0.1"
  }
}
// App.tsx
import { ProductsApp } from "@mycompany/mfe-products";
import { CheckoutApp } from "@mycompany/mfe-checkout";
import { ProfileApp } from "@mycompany/mfe-profile";

export function App() {
  return (
    <Router>
      <Route path="/products/*" element={<ProductsApp />} />
      <Route path="/checkout/*" element={<CheckoutApp />} />
      <Route path="/profile/*" element={<ProfileApp />} />
    </Router>
  );
}

Vantagens:

  • Simples de implementar
  • Não precisa infra complexa
  • Type safety completo (TypeScript)
  • Bundling otimizado

Desvantagens:

  • Deploy acoplado (precisa rebuildar tudo)
  • Não é independente de verdade
  • Versões fixas no build

Quando usar: Times pequenos que querem modularização sem complexidade de runtime.

  1. Server-Side Integration (SSR/SSI)

Micro frontends são compostos no servidor antes de chegar no browser.

# nginx.conf
location /products {
  proxy_pass http://products-mfe:3001;
}

location /checkout {
  proxy_pass http://checkout-mfe:3002;
}

location /profile {
  proxy_pass http://profile-mfe:3003;
}

Cada rota aponta para um servidor diferente servindo HTML completo.

Vantagens:

  • SEO excelente
  • First paint rápido
  • Separação física real

Desvantagens:

  • Difícil compartilhar estado
  • Navegação entre MFEs recarrega a página
  • Complexidade de infra

Quando usar: E-commerce, blogs, sites onde SEO é crítico.

  1. Runtime Integration (Client-Side)

Micro frontends são carregados dinamicamente no browser em runtime. Essa é a abordagem mais poderosa e complexa.

// shell/src/App.tsx
import { lazy, Suspense } from "react";

const ProductsApp = lazy(() => import("products_mfe/App"));
const CheckoutApp = lazy(() => import("checkout_mfe/App"));
const ProfileApp = lazy(() => import("profile_mfe/App"));

export function App() {
  return (
    <Router>
      <Suspense fallback={<Loading />}>
        <Route path="/products/*" element={<ProductsApp />} />
        <Route path="/checkout/*" element={<CheckoutApp />} />
        <Route path="/profile/*" element={<ProfileApp />} />
      </Suspense>
    </Router>
  );
}

Esse import(‘products_mfe/App’) não vem do npm — vem de um servidor remoto em runtime.

Vantagens:

  • Deploy 100% independente
  • Pode usar versões diferentes de libs
  • Atualização instantânea sem rebuild
  • Verdadeira autonomia

Desvantagens:

  • Complexidade técnica alta
  • Possível duplicação de código
  • Type safety mais difícil

Quando usar: Aplicações grandes com múltiplos times independentes.

Implementação prática: Module Federation

A forma mais moderna e poderosa de implementar runtime integration é usando Module Federation (Webpack 5 / Rspack / Vite).

Setup do Micro Frontend (Products)

// products-mfe/webpack.config.js
const ModuleFederationPlugin =
  require("webpack").container.ModuleFederationPlugin;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "products_mfe",
      filename: "remoteEntry.js",
      exposes: {
        "./App": "./src/App",
        "./ProductList": "./src/components/ProductList",
        "./ProductDetail": "./src/components/ProductDetail",
      },
      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
        "react-router-dom": { singleton: true },
      },
    }),
  ],
};

O que está acontecendo aqui:

  • exposes: componentes que esse MFE disponibiliza para outros
  • shared: dependências compartilhadas entre MFEs (evita duplicação)
  • singleton: garante que só existe UMA versão do React na página

Setup do Shell (Orquestrador)

// shell/webpack.config.js
const ModuleFederationPlugin =
  require("webpack").container.ModuleFederationPlugin;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "shell",
      remotes: {
        products_mfe: "products_mfe@http://localhost:3001/remoteEntry.js",
        checkout_mfe: "checkout_mfe@http://localhost:3002/remoteEntry.js",
        profile_mfe: "profile_mfe@http://localhost:3003/remoteEntry.js",
      },
      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
        "react-router-dom": { singleton: true },
      },
    }),
  ],
};

Consumindo o Micro Frontend

// shell/src/App.tsx
import React, { Suspense, lazy } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";

// Importação dinâmica de MFEs remotos
const ProductsApp = lazy(() => import("products_mfe/App"));
const CheckoutApp = lazy(() => import("checkout_mfe/App"));
const ProfileApp = lazy(() => import("profile_mfe/App"));

export function App() {
  return (
    <BrowserRouter>
      <header>
        <nav>
          <a href="/products">Produtos</a>
          <a href="/checkout">Carrinho</a>
          <a href="/profile">Perfil</a>
        </nav>
      </header>

      <main>
        <Suspense fallback={<Loading />}>
          <Routes>
            <Route path="/products/*" element={<ProductsApp />} />
            <Route path="/checkout/*" element={<CheckoutApp />} />
            <Route path="/profile/*" element={<ProfileApp />} />
          </Routes>
        </Suspense>
      </main>
    </BrowserRouter>
  );
}

Magia acontecendo:

  1. Shell não tem o código de ProductsApp em build time
  2. Quando usuário acessa /products, o browser baixa o código dinamicamente de http://localhost:3001
  3. React renderiza o componente como se fosse local
  4. Se o time de produtos fizer deploy, a próxima request já pega a versão nova — sem rebuild do shell

Design Patterns essenciais

  1. Shell Application Pattern

O shell é a aplicação container responsável por:

// shell/src/App.tsx
import { ErrorBoundary } from "./components/ErrorBoundary";
import { AuthProvider } from "./contexts/AuthContext";
import { ThemeProvider } from "./contexts/ThemeContext";

export function App() {
  return (
    <ErrorBoundary>
      <AuthProvider>
        <ThemeProvider>
          <Layout>
            <Navigation />
            <Suspense fallback={<Loading />}>
              <MicroFrontendRoutes />
            </Suspense>
          </Layout>
        </ThemeProvider>
      </AuthProvider>
    </ErrorBoundary>
  );
}

Responsabilidades do shell:

  • Roteamento principal
  • Autenticação e autorização
  • Temas e estilos globais
  • Navigation/Header/Footer compartilhados
  • Error boundaries
  • Analytics e monitoramento

O que o shell NÃO deve fazer:

  • Lógica de negócio
  • Chamadas de API específicas
  • Componentes complexos de domínio
  1. Shared State Pattern

Como compartilhar estado entre micro frontends independentes?

Opção 1: Context Provider no Shell

// shell/src/contexts/CartContext.tsx
export const CartContext = createContext<CartContextType | null>(null);

export function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);

  const addItem = (item: CartItem) => {
    setItems((prev) => [...prev, item]);
  };

  const removeItem = (id: string) => {
    setItems((prev) => prev.filter((item) => item.id !== id));
  };

  return (
    <CartContext.Provider value={{ items, addItem, removeItem }}>
      {children}
    </CartContext.Provider>
  );
}
// products-mfe/src/components/ProductCard.tsx
import { useContext } from "react";
import { CartContext } from "shell/contexts/CartContext";

export function ProductCard({ product }: Props) {
  // MFE consome contexto do shell
  const cart = useContext(CartContext);

  const handleAddToCart = () => {
    cart?.addItem({
      id: product.id,
      name: product.name,
      price: product.price,
    });
  };

  return (
    <div>
      <h3>{product.name}</h3>
      <button onClick={handleAddToCart}>Adicionar ao Carrinho</button>
    </div>
  );
}

Opção 2: Event Bus (Pub/Sub) — A que eu mais gosto, porem a mais perigosa

Quando você quer desacoplamento total entre MFEs:

// shell/src/utils/eventBus.ts
type EventCallback = (payload: any) => void;

class EventBus {
  private events: Map<string, EventCallback[]> = new Map();

  subscribe(event: string, callback: EventCallback) {
    const handlers = this.events.get(event) || [];
    this.events.set(event, [...handlers, callback]);

    // Retorna função de cleanup
    return () => {
      const handlers = this.events.get(event) || [];
      this.events.set(
        event,
        handlers.filter((h) => h !== callback)
      );
    };
  }

  publish(event: string, payload?: any) {
    const handlers = this.events.get(event) || [];
    handlers.forEach((handler) => handler(payload));
  }
}

export const eventBus = new EventBus();

// Expõe globalmente para todos os MFEs
(window as any).__EVENT_BUS__ = eventBus;
// products-mfe: publica evento
import { eventBus } from "shell/utils/eventBus";

function ProductCard({ product }: Props) {
  const handleAddToCart = () => {
    eventBus.publish("cart:item-added", {
      id: product.id,
      name: product.name,
      price: product.price,
    });
  };

  return <button onClick={handleAddToCart}>Adicionar</button>;
}
// checkout-mfe: escuta evento
import { useEffect, useState } from "react";
import { eventBus } from "shell/utils/eventBus";

export function CartCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const unsubscribe = eventBus.subscribe("cart:item-added", () => {
      setCount((prev) => prev + 1);
    });

    return unsubscribe;
  }, []);

  return <span>Carrinho ({count})</span>;
}

Vantagem: MFEs não conhecem uns aos outros. Totalmente desacoplados.

Desvantagem: Mais difícil debugar. Sem type safety. Se não tiver uma boa gestão dos eventos, quando começa a escalar pode virar uma bagunça.

Opção 3: State Management Global (Zustand/Redux)

// shell/src/store/cartStore.ts
import create from "zustand";

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
}

export const useCartStore = create<CartStore>((set) => ({
  items: [],
  addItem: (item) =>
    set((state) => ({
      items: [...state.items, item],
    })),
  removeItem: (id) =>
    set((state) => ({
      items: state.items.filter((i) => i.id !== id),
    })),
}));

// Expõe para todos os MFEs
(window as any).__CART_STORE__ = useCartStore;
// Qualquer MFE pode usar
const { items, addItem } = (window as any).__CART_STORE__();
  1. Styling Isolation Pattern

Como evitar conflito de estilos entre MFEs?

Não faça isso:

/* products-mfe */
.button {
  background: blue;
}

/* checkout-mfe */
.button {
  background: red; /* ❌ Conflito! */
}

Opção 1: CSS Modules

// products-mfe/ProductCard.module.css
.button {
  background: blue;
}
// ProductCard.tsx
import styles from "./ProductCard.module.css";

export function ProductCard() {
  return <button className={styles.button}>Comprar</button>;
  // Gera: <button class="ProductCard_button__a1b2c">
}

Opção 2: CSS-in-JS com prefixo

// products-mfe
import styled from "@emotion/styled";

const Button = styled.button`
  background: blue;
  // CSS escopado automaticamente
`;

Opção 3: Shadow DOM

// Isola completamente o CSS
class ProductWidget extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: "open" });
    shadow.innerHTML = `
      <style>
        .button { background: blue; }
      </style>
      <button class="button">Comprar</button>
    `;
  }
}

customElements.define("product-widget", ProductWidget);

4. Versioning Strategy Pattern

Como gerenciar versões de MFEs?

// shell/webpack.config.js - Ambiente de dev
remotes: {
  products_mfe: 'products_mfe@http://localhost:3001/remoteEntry.js',
}

// shell/webpack.config.js - Produção
remotes: {
  products_mfe: 'products_mfe@https://cdn.myapp.com/products-mfe/v2.3.1/remoteEntry.js',
}

Estratégia de versionamento:

// Configuração dinâmica em runtime
const MFE_REGISTRY = {
  products: {
    url: process.env.REACT_APP_PRODUCTS_MFE_URL,
    version: "2.3.1",
    fallback: "/fallback-products",
  },
  checkout: {
    url: process.env.REACT_APP_CHECKOUT_MFE_URL,
    version: "1.5.0",
    fallback: "/fallback-checkout",
  },
};

function loadMicroFrontend(name: string) {
  const config = MFE_REGISTRY[name];

  return import(/* webpackIgnore: true */ config.url).catch(() => {
    console.error(`Failed to load ${name}, using fallback`);
    return import(config.fallback);
  });
}

Quando usar Micro Frontends

Sinais de que você PRECISA de micro frontends:

  1. Múltiplos times trabalhando no mesmo frontend

Se você tem mais de 2–3 times mexendo no mesmo repositório, os conflitos de merge e dependências começam a atrapalhar mais do que ajudar.

  1. Aplicação com domínios bem separados
E-commerce:
├── Catálogo de produtos (Time A)
├── Carrinho e Checkout (Time B)
├── Perfil e pedidos (Time C)
└── Admin/Backoffice (Time D)

Cada domínio é quase uma aplicação separada. Faz sentido separá-los.

  1. Deploy frequente e independente

Se o time de produtos precisa deployar 5x por dia, mas o time de checkout só deploya 1x por semana, eles não deveriam estar no mesmo build.

  1. Tecnologia heterogênea

Legado em AngularJS + novo em React? Micro frontends permitem migração gradual:

// Shell pode carregar ambos
<Route path="/legacy/*" element={<AngularMFE />} />
<Route path="/new/*" element={<ReactMFE />} />
  1. Escala de organização

Empresas com 50+ desenvolvedores frontend geralmente se beneficiam da autonomia que micro frontends trazem.

Checklist: você está pronto?

  • [ ] Tem pelo menos 3 times independentes trabalhando no frontend
  • [ ] Domínios de negócio bem definidos e separados
  • [ ] Necessidade de deploy independente
  • [ ] Infraestrutura para servir múltiplas aplicações
  • [ ] Time com conhecimento técnico em arquitetura distribuída

Quando NÃO usar Micro Frontends

Sinais de que você vai se arrepender:

  1. Aplicação pequena ou time pequeno

Se você tem 2–5 desenvolvedores, a complexidade de micro frontends vai atrapalhar mais do que ajudar.

Não faça isso:
startup-app/
├── products-mfe/      # 1 dev
├── checkout-mfe/      # 1 dev
├── profile-mfe/       # 1 dev
└── shell/             # Overhead absurdo
  1. Domínios muito acoplados

Se suas “features” conversam o tempo todo e compartilham muita lógica, separar vai criar mais problemas:

// Má separação
user-profile-mfe   → precisa de dados de orders-mfe
orders-mfe         → precisa de dados de products-mfe
products-mfe       → precisa de dados de user-profile-mfe
// Isso não é separação, é bagunça distribuída
  1. Performance é crítica

Micro frontends adicionam overhead:

  • Múltiplos bundles sendo baixados
  • Possível duplicação de dependências
  • Runtime de orquestração

Se você está construindo um dashboard de trading em tempo real onde cada milissegundo importa, talvez um monolito bem otimizado seja melhor.

  1. Falta de maturidade DevOps

Micro frontends exigem:

  • CI/CD separado para cada MFE
  • CDN ou servidores para hospedar cada bundle
  • Monitoramento distribuído
  • Estratégia de versionamento

Se sua infra ainda é “FTP para produção”, não está pronto.

  1. Aplicação com UX muito integrada

Se sua aplicação é tipo Figma, Notion ou VSCode — onde tudo é uma experiência fluida e integrada — micro frontends podem quebrar essa coesão.

Red flags de má implementação:

// MFE comunicando diretamente com outro MFE
import { getUserData } from 'profile-mfe/utils';

// Lógica de negócio no shell
function Shell() {
  const products = await fetchProducts(); // ❌ não!
  return <ProductsMFE products={products} />;
}

// Dependências desalinhadas
products-mfe: React 17
checkout-mfe: React 18
profile-mfe: React 16
// = caos de singleton

// Cada MFE com seu próprio design system
// Parece Frankenstein na UI

A combinação perfeita: Micro Frontends + BFF

Lembra do artigo sobre BFF? Micro Frontends e BFF são feitos um para o outro.

Arquitetura completa

┌─────────────────────────────────────────────┐
│              SHELL APP                      │
│  (routing, auth, layout compartilhado)      │
└─────────────────────────────────────────────┘
         │              │              │
    ┌────▼───┐     ┌────▼───┐     ┌────▼───┐
    │Products│     │Checkout│     │Profile │
    │  MFE   │     │  MFE   │     │  MFE   │
    └────┬───┘     └────┬───┘     └────┬───┘
         │              │              │
    ┌────▼────┐    ┌────▼────┐    ┌────▼────┐
    │Products │    │Checkout │    │Profile  │
    │  BFF    │    │  BFF    │    │  BFF    │
    └────┬────┘    └────┬────┘    └────┬────┘
         │              │              │
    ┌────▼─────────────▼──────────────▼─────┐
    │         MICROSSERVIÇOS                │
    │   (orders, users, products, etc)      │
    └───────────────────────────────────────┘

Implementação prática

// products-mfe/src/services/api.ts
const PRODUCTS_BFF_URL = process.env.REACT_APP_PRODUCTS_BFF;

export async function getProducts(filters: ProductFilters) {
  // Chama o BFF específico deste MFE
  const response = await fetch(`${PRODUCTS_BFF_URL}/products`, {
    method: "POST",
    body: JSON.stringify(filters),
  });

  return response.json();
}
// products-bff/src/routes/products.ts
app.post("/products", async (req, res) => {
  // BFF agrega dados de múltiplos microsserviços
  const [products, inventory, reviews] = await Promise.all([
    productsService.search(req.body.filters),
    inventoryService.getStock(productIds),
    reviewsService.getRatings(productIds),
  ]);

  // Retorna exatamente o que o products-mfe precisa
  res.json({
    products: products.map((p) => ({
      id: p.id,
      name: p.name,
      price: p.price,
      stock: inventory[p.id].quantity,
      rating: reviews[p.id].average,
      // Dados otimizados para este MFE
    })),
  });
});

Benefícios da combinação

Autonomia total:

  • Time possui MFE + BFF + testes + deploy
  • Não depende de outros times para evoluir
  • Pode escolher tecnologias específicas

Performance otimizada:

  • MFE carrega apenas seu código
  • BFF retorna apenas dados necessários
  • Sem over-fetching ou under-fetching

Contratos claros:

// products-mfe/types/api.ts
export interface ProductListResponse {
  products: Array<{
    id: string;
    name: string;
    price: number;
    stock: number;
    rating: number;
  }>;
  total: number;
  page: number;
}

BFF garante esse contrato. MFE confia no contrato. Backend pode mudar internamente sem quebrar nada. Caso queria saber mais sobre BFF, recomendo fortemente ler o artigo:

BFF: Por que o Frontend deveria liderar essa revolução?

Armadilhas comuns (e como evitar)

  1. “Micro” frontends gigantes
// Isso não é micro
products-mfe/
├── src/
│   ├── catalog/
│   ├── cart/
│   ├── checkout/
│   ├── orders/
│   ├── reviews/
│   └── recommendations/

Se seu “micro” frontend faz 6 coisas diferentes, não é micro.

Corrija:

catalog-mfe/        → Só catálogo
cart-mfe/           → Só carrinho
checkout-mfe/       → Só checkout
orders-mfe/         → Só pedidos
  1. Duplicação excessiva de código
// Cada MFE reimplementa o mesmo botão
products - mfe / src / Button.tsx;
checkout - mfe / src / Button.tsx;
profile - mfe / src / Button.tsx;

Corrija: Crie uma lib compartilhada

// @mycompany/design-system (npm package)
export { Button, Input, Modal } from './components';

// products-mfe/package.json
{
  "dependencies": {
    "@mycompany/design-system": "^2.1.0"
  }
}

Configure como shared no Module Federation:

shared: {
  '@mycompany/design-system': {
    singleton: true,
    requiredVersion: '^2.0.0',
  },
}
  1. Comunicação caótica entre MFEs
// products-mfe chamando checkout-mfe diretamente
import { addToCart } from "checkout-mfe/actions";

addToCart(product); // Acoplamento!

Corrija: Use event bus ou shared context

// Desacoplado via eventos
eventBus.publish("product:add-to-cart", { productId: product.id });
  1. Falta de error handling
// Se o MFE falhar, quebra tudo
<Suspense fallback={<Loading />}>
  <ProductsMFE />
</Suspense>

Corrija: Error boundaries + fallback

// Degrada gracefully
<ErrorBoundary
  fallback={<ErrorFallback message="Produtos indisponíveis" />}
  onError={(error) => logToMonitoring(error)}
>
  <Suspense fallback={<Loading />}>
    <ProductsMFE />
  </Suspense>
</ErrorBoundary>
  1. Dependências desalinhadas
// Versões diferentes de React
products-mfe:  react@18.2.0
checkout-mfe:  react@18.0.0
profile-mfe:   react@17.0.2

Resultado: três cópias do React no bundle. Pesadelo de performance.

// Todos os MFEs devem ter:
shared: {
  react: {
    singleton: true,
    requiredVersion: '^18.2.0',
    strictVersion: true, // Falha se versão incompatível
  },
}

Melhores práticas consolidadas

  1. Defina contratos claros
// shared-types/src/contracts.ts
export interface MicroFrontendContract {
  name: string;
  version: string;
  exposes: string[];
  requires: {
    react: string;
    "react-router-dom": string;
  };
  events: {
    emits: string[];
    listens: string[];
  };
}

// products-mfe/contract.ts
export const contract: MicroFrontendContract = {
  name: "products-mfe",
  version: "2.1.0",
  exposes: ["./App", "./ProductList"],
  requires: {
    react: "^18.2.0",
    "react-router-dom": "^6.0.0",
  },
  events: {
    emits: ["product:added-to-cart", "product:viewed"],
    listens: ["cart:cleared"],
  },
};
  1. Teste contratos (Contract Testing)
// products-mfe/tests/contract.test.ts
import { Pact } from "@pact-foundation/pact";

describe("Products MFE Contract", () => {
  it("should provide ProductList component", async () => {
    const { ProductList } = await import("products-mfe/ProductList");
    expect(ProductList).toBeDefined();
    expect(typeof ProductList).toBe("function");
  });

  it("should emit correct events", () => {
    const eventSpy = jest.fn();
    eventBus.subscribe("product:added-to-cart", eventSpy);

    // Simula ação
    addProductToCart("123");

    expect(eventSpy).toHaveBeenCalledWith({
      productId: "123",
      timestamp: expect.any(Number),
    });
  });
});
  1. Monitore cada MFE separadamente
// shell/src/monitoring.ts
import * as Sentry from "@sentry/react";

export function setupMFEMonitoring(mfeName: string) {
  Sentry.init({
    dsn: process.env.REACT_APP_SENTRY_DSN,
    environment: process.env.NODE_ENV,
    tags: {
      microFrontend: mfeName,
    },
  });
}

// products-mfe/src/index.tsx
import { setupMFEMonitoring } from "shell/monitoring";

setupMFEMonitoring("products-mfe");

// Agora erros são taggeados por MFE
  1. Versionamento semântico rigoroso
MAJOR.MINOR.PATCH

MAJOR: Breaking changes (muda contrato)
MINOR: Novas features (mantém compatibilidade)
PATCH: Bug fixes
// products-mfe v2.0.0 → v3.0.0 (breaking change)
// ANTES:
export function ProductList() { ... }

// DEPOIS:
export function ProductList({ config }: { config: Config }) { ... }
// Quebra quem usa

// Shell precisa atualizar:
- <ProductList />
+ <ProductList config={defaultConfig} />
  1. Feature flags por MFE
// feature-flags.ts
export const FEATURES = {
  "products-mfe": {
    newProductCard: true,
    aiRecommendations: false,
  },
  "checkout-mfe": {
    expressCheckout: true,
    pixPayment: true,
  },
};

// products-mfe/src/ProductList.tsx
import { FEATURES } from "shell/feature-flags";

export function ProductList() {
  const showNewCard = FEATURES["products-mfe"].newProductCard;

  return (
    <>
      {products.map((p) =>
        showNewCard ? (
          <NewProductCard product={p} />
        ) : (
          <OldProductCard product={p} />
        )
      )}
    </>
  );
}

Como pensar em Micro Frontends

Micro frontends não são sobre tecnologia. São sobre organização e autonomia.

A pergunta que devemos fazer não é “como implementar Module Federation?”, mas sim:

”Como estruturamos os times para que possam evoluir independentemente?”

Quando você escreve um micro frontend pensando “isso precisa funcionar sem depender de outros times”, você está no caminho certo.

Quando você escreve pensando “vou só separar em pastas e chamar de micro frontend”, você está criando complexidade desnecessária.

Os princípios para levar

  1. Autonomia > Reuso → Prefira duplicar código a criar dependências entre MFEs (eu sei que isso pode ferir o DRY, mas as vezes é valido comprar um tradeoff se os beneficios forem maiores)
  2. Contratos > Implementação → Garanta compatibilidade através de contratos, não de código compartilhado
  3. Comunicação assíncrona → MFEs devem conversar via eventos, não via chamadas diretas
  4. Falhas isoladas → Se um MFE quebrar, os outros continuam funcionando
  5. Deploy independente → Se precisa coordenar deploy de múltiplos MFEs, algo está errado

Conclusão: divida para conquistar

A indústria de software aprendeu há anos que monólitos backend não escalam. Microsserviços revolucionaram o backend não porque são tecnicamente superiores, mas porque resolvem um problema de organização humana.

Micro frontends trazem a mesma revolução para o frontend.

O problema não é técnico. O problema é permitir que times cresçam sem se atropelarem.

A verdadeira habilidade não está em configurar webpack — está em estruturar times e código para que ambos possam evoluir.

A pergunta final

Antes de implementar micro frontends, pergunte-se:

”Meus times realmente precisam de autonomia total, ou só estou seguindo a moda?”

Se a resposta for “precisamos de autonomia”, micro frontends são a solução.

Se a resposta for “só queremos modularizar”, crie uma arquitetura de pastas melhor.

Micro frontends não são sobre separar código. São sobre separar responsabilidades. E código bem organizado não é aquele que compila — é aquele que permite times trabalharem sem se bloquearem.

Referências e aprofundamento

Este artigo foi construído com base em práticas consolidadas e implementações reais:

  • Cam JacksonMicro Frontends (martinfowler.com): artigo seminal que popularizou o conceito.
  • Luca MezzaliraBuilding Micro-Frontends: livro definitivo sobre arquitetura de micro frontends.
  • Michael GeersMicro Frontends in Action: guia prático com exemplos reais.
  • Webpack Module FederationDocumentação oficial: a tecnologia que viabilizou runtime integration moderna.
  • Single-SPAFramework para micro frontends: alternativa ao Module Federation, agnóstica de framework.
  • Zack Jackson — Criador do Module Federation: palestras e artigos sobre a arquitetura.
  • ThoughtWorks Technology Radar — Avaliações e recomendações sobre adoção de micro frontends.

Se este artigo te fez repensar como estruturar seu frontend, ele cumpriu seu papel. Compartilhe com seu time e vamos elevar o nível da arquitetura frontend brasileira.

👏 Gostou? Deixe um clap e compartilhe suas experiências com micro frontends nos comentários!


메타데이터
post_id
77d59ff2bdcb
slug
micro-frontends-divida-para-conquistar-77d59ff2bdcb
url
https://medium.com/@davysz/micro-frontends-divida-para-conquistar-77d59ff2bdcb
canonical_url
https://medium.com/@davysz/micro-frontends-divida-para-conquistar-77d59ff2bdcb
author_url
https://medium.com/@davysz
status
ok
fetched_at
2026-07-13 17:43:34