Estratégias de decomposição de monolitos em microservices
1. Fundamentos da Decomposição de Monolitos
1.1. Definição de monolito vs. microservices
Um monolito é uma aplicação única onde todos os componentes (interface, lógica de negócio, acesso a dados) são executados em um único processo. Suas vantagens incluem simplicidade inicial, facilidade de deploy e baixa latência interna. As desvantagens surgem com o crescimento: acoplamento excessivo, dificuldade de escalar componentes específicos e impacto de falhas em toda a aplicação.
Microservices decompõem a aplicação em serviços independentes, cada um com seu próprio domínio, banco de dados e ciclo de vida. Vantagens: escalabilidade granular, isolamento de falhas e autonomia de equipes. Desvantagens: complexidade de rede, consistência eventual e custo operacional.
1.2. Critérios de decisão
Decomponha quando:
- Equipes crescem e o deploy monolítico se torna gargalo
- Partes do sistema têm requisitos de escalabilidade distintos
- Há necessidade de tecnologias diferentes para funcionalidades específicas
Evite quando:
- O domínio é pequeno e estável
- A equipe não tem maturidade em operações distribuídas
- O custo de migração supera os benefícios esperados
1.3. Riscos comuns
Os principais riscos incluem latência de rede entre serviços, consistência de dados eventual (não mais transações ACID simples) e aumento exponencial da complexidade operacional. A migração mal planejada pode resultar em um "distributed monolith", onde serviços continuam acoplados.
2. Estratégias de Decomposição Baseadas em Domínio
2.1. Decomposição por bounded contexts (DDD)
O Domain-Driven Design propõe a identificação de contextos delimitados — áreas do domínio com linguagem e regras próprias. Exemplo prático:
# Contexto original (monolito): Sistema de e-commerce
# Bounded contexts identificados:
# - Catálogo de Produtos (gerencia inventário, preços)
# - Pedidos (processa compras, pagamentos)
# - Clientes (cadastro, histórico)
# - Logística (frete, entregas)
Cada contexto se torna um microservice candidato, com seu próprio modelo de dados e API.
2.2. Decomposição por capacidade de negócio
Identifique funcionalidades que mudam independentemente. Exemplo:
# Capacidades de negócio do monolito:
# 1. Autenticação e autorização → Service Auth
# 2. Processamento de pagamentos → Service Payment
# 3. Notificações (email/SMS) → Service Notification
# 4. Relatórios e analytics → Service Reports
2.3. Decomposição por subdomínio
Priorize subdomínios core (diferenciais competitivos) primeiro, seguidos por supporting e generic:
# Priorização de extração:
# Core: Engine de recomendações (diferencial competitivo)
# Supporting: Gestão de estoque (necessário, mas não único)
# Generic: Autenticação (pode usar serviço terceirizado)
3. Padrões de Extração Incremental
3.1. Strangler Fig Pattern
Substitua funcionalidades gradualmente. Exemplo de roteamento:
# Roteador inicial (monolito + microservice)
# Se rota = /api/v2/pagamentos → Service Payment
# Senão → Monolito legado
# Após migração completa:
# Todas as rotas → Microservices
# Monolito → Descomissionado
3.2. Branch by Abstraction
Crie interfaces abstratas para isolar componentes:
# Antes: Módulo de pagamento chama gateway diretamente
class PagamentoService:
def processar(self, pedido):
return GatewayPagamento.cobrar(pedido.valor)
# Depois: Interface abstrata
class PagamentoInterface:
def processar(self, pedido): pass
class PagamentoMonolito(PagamentoInterface):
def processar(self, pedido):
return GatewayPagamento.cobrar(pedido.valor)
class PagamentoMicroservice(PagamentoInterface):
def processar(self, pedido):
return requests.post("http://payment-service/charge", pedido)
3.3. Parallel Run
Execute ambos sistemas simultaneamente para validação:
# Fluxo paralelo:
# 1. Monolito processa pedido normalmente
# 2. Microservice também processa (em background)
# 3. Comparador verifica resultados
# 4. Se discrepância: alerta, usa resultado do monolito
# 5. Após validação consistente: ativa microservice
4. Gerenciamento de Dados e Transações
4.1. Database per Service
Cada serviço gerencia seu próprio banco. Estratégia de migração:
# Migração de dados de pedidos:
# 1. Criar banco separado para service-pedidos
# 2. Sincronizar dados em lote (ETL)
# 3. Ativar dual-write (escrever em ambos)
# 4. Verificar consistência
# 5. Remover escrita no banco original
4.2. Eventual consistency e sagas
Para transações distribuídas, use sagas coreografadas:
# Saga: Processar pedido
# 1. Service Pedido: reserva item (evento: ItemReservado)
# 2. Service Pagamento: cobra cartão (evento: PagamentoConfirmado)
# 3. Service Estoque: baixa estoque (evento: EstoqueAtualizado)
# 4. Se falha no pagamento: evento de compensação
# - Service Pedido: cancela reserva
4.3. Event sourcing e CQRS
Padronize comunicação com eventos:
# Evento: PedidoCriado
{
"eventId": "uuid",
"type": "PedidoCriado",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"pedidoId": "123",
"clienteId": "456",
"itens": ["SKU-001", "SKU-002"],
"total": 150.00
}
}
5. Integração e Comunicação entre Serviços
5.1. APIs RESTful e gRPC
Definição de contratos com versionamento:
# API REST do Service Pedidos
GET /api/v1/pedidos/{id}
POST /api/v1/pedidos
PUT /api/v2/pedidos/{id}/status
# gRPC proto
service PedidoService {
rpc CriarPedido (CriarPedidoRequest) returns (PedidoResponse);
rpc ConsultarPedido (ConsultarPedidoRequest) returns (PedidoResponse);
}
5.2. Mensageria assíncrona
Exemplo com Kafka:
# Producer (Service Pedidos)
kafka_producer.send(
topic='pedidos-criados',
value={'pedidoId': '123', 'clienteId': '456'}
)
# Consumer (Service Estoque)
for message in kafka_consumer:
pedido = message.value
estoque_service.atualizar_estoque(pedido)
5.3. Service mesh (Istio/Envoy)
Gerenciamento de tráfego e segurança:
# VirtualService (Istio)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: pedido-service
spec:
hosts:
- pedido-service
http:
- match:
- uri:
prefix: /api/v2
route:
- destination:
host: pedido-service-v2
- route:
- destination:
host: pedido-service-v1
6. Testes e Qualidade na Migração
6.1. Testes de contrato
Validam interfaces entre serviços:
# Teste de contrato (Pact)
# Provedor: Service Pedidos
# Consumidor: Service Pagamento
pact = Pact(consumer='pagamento-service', provider='pedido-service')
pact.given('pedido existe').upon_receiving(
'consulta de pedido'
).with_request('GET', '/api/v1/pedidos/123').will_respond_with(200, body={
'id': '123', 'status': 'pendente', 'total': 150.00
})
6.2. Testes de integração e caixa de areia
Simule ambiente distribuído:
# Ambiente de teste (Docker Compose)
services:
pedido-service:
image: pedido-service:test
environment:
- DB_HOST=pedido-db
- KAFKA_BROKER=kafka:9092
pedido-db:
image: postgres:13
kafka:
image: confluentinc/cp-kafka:latest
6.3. Monitoramento contínuo
Métricas pós-decomposição:
# Métricas chave
# - Latência p95 entre serviços
# - Taxa de erros (HTTP 5xx)
# - Throughput de mensagens
# - Consistência de dados (comparação periódica)
# - Tempo de resposta de sagas
7. Automação e Infraestrutura para Microservices
7.1. CI/CD para múltiplos serviços
Pipelines independentes:
# Pipeline do Service Pagamento
# 1. Build: mvn clean package
# 2. Testes unitários e de contrato
# 3. Build imagem Docker
# 4. Push para registry
# 5. Deploy no Kubernetes (canary)
# 6. Smoke tests
# 7. Rollout completo
7.2. Containerização e orquestração
Deploy com Kubernetes:
# Deployment do Service Pedidos
apiVersion: apps/v1
kind: Deployment
metadata:
name: pedido-service
spec:
replicas: 3
selector:
matchLabels:
app: pedido-service
template:
metadata:
labels:
app: pedido-service
spec:
containers:
- name: pedido-service
image: pedido-service:1.2.3
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
7.3. Observabilidade distribuída
Tracing com Jaeger:
# Configuração de tracing
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("processar_pedido"):
# Chamada ao serviço de pagamento
with tracer.start_as_current_span("cobrar_pagamento"):
payment_service.cobrar(pedido)
# Chamada ao serviço de estoque
with tracer.start_as_current_span("atualizar_estoque"):
estoque_service.atualizar(pedido)
Referências
- Martin Fowler - Strangler Fig Application — Artigo original sobre o padrão de substituição gradual de monolitos
- Microsoft - Microservices Architecture Guide — Guia completo de arquitetura de microservices com exemplos práticos
- Sam Newman - Building Microservices (O'Reilly) — Livro referência sobre design e implementação de microservices
- Uber - Domain-Driven Design e Microservices — Estudo de caso real de decomposição de monolito na Uber
- Kubernetes Documentation - Microservices — Documentação oficial sobre orquestração de microservices com Kubernetes
- Confluent - Event-Driven Microservices with Kafka — Tutoriais sobre mensageria assíncrona e event sourcing
- Jaeger - Distributed Tracing — Documentação oficial de tracing distribuído para observabilidade em microservices