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