Architecture

Microservices

Microservices dividem uma aplicação em serviços implantáveis independentemente em torno de capacidades de negócio. O objetivo é a autonomia da equipe e lançamentos independentes, não o escalonamento por si só — e a rede entre eles é o preço a pagar.

advanced16 min readUpdated 16 de set. de 2026
services/orders/service.yaml
yaml
# services/orders/service.yaml
name: orders
image: ghcr.io/acme/orders:1.14.2
port: 8080
replicas: 3

env:
  DATABASE_URL: secret://orders-db
  BROKER_URL: kafka://events:9092
  OTEL_EXPORTER_OTLP_ENDPOINT: http://otel:4317

resources:
  requests: { cpu: "250m", memory: "256Mi" }
  limits: { cpu: "1", memory: "512Mi" }

probes:
  liveness: /healthz
  readiness: /readyz

dependencies:
  inventory: http://inventory:8080
  payments: http://payments:8080
Unidade de implantação
Um serviço, um pipeline
Propriedade de dados
Um banco de dados por serviço
Comunicação padrão
HTTP ou gRPC, depois eventos
Problema mais difícil
Transações distribuídas
Modo de falha
Falha parcial
Estrutura da equipe
Equipes pequenas e autônomas

Por que importa

O que a independência realmente proporciona

Implantáveis independentemente

Cada serviço é entregue em seu próprio cronograma. Uma alteração no serviço de envios não espera por um ciclo de lançamento que inclua faturamento e catálogo.

Propriedade de ponta a ponta

Uma equipe pequena é dona do código, dos dados e do on-call de um serviço. A propriedade clara é o que torna a autonomia real em vez de nominal.

Comunicação via rede

Os serviços se integram através de contratos explícitos e eventos em vez de memória compartilhada ou tabelas compartilhadas, o que é ao mesmo tempo o benefício e o custo.

O panorama completo

As três forças por trás de um limite de serviço

Um limite é uma capacidade de negócio, um contrato explícito e um fluxo de eventos que desacopla os serviços em ambos os lados.

Capacidade de negócio

Limite

Um serviço detém uma capacidade e a linguagem que a descreve. O limite segue o domínio, não uma camada técnica.

Contrato

Interface

Os chamadores dependem de um contrato versionado, nunca dos internos do serviço. Mudanças aditivas mantêm os consumidores funcionando.

Eventos

Desacoplar

Um fato publicado permite que outros serviços reajam sem que o produtor saiba quem está ouvindo, removendo o acoplamento temporal.

HTML5 de uma olhada

As peças móveis que você agora opera

Serviços

Unidades implantáveis pequenas, cada uma com seu próprio ciclo de vida e repo.

Gateway

Um único ponto de entrada para roteamento, auth e limites de taxa (rate limits).

Discovery

Nomes são resolvidos para instâncias saudáveis via DNS ou um registro.

Armazenamento de dados

Cada serviço detém seu próprio schema e ninguém mais escreve nele.

Event bus

Tópicos duráveis transportam fatos entre serviços de forma assíncrona.

Tracing

IDs de correlação acompanham uma requisição em cada salto.

Fluxo

Uma requisição entre serviços

Uma única ação do usuário torna-se várias chamadas de rede, cada uma das quais pode falhar individualmente. O gateway detém o prazo (deadline) e o fallback.

  1. 1

    O gateway recebe a requisição

    Ele autentica o chamador, aplica limites de taxa e roteia a chamada para o primeiro serviço.

  2. 2

    O Serviço A inicia o trabalho

    Ele valida a entrada e escreve em seu próprio banco de dados. Até aqui, nada é distribuído exceto o ponto de entrada.

  3. 3

    A precisa de dados de B

    Ele chama B sincronicamente com um timeout ou publica um evento e segue em frente. A escolha decide o quão acoplados os dois estão.

  4. 4

    B pode chamar C

    A cadeia cresce. Cada salto extra adiciona latência e outra chance de falha parcial que A deve tratar.

  5. 5

    Cada serviço pode falhar sozinho

    B pode estar fora do ar enquanto A está saudável. A precisa de um timeout, uma política de retry e um fallback exatamente para este caso.

  6. 6

    O gateway agrega e retorna

    Ele combina os resultados sob um único prazo, degradando graciosamente em vez de travar quando um serviço downstream está lento.

Uma breve historia

Como a indústria chegou aqui

  1. 2011

    O termo ganha força

    Um workshop de arquitetos nomeia o estilo, e a ideia de pequenos serviços em torno de capacidades de negócio se espalha através de palestras em conferências.

    11
  2. 2014

    Definição mainstream

    Martin Fowler e James Lewis publicam o artigo canônico, e o Docker torna a embalagem de um serviço trivial.

    14
  3. 2015

    Containers mudam a unidade

    Docker e os primeiros orquestradores transformam a implantação de uma operação especial em algo rotineiro, o que torna muitos serviços práticos.

    15
  4. 2017

    Orquestração se consolida

    Kubernetes torna-se o escalonador padrão, e service discovery, escalonamento e rollout deixam de ser problemas customizados.

    17
  5. 2019

    Service mesh e tracing

    Sidecars movem retries, mTLS e telemetria para fora do código da aplicação, e o OpenTelemetry padroniza o distributed tracing.

    19
  6. 2022

    Uma correção

    Equipes que dividiram cedo demais falam abertamente sobre o modular monolith, e a conversa muda de "quão pequeno" para "quão implantável independentemente".

    22

O guia completo

Microservices: Tudo que voce precisa saber

O que são microservices, e o que não são

Microservices são um estilo arquitetural no qual uma aplicação é composta por serviços implantáveis independentemente e organizados em torno de capacidades de negócio, onde cada um detém seus próprios dados e se comunica através da rede. A palavra-chave aqui é independentemente. Um serviço que não pode ser implantado por conta própria não é um microservice; é apenas um módulo com uma fronteira de rede.

Vale a pena dizer o que esse estilo não é. Não é uma regra que os serviços devam ser pequenos. Não é um requisito utilizar containers, Kubernetes ou um service mesh, embora essas ferramentas existam porque este estilo é complexo sem elas. Não é automaticamente mais escalável, mais confiável ou mais moderno que um monólito. Essas são propriedades que você constrói, e não propriedades que você obtém de uma topologia.

A única característica definidora é que uma alteração em um serviço pode chegar à produção sem a necessidade de um release coordenado dos demais. Todo o resto — as fronteiras, os contratos, os eventos, a plataforma — existe para tornar essa característica real e para sobreviver às suas consequências.

Um exemplo de decomposição

Imagine uma loja online. As capacidades são fáceis de nomear, e cada uma se torna um serviço com um dono claro.

catalog        products, prices, availability
cart           the pre-purchase basket
orders         the placed order and its lifecycle
inventory      stock levels and reservations
payments       charges, refunds, payment methods
shipping       labels, carriers, tracking
notifications  email, push, SMS
identity       accounts, sessions, API keys

Observe o que está faltando nessa lista. Não existe um “serviço de banco de dados”, nem um “serviço de envio de e-mail” — notificações são uma capacidade, não um transporte — e nenhum “serviço de usuário” que todos os outros serviços chamam apenas para renderizar um nome. Cada item detém uma parte do negócio, e uma equipe pode ser responsável por um ou dois deles de ponta a ponta.

As interações importam tanto quanto a lista. Navegar no catálogo é uma operação com muita leitura e pode ser cacheada agressivamente. Fazer um pedido é uma escrita que coordena inventário, pagamentos e envio. Notificações são um consumidor puro: reage a eventos e nunca é chamado. Essas formas diferentes justificam tratamentos diferentes — caching, sagas, filas — embora todos sejam “serviços”.

O verdadeiro motor é a capacidade de deploy independente

As equipes frequentemente adotam microservices “para escalar”, e então descobrem que nunca precisaram desse escalonamento. O motivo honesto para a divisão é organizacional. Quando dez engenheiros trabalham em um único deployable, cada release espera pela alteração mais lenta, e um teste quebrado em uma área bloqueia todos. A divisão por capacidade dá a cada equipe seu próprio pipeline, seu próprio on-call e seu próprio ritmo.

Isso é a Lei de Conway ao contrário: você molda o sistema para que suas divisões correspondam à maneira como suas equipes se comunicam. Se uma única equipe é dona de todo o produto, microservices trazem falhas de rede e uma plataforma para manter em troca de nada. Se seis equipes precisam fazer ship de forma independente, as divisões começam a se pagar.

Portanto, a primeira pergunta não é “como decompomos este domínio?”, mas “quais partes deste sistema genuinamente precisam mudar em ritmos diferentes, sendo mantidas por pessoas diferentes?”. Apenas as respostas a isso se tornam serviços.

Fronteiras seguem capacidades de negócio

A costura correta é uma capacidade de negócio, não uma camada técnica. Faturamento, envio, catálogo, identidade e notificações são capacidades. Um “serviço de banco de dados”, um “serviço de e-mail” ou um “serviço de validação” é uma camada técnica fantasiada de serviço, e cada nova funcionalidade exigirá a alteração de três deles simultaneamente.

O Domain-driven design fornece o vocabulário. Um bounded context é uma fronteira dentro da qual um modelo específico e sua linguagem são consistentes. “Pedido” no contexto de vendas significa um carrinho prestes a ser pago; no contexto de logística, significa um pacote para ser coletado e enviado. Ambos estão corretos dentro de seus próprios contextos, e forçar uma única classe Order compartilhada entre ambos é como um monólito se torna uma bagunça.

Uma boa fronteira possui três propriedades:

  • Ela detém um conjunto coerente de regras que mudam juntas.
  • Ela possui uma interface pequena e estável em relação à quantidade de lógica por trás dela.
  • Ela pode ser compreendida por uma equipe sem a necessidade de ler o restante do sistema.

Fronteiras são caras de mover depois que outros serviços dependem delas, portanto, tenda a criar menos serviços e maiores no início. Dividir um serviço é fácil; fundir dois que divergiram é doloroso.

Comunicação síncrona: REST e gRPC

A interação mais simples é uma requisição e uma resposta. Use REST com JSON para qualquer coisa que um navegador ou cliente externo acesse, e gRPC quando ambos os lados forem internos e você desejar um contrato tipado, compacto e de baixa latência. Os clientes gerados pelo gRPC tornam difícil divergir do schema, e é por isso que muitas service meshes internas o padronizam.

Chamadas síncronas são fáceis de compreender, mas impossíveis de tornar totalmente seguras. Dois serviços agora estão temporalmente acoplados: o chamador não pode prosseguir a menos que o chamado esteja online e respondendo. Três regras evitam que isso se torne uma interrupção total do serviço.

  • Sempre defina um timeout. Uma chamada sem prazo pode travar até que seus próprios recursos se esgotem. Escolha um orçamento que se ajuste ao prazo do próprio chamador e subtraia o restante do trabalho dele.
  • Tente novamente apenas operações idempotentes. Um GET repetido é inofensivo; um POST /charge repetido é uma segunda cobrança, a menos que o endpoint aceite e respeite uma chave de idempotência.
  • Interrompa o circuito. Quando uma dependência estiver falhando, pare de chamá-la por um tempo em vez de enfileirar requisições que também irão falhar. Este é o circuit breaker, e é ele que transforma uma dependência lenta em um erro rápido e honesto.

Retentativas (retries) amplificam a carga. Se três camadas repetirem a tentativa três vezes cada, um serviço sobrecarregado receberá vinte e sete requisições para cada original. Limite as retentativas na borda (edge), onde você consegue ter a visão geral, e adicione jitter para que as retentativas não cheguem em uma onda sincronizada.

Comunicação assíncrona: eventos

A alternativa a perguntar é anunciar. Um serviço publica um fato — orders.created, payment.captured — e qualquer número de consumidores reage sem que o produtor saiba que eles existem. Isso remove o acoplamento temporal: o serviço de pagamento pode estar fora do ar e o pedido ainda assim é criado, pois o evento aguarda no broker.

A comunicação assíncrona proporciona resiliência e flexibilidade ao custo da imediatez e da clareza. A resposta do produtor não inclui mais o efeito a jusante, portanto, o sistema é eventualmente consistente. Rastrear uma requisição significa seguir uma trilha de eventos em vez de uma única call stack. E o broker torna-se uma infraestrutura crítica que deve ser durável e monitorada.

Existem duas maneiras de coordenar um processo de múltiplas etapas. Na coreografia, cada serviço escuta eventos e decide o que fazer a seguir; não há um cérebro central, o que é elegante até que ninguém consiga dizer qual é o fluxo geral. Na orquestração, um gerenciador de processos ou coordenador de saga conduz explicitamente as etapas. A coreografia é melhor para reações simples, e a orquestração para fluxos com compensações e timeouts. Os guias de Event-Driven Architecture e Kafka aprofundam-se mais na mecânica.

O imposto dos sistemas distribuídos

No momento em que uma chamada atravessa a rede, um conjunto de modos de falha que não existem em-process tornam-se sua responsabilidade. Isso não é motivo para evitar serviços; é a conta que vem com eles.

  • A rede não é confiável. Pacotes são perdidos, o DNS retorna endereços obsoletos, conexões são resetadas no meio de uma requisição.
  • A falha é parcial. O Serviço A está saudável enquanto o B está fora do ar. Não existe uma flag única de “o sistema está online”.
  • A latência não é gratuita. Uma chamada que levava microssegundos em-process agora leva milissegundos, e cadeias de chamadas multiplicam esse valor.
  • A resposta pode ser perdida. Uma requisição pode ter sucesso, mas a confirmação nunca chega. Do ponto de vista de quem chamou, ela falhou; porém, o trabalho foi realizado.
  • O tempo não é sincronizado. Os relógios entre máquinas divergem, portanto, ordenar eventos apenas por timestamp é inseguro.

A resposta de design é um conjunto de ferramentas pequeno e bem conhecido: timeouts em cada chamada, retentativas limitadas com backoff e jitter, circuit breakers, bulkheads que isolam a falha de uma dependência e chaves de idempotência para que requisições repetidas sejam seguras. Nada disso é opcional. Um sistema de microservices sem esses itens é um sistema que funciona até o primeiro dia ruim.

Propriedade de dados e a armadilha do banco de dados compartilhado

Cada serviço é dono de seus próprios dados. Isso significa que um único serviço é o único escritor de seu schema, e nenhum outro serviço se conecta a esse banco de dados. A propriedade é o que torna o deploy independente possível, pois uma alteração de schema afeta apenas o serviço proprietário e seu contrato.

Um banco de dados compartilhado parece conveniente, mas destrói silenciosamente a arquitetura. Dois serviços lendo as mesmas tabelas ficam acoplados através do schema: renomeie uma coluna e ambos precisarão ser implantados. Pior ainda, o schema compartilhado torna-se, de fato, um modelo compartilhado, e os bounded contexts colapsam novamente em um só.

Consultas entre serviços são o motivo usual para as equipes recorrerem ao banco de dados compartilhado. As soluções são read models e APIs:

  • Solicite ao serviço proprietário, via API, uma resposta pequena e construída para esse propósito.
  • Inscreva-se em seus eventos e mantenha uma projection local moldada para suas consultas.
  • Aceite que algumas consultas precisam de um store de relatórios dedicado, e não de um join entre bancos de dados de serviços.

Uma projection é desnormalizada e eventualmente consistente, que é exatamente por isso que ela é rápida e por que você deve estar confortável com um pequeno atraso (lag). Este é o lado de leitura do CQRS, e é a maneira padrão de responder a perguntas entre serviços sem quebrar a propriedade dos dados.

Sagas em vez de transações distribuídas

Não existem transações ACID que abrangem múltiplos serviços. O two-phase commit exige que todos os participantes mantenham locks e estejam disponíveis, que é precisamente a condição que você não pode garantir através de uma rede, e a maioria dos datastores modernos não oferece suporte a isso. A alternativa é a saga.

Uma saga é uma sequência de transações locais, uma por serviço, onde cada etapa publica um evento ou invoca a próxima. Se uma etapa posterior falhar, a saga executa ações compensatórias para desfazer o trabalho concluído em termos de negócio. Considere a criação de um pedido:

  1. Orders cria o pedido como pending.
  2. Inventory reserva o estoque.
  3. Payments cobra o cliente.
  4. Fulfilment agenda o envio e orders marca o pedido como confirmed.

Se o pagamento falhar, a compensação libera a reserva e cancela o pedido. Note que “desfazer” não é um rollback; é um novo fato de negócio. Você não pode “desenviar” um e-mail de confirmação, então a compensação é um segundo e-mail explicando o cancelamento.

Como cada etapa pode ser repetida, cada etapa deve ser idempotente. O mesmo pedido não deve ser cobrado duas vezes. Derive uma chave de deduplicação da operação de negócio — o ID do pedido — e não de um ID de requisição aleatório, e utilize uma constraint de unicidade ou um SET NX para tornar a verificação atômica. Sagas são a parte mais exigente de microserviços, e ignorar o caminho de compensação é como os sistemas acabam com reservas presas e cobranças duplicadas.

Contratos, versionamento e compatibilidade

O contrato de um serviço é a sua API, e quem a consome depende dela. Trate-o como uma interface publicada com um ciclo de vida:

  • Adicione, não quebre. Novos campos opcionais são seguros; renomear, remover ou alterar o significado de um campo não é.
  • Versione deliberadamente. O versionamento via URL ou header torna as mudanças disruptivas (breaking changes) explícitas, e os guias de REST e versionamento de API cobrem as compensações (trade-offs).
  • Dê tempo aos consumidores. Anuncie depreciações, emita métricas de uso por cliente e remova um endpoint apenas quando ninguém mais o estiver chamando.
  • Teste o contrato de ambos os lados. Testes de contrato orientados ao consumidor (consumer-driven contract tests) capturam casos onde uma mudança “compatível” do produtor não é compatível com um consumidor específico.

A mesma disciplina se aplica aos esquemas de eventos. Um consumidor que lê um tópico verá mensagens escritas por uma versão mais antiga do produtor, portanto, os eventos devem ser aditivos e autodescritivos. Coloque uma versão ou um ID de esquema no envelope e permita que os consumidores tolerem campos desconhecidos.

Discovery, gateways e load balancing

Serviços mudam. Instâncias são reagendadas, escaladas e substituídas, portanto, quem faz a chamada não pode utilizar endereços hard-coded. O Service discovery resolve isso: ou o cliente solicita ao registro as instâncias saudáveis (client-side), ou um endereço virtual estável à frente das instâncias faz isso (server-side). O Kubernetes oferece a segunda opção gratuitamente através de Services e DNS.

Um API gateway fica na borda e lida com as preocupações que todo serviço teria que repetir: autenticação, rate limiting, TLS termination, roteamento de requisições e agregação de respostas. Ele é genuinamente útil. No entanto, ele também se torna um ponto único de falha e, se acumular lógica de negócio, torna-se um monólito distribuído em miniatura. Mantenha o gateway “thin” e empurre as regras específicas de cada funcionalidade de volta para o serviço proprietário.

Load balancing não é apenas round-robin. Conexões de longa duração, sticky sessions e consumidores lentos afetam a uniformidade da distribuição do tráfego. Deixe o load balancer da plataforma fazer o seu trabalho e torne os serviços stateless, para que qualquer instância possa atender a qualquer requisição.

Observabilidade entre serviços

Em um monólito, um stack trace geralmente explica um incidente. Entre serviços, a requisição deixou seu processo há vários saltos e o rastro desapareceu. Três instrumentos restauram isso.

  • Correlation ids. Gere um id na borda (edge), propague-o nos headers e envelopes de eventos, e coloque-o em cada linha de log. Ele é o fio que vincula a ação de um usuário a cada serviço que ela tocou.
  • Distributed tracing. Spans do OpenTelemetry registram o tempo gasto em cada serviço e chamada. Um trace mostra rapidamente qual salto está lento, que é a pergunta que os logs não conseguem responder.
  • Métricas por serviço. Monitore a taxa de requisições, taxa de erro e duração para cada endpoint, além de sinais de saturação como profundidade de fila e uso de pool. Configure alertas para sintomas que seus usuários sentem, não para cada pequena oscilação.

Os logs devem ser JSON estruturado com o nome do serviço e o correlation id, enviados para um único lugar. Um log que você não consegue pesquisar entre serviços é um log que você não usará às 3 da manhã. O guia de Cloud Deployment cobre a parte operacional de coleta desses sinais.

Deadlines e orçamentos de requisição (request budgets)

A latência em uma cadeia de chamadas é aditiva, e cada serviço na cadeia fica apenas “adivinhando” a menos que você propague um deadline. Uma requisição voltada ao usuário que deve responder em 800ms não pode se dar ao luxo de ter quatro chamadas downstream, onde cada uma está disposta a esperar dois segundos.

Atribua um budget (orçamento) a cada requisição na borda (edge) e passe o tempo restante adiante com a chamada. Cada serviço subtrai o tempo de seu próprio processamento e encaminha um deadline menor, para que a cadeia falhe rapidamente em vez de empilhar timeouts.

// The gateway sets the budget once.
const deadline = Date.now() + 800;

await orders.place(input, { deadline }); // forwards the remaining time

Quando um serviço percebe que o deadline expirou, ele deve parar e retornar um erro honesto, em vez de iniciar mais trabalho que não conseguirá concluir. Combine isso com um fallback para chamadas não essenciais: se o serviço de recomendações não puder responder em 100ms, retorne a página sem elas. Um request budget transforma o “às vezes trava” em um p99 previsível, sendo uma das vitórias de confiabilidade mais baratas disponíveis entre serviços.

Implantação e infraestrutura

A implantação independente é uma propriedade do seu pipeline, não apenas da sua topologia. Cada serviço precisa de seu próprio build, teste, imagem e rollout, além de uma maneira de executar migrações de banco de dados sem downtime e um rollback que não dependa de implantar tudo novamente.

Isso geralmente significa containers e um orquestrador. Containers tornam o runtime do serviço portátil e suas dependências explícitas; Kubernetes ou um equivalente gerenciado cuida do agendamento, discovery, health checks, autoscaling e rolling updates. Você também precisará de configurações e secrets entregues por ambiente, e uma estratégia para migrações de schema que permaneça compatível com a versão anterior do código, pois instâncias antigas e novas rodam lado a lado durante um rollout.

Este é o “imposto da plataforma”. Ele é real, é contínuo e é por isso que equipes pequenas devem pensar bem antes de adotar esse estilo.

O monólito distribuído

O monólito distribuído é o pior cenário possível: muitos serviços que ainda precisam ser implantados juntos. Seus sintomas são inconfundíveis.

  • Serviços compartilham um banco de dados, fazendo com que mudanças de schema atravessem as fronteiras das equipes.
  • Um release exige a coordenação de vários serviços simultaneamente.
  • Uma cadeia de chamadas síncronas percorre quatro serviços para uma única ação do usuário.
  • Dois serviços são sempre alterados no mesmo pull request.

Quando você notar isso, significa que as fronteiras estão erradas. As causas comuns são a divisão por camada técnica, a divisão antes que o domínio fosse compreendido ou permitir que um banco de dados compartilhado sobreviva à separação. A solução geralmente é fundir os serviços problemáticos novamente e encontrar um ponto de corte melhor — o que é uma admissão difícil, mas mais barata do que uma década de releases coordenados.

Quando não usar microsserviços

Não faça a divisão se qualquer um destes pontos for verdadeiro:

  • A equipe é pequena o suficiente para que todos consigam fazer o deploy de toda a aplicação com segurança.
  • O domínio ainda está sendo descoberto, portanto, as fronteiras seriam apenas suposições.
  • O produto está em estágio inicial e os requisitos mudam semanalmente.
  • Você ainda não possui a capacidade de CI/CD, observabilidade e regime de on-call para gerenciar múltiplos serviços.

Em todos esses casos, um monólito modular oferece a mesma disciplina interna, mas com um único deploy, transações in-process e refatorações que não exigem um plano de migração. Como seus módulos se comunicam através de interfaces explícitas, um módulo pode ser extraído para um serviço posteriormente, quando surgir um motivo concreto: uma equipe que precise de sua própria cadência, um componente com um perfil de escalonamento genuinamente diferente ou uma fronteira de compliance. Extrair um módulo limpo é muito mais barato do que fundir uma divisão mal feita.

Migrando com o strangler fig

Quando você for dividir um sistema existente, faça isso de forma incremental. O padrão strangler fig roteia o tráfego através de uma facade e move uma funcionalidade por vez até que o código antigo não seja mais utilizado e possa ser deletado.

  1. Encontre uma costura (seam). Escolha uma funcionalidade que já seja razoavelmente autocontida e que mude com frequência. Um limite de módulo bem definido é o melhor candidato.
  2. Coloque uma facade na frente. Roteie o tráfego relevante através de um proxy ou gateway para que você possa desviá-lo sem precisar alterar os clientes.
  3. Extraia o módulo para um serviço. Dê a ele seu próprio deployable e pipeline, e mova suas tabelas para um banco de dados próprio.
  4. Sincronize os dados com cuidado. Utilize dual-write ou publique eventos e faça o backfill até que o novo armazenamento seja a fonte da verdade; então, pare de escrever nas tabelas antigas.
  5. Faça o cut over e remova. Desvie o tráfego, monitore as métricas e delete o caminho do código antigo assim que ele estiver inativo.

Nunca comece com um rewrite do tipo “big-bang”. O valor da abordagem strangler é que cada etapa é reversível e o sistema permanece em produção durante todo o processo.

Definindo o contrato primeiro

Um contrato HTTP interno é fácil de desviar, pois o produtor e o consumidor são escritos por equipes diferentes e apenas os testes de integração percebem isso. Definir o contrato como um schema antes de escrever qualquer um dos lados mantém a consistência e permite gerar clientes, servidores e documentação a partir de uma única fonte.

// inventory.proto
service Inventory {
  rpc GetStock(GetStockRequest) returns (StockLevel);
  rpc Reserve(ReserveRequest) returns (Reservation);
}

message GetStockRequest { string sku = 1; }
message StockLevel { string sku = 1; int32 available = 2; }

Para HTTP, um documento OpenAPI desempenha o mesmo papel. De qualquer forma, o schema é o artefato sob revisão, e uma alteração disruptiva (breaking change) aparece em um diff em vez de aparecer em produção. Testes de contrato orientados ao consumidor (consumer-driven contract tests) completam o ciclo ao codificar o que cada consumidor realmente utiliza, para que um produtor saiba, antes do release, se a alteração é segura para os chamadores existentes.

Combinando o transporte com a interação

O erro comum é utilizar um único transporte para tudo. Um sistema puramente REST serializa cada reação atrás de uma cadeia de chamadas; um sistema puramente baseado em eventos torna uma consulta simples absurdamente indireta. Escolha por interação, não por sistema.

Interação Transporte Por que
Browser para backend REST/JSON Ubíquo, cacheável, fácil de depurar
Serviço para serviço, baixa latência gRPC Tipado, compacto, clientes gerados
Reação do tipo “dispare e esqueça” Events Sem acoplamento temporal, bufferizado
Workflow de longa duração Events mais uma saga Durável, permite tentativas, compensável
Leitura de alto volume Cache ou read model Evita a chamada completamente

Um padrão útil é tornar as escritas autoritativas via chamada síncrona quando quem chama precisa da resposta, e publicar um evento para cada efeito que quem chama não precise esperar.

Chaves de idempotência na prática

Sagas, retries e eventos “at-least-once” significam que a mesma operação pode chegar duas vezes. A única defesa que sobrevive à concorrência é uma verificação de deduplicação que seja atômica com o efeito colateral.

export async function capturePayment(cmd: CapturePayment) {
  const key = `payment:captured:${cmd.orderId}`;
  const inserted = await redis.set(key, "1", "NX", "EX", 86_400);
  if (inserted === null) return { skipped: true };

  return gateway.charge(
    { orderId: cmd.orderId, amountCents: cmd.amountCents },
    { idempotencyKey: cmd.orderId },
  );
}

Três detalhes são fundamentais. A chave é derivada da operação de negócio, e não de um ID de requisição aleatório, para que um produtor que enfileire o mesmo trabalho lógico duas vezes ainda cause uma colisão. A verificação e a escrita do marcador são uma única operação atômica, evitando que dois workers concorrentes vençam simultaneamente. E o provedor recebe sua própria chave de idempotência, pois o seu marcador pode ser perdido enquanto o registro do provedor sobrevive.

Outbox, inbox e efeitos exactly-once

Um serviço que escreve em seu banco de dados e depois publica um evento possui uma lacuna: o processo pode travar entre as duas operações, deixando o estado alterado e o evento não enviado. O padrão outbox resolve isso escrevendo o evento em uma tabela outbox na mesma transação local da alteração de estado, permitindo que um relay publique as linhas.

await db.transaction(async (tx) => {
  await orders.save(order, tx);
  await tx.insert(outbox).values({
    id: randomUUID(),
    topic: "orders.created",
    key: order.id,
    payload: order.toEvent(),
  });
});

// A separate relay polls outbox and publishes, at least once.

O lado do consumidor é a imagem espelhada. Como a entrega é at-least-once, um consumidor pode receber o mesmo evento duas vezes. Mantenha uma tabela inbox com os IDs dos eventos processados e ignore as duplicatas, novamente na mesma transação do efeito. Outbox e inbox juntos proporcionam um processamento effectively-once sem fingir que a rede é confiável.

Bulkheads e degradação graciosa

Um circuit breaker interrompe as chamadas para uma dependência que está falhando. Um bulkhead vai além e isola os recursos para que uma única dependência não consuma tudo. Se o serviço de recomendações tiver seu próprio pool de conexões, uma lentidão nele não poderá esgotar o pool do qual o checkout depende.

const pools = {
  checkout: new Pool({ max: 20 }),
  recommendations: new Pool({ max: 5, timeoutMs: 500 }),
};

A degradação é a parte voltada ao usuário dessa mesma ideia. Quando uma dependência não essencial está lenta, retorne uma resposta reduzida em vez de um erro. Uma página de produto sem recomendações personalizadas ainda é uma página útil; já uma página de produto que sofre timeout porque as recomendações falharam é considerada uma queda no serviço. Decida, para cada chamada, se ela é obrigatória ou “best-effort”, e codifique essa decisão no local onde a chamada é feita.

A camada anti-corrupção (anti-corruption layer)

Quando um serviço consome o modelo de outro diretamente, ele herda as premissas desse modelo, e qualquer alteração no formato do Order do produtor reverbera em todos os consumidores. Uma anti-corruption layer é uma camada fina de tradução que mapeia o contrato externo para a linguagem de domínio do próprio consumidor.

// Shipping has its own idea of a shipment. It translates the event.
function toShipment(event: OrderCreated): ShipmentRequest {
  return {
    reference: event.id,
    destination: event.shippingAddress,
    lines: event.lines.map((l) => ({ sku: l.sku, quantity: l.qty })),
  };
}

Essa camada exige um pouco mais de código, mas garante independência real. O consumidor pode renomear seus próprios conceitos livremente, tolerar campos desconhecidos e absorver mudanças disruptivas (breaking changes) de terceiros que ele não controla.

Como saber se a divisão está funcionando

Microserviços são um meio, não um objetivo; portanto, meça aquilo que você realmente desejava. Os sinais de que a divisão está valendo a pena são organizacionais: o número de equipes que conseguem fazer deploy sem coordenação, o lead time do commit até a produção para um único serviço e a frequência com que uma única alteração afeta vários serviços. Se esses números não estiverem melhorando, a topologia não está compensando o seu custo.

Já os sinais de que as coisas estão indo mal são técnicos e fáceis de identificar:

  • Os deploys ainda acontecem em uma ordem fixa entre os serviços.
  • Uma única ação do usuário gera uma longa cadeia de chamadas síncronas.
  • Uma alteração de schema em um serviço exige o release de outra equipe.
  • A escala de on-call não consegue identificar qual serviço causou um incidente.

Qualquer um desses pontos significa que você tem um monólito distribuído, e a solução geralmente é fundir uma fronteira, e não adicionar outro serviço.

Um checklist antes de separar

Antes de extrair um módulo de um monólito, certifique-se de que todos os pontos abaixo sejam verdadeiros.

  • O módulo já é dono dos seus próprios dados e ninguém mais escreve em suas tabelas.
  • Outros módulos dependem da sua interface, e não de seus internos.
  • Você tem um motivo além de “parece grande”: a cadência de um time, um perfil de escalabilidade ou um limite de conformidade (compliance).
  • Você possui CI/CD, tracing e capacidade de on-call para mais um deployable.
  • Você tem um plano para a migração de dados e uma forma de revertê-la (rollback).
  • Você decidiu como o módulo se comunicará com o restante, de forma síncrona ou assíncrona, e escreveu o contrato.

Se qualquer resposta for “não”, a extração será mais cara do que parece. Corrigir a fronteira dentro do monólito primeiro é quase sempre mais barato do que corrigi-la através de uma rede.

Boas práticas

  • Divida para deploy independente e autonomia do time, não por escalabilidade.
  • Alinhe cada serviço a um bounded context e defina um responsável claro.
  • Dê a cada serviço seu próprio banco de dados e proíba queries entre serviços.
  • Configure timeouts, retentativas limitadas com jitter e circuit breakers em cada chamada.
  • Torne as operações de escrita idempotentes antes de permitir qualquer retentativa.
  • Prefira eventos para reações que não exijam uma resposta imediata.
  • Use sagas com compensações em vez de transações distribuídas.
  • Versione os contratos e garanta que cada alteração seja aditiva.
  • Propague um correlation id e implemente tracing desde o primeiro serviço.
  • Mantenha o gateway leve e mova a lógica de negócio para os serviços.
  • Extraia serviços utilizando o padrão strangler fig, uma capacidade por vez.

Erros comuns

  • Dividir por camada técnica e acabar precisando de três serviços para cada funcionalidade.
  • Compartilhar um banco de dados e se perguntar por que os deploys ainda exigem coordenação.
  • Fazer o deploy de serviços juntos e chamar o resultado de microserviços.
  • Adicionar retries sem idempotência e cobrar os clientes em duplicidade.
  • Deixar timeouts desconfigurados, fazendo com que uma dependência lenta trave toda a cadeia de chamadas.
  • Usar cadeias síncronas onde um evento removeria o acoplamento.
  • Construir uma saga sem um caminho de compensação para os casos de falha.
  • Ignorar o distributed tracing e debugar tentando adivinhar através dos logs.
  • Dividir cedo demais, congelando fronteiras erradas em infraestruturas caras.
  • Tratar Kubernetes e um service mesh como o objetivo, em vez do custo para atingir o objetivo.

Próximos passos

Se as compensações (trade-offs) apresentadas aqui parecerem pesadas para a sua equipe, leia o guia de Monólito Modular — ele é a escolha padrão ideal e a melhor preparação para uma futura divisão. Para fazer com que os serviços se comuniquem sem chamadas diretas entre si, o guia de Arquitetura Orientada a Eventos é o próximo passo, utilizando Kafka para o log subjacente. E, quando você estiver operando múltiplos serviços, o guia de Implantação na Nuvem aborda a plataforma e o trabalho de observabilidade necessários para mantê-los saudáveis.

Na pratica

Contrato, chamada, evento, prontidão

Os quatro artefatos que cada serviço em um sistema saudável produz.

services/orders/http.ts
import Fastify from "fastify";
import { z } from "zod";

const app = Fastify();

const CreateOrder = z.object({
  customerId: z.string().uuid(),
  lines: z
    .array(z.object({ sku: z.string(), qty: z.number().int().positive() }))
    .min(1),
});

app.post("/orders", async (req, reply) => {
  const body = CreateOrder.parse(req.body);
  const order = await orders.create(body);
  reply.code(201).send({ id: order.id, status: order.status });
});

Eventos versus chamadas síncronas

Uma chamada diz 'agora' e espera. Um evento diz 'isso aconteceu' e deixa o outro lado reagir. Escolha por interação, não por sistema.

Prefira eventos
// The order service does not know or care who reacts.
await events.publish("orders.created", { key: order.id, value: order });

// Analytics, email and fulfilment subscribe independently.
Evite cadeias de chamadas
// Every new reaction edits this function and adds a hop
// that can fail before the response is returned.
await analytics.record(order);
await email.sendReceipt(order);
await fulfilment.reserve(order);

Um banco de dados por serviço

Um banco de dados compartilhado é a maneira mais rápida de perder a implantabilidade independente. Uma mudança de schema exigiria que todas as equipes proprietárias lançassem juntas.

Prefira
// orders owns orders_db. Nobody else connects to it.
const order = await ordersDb.insert(orders).values(input);

await events.publish("orders.created", { key: order.id, value: order });
Evite
// shipping reaches into the orders tables directly.
// Now a column rename is a cross-team release.
const rows = await db.query(
  "SELECT id, status FROM orders WHERE customer_id = $1",
  [customerId],
);

Trade-offs

Você deve dividir isso em serviços?

Microservices trocam a simplicidade de processos internos por complexidade operacional e de coordenação. Apenas algumas organizações obtêm retorno nessa troca.

Strengths

  • Equipes entregam sem esperar

    Um serviço pertencente a uma equipe pode ser implantado em seu próprio cronograma. O ciclo de lançamento coordenado desaparece, assim como a maior parte da coordenação entre equipes.

  • Falhas ficam contidas

    Uma falha nas recomendações não derruba o checkout. Bulkheads, implantações separadas e escalonamento independente limitam o raio de explosão.

  • Cada parte escala em sua própria curva

    O serviço de busca pode rodar em muitos nós com CPU pesada enquanto o de faturamento roda em dois pequenos, em vez de escalar a aplicação inteira junta.

Trade-offs

  • A rede torna-se seu problema

    Cada chamada pode ser lenta, pode sofrer timeout ou pode ter sucesso enquanto a resposta é perdida. Timeouts, retries e idempotência agora são preocupações da aplicação.

  • Consistência torna-se mais difícil

    Não existe transação entre serviços. Você precisa de sagas, ações compensatórias e consistência eventual em vez de um único rollback.

  • A conta da plataforma é real

    Containers, orquestração, registries, tracing, gateways e on-call para muitos serviços são custos que um monolith simplesmente não possui.

  • Limites errados são caros

    Se você dividir antes que o domínio esteja claro, terá um monolith distribuído onde cada mudança ainda exige um lançamento coordenado.

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender Microservices?

Nosso tutorial interativo te guia por Microservices passo a passo — com quizzes e codigo real que voce pode executar no navegador.