Módulo 10: Paradigmas de Programação & Padrões de Projeto • Bloco 5: Arquitetura e Enterprise Patterns

🌐 Sistemas que Falham Parcialmente: Em um monólito, uma chamada de método ou funciona ou lança exceção. Em microsserviços, uma chamada de rede pode demorar indefinidamente, falhar depois de ter executado ou executar duas vezes. As Oito Falácias da Computação Distribuída (Deutsch, 1994) — “a rede é confiável”, “a latência é zero”… — derrubam quem as ignora. Neste capítulo, estudamos os padrões que tornam sistemas distribuídos resilientes: Retry com Backoff, Circuit Breaker, Saga, Transactional Outbox e CQRS.


🗺️ Mapa Conceitual do Capítulo

flowchart TD
    ENT_DIST["Padrões Enterprise Distribuídos"]

    ENT_DIST --> RES["1. Resiliência de Chamadas<br/>• Timeout, Retry com Backoff + Jitter<br/>• Circuit Breaker: Closed, Open, Half-Open<br/>• Bulkhead e Fallback"]

    ENT_DIST --> SAGA["2. Saga (Transações Distribuídas)<br/>• Sequência de transações locais<br/>• Compensações em ordem inversa<br/>• Orquestração vs. Coreografia"]

    ENT_DIST --> DADOS["3. Consistência de Dados e Eventos<br/>• Transactional Outbox + Relay<br/>• Idempotência e entrega at-least-once<br/>• CQRS: escrita e leitura separadas"]

    style ENT_DIST fill:#ede7f6,stroke:#512da8,stroke-width:2px
    style RES fill:#e8f5e9,stroke:#4caf50,stroke-width:1.5px
    style SAGA fill:#fff3e0,stroke:#ff9800,stroke-width:1.5px
    style DADOS fill:#e1f5fe,stroke:#03a9f4,stroke-width:1.5px

🔁 1. Timeout e Retry com Exponential Backoff e Jitter

A primeira linha de defesa é nunca esperar para sempre: toda chamada remota precisa de um timeout. A segunda é tentar de novo falhas transitórias (oscilação de rede, 503, deadlock de banco):

  • Backoff exponencial: espera 100 ms, 200 ms, 400 ms, 800 ms… para dar tempo ao serviço de se recuperar.
  • Jitter (aleatoriedade): sem ele, mil clientes que falharam juntos tentam de novo no mesmo instante, criando picos sincronizados (thundering herd). O full jitter sorteia a espera entre zero e o teto exponencial.
espera = random.uniform(0, min(teto_s, base_s * 2 ** tentativa))  # full jitter

⚠️ Só repita operações idempotentes. Repetir um POST /pagamentos que já foi processado (mas cuja resposta se perdeu) cobra o cliente duas vezes. Use chaves de idempotência (Idempotency-Key) para que o servidor reconheça e descarte duplicatas.


⚡ 2. O Padrão Circuit Breaker (Disjuntor)

Descrito por Michael Nygard em Release It! (2007) e popularizado pelo Netflix Hystrix, Resilience4j (Java) e Polly (.NET).

2.1 O Problema: Falhas em Cascata

Se o serviço de frete trava, cada requisição de checkout prende uma thread esperando o timeout. Em segundos, o pool de threads do checkout se esgota — e agora o checkout está fora do ar, derrubando quem depende dele. Retry, nesse cenário, piora o problema.

2.2 A Máquina de Estados

stateDiagram-v2
    [*] --> Closed
    Closed --> Closed: sucesso (zera contador)
    Closed --> Open: N falhas seguidas (ex. 3)
    Open --> Open: chamadas rejeitadas com fast-fail + fallback
    Open --> HalfOpen: cooldown expirou (ex. 30s)
    HalfOpen --> Closed: chamada de teste com sucesso
    HalfOpen --> Open: chamada de teste falhou
  1. CLOSED (fechado): operação normal; as falhas são contadas.
  2. OPEN (aberto): o disjuntor rejeita imediatamente (fast-fail), sem chamar o serviço, e aciona um fallback (valor em cache, resposta padrão, fila para depois).
  3. HALF-OPEN (semiaberto): após o cooldown, deixa passar uma chamada de teste para decidir se volta a fechar.

2.3 Padrões Companheiros

  • Bulkhead (anteparas): pools de threads/conexões separados por dependência, como compartimentos estanques de um navio — a pane do frete não afunda o pagamento.
  • Fallback: a resposta degradada precisa ser pensada com o negócio (mostrar “frete a calcular” é melhor que um erro 500).
  • Rate Limiter: protege o seu serviço de clientes abusivos, complementando o disjuntor, que protege você de dependências instáveis.

📜 3. O Padrão Saga (Transações Distribuídas)

3.1 Por que Não Usar uma Transação ACID Global?

Com um banco por serviço (Database-per-Service), não existe um único BEGIN ... COMMIT. O protocolo 2PC (Two-Phase Commit) até resolve, mas bloqueia recursos enquanto espera todos os participantes, não escala e não é suportado por brokers como o Kafka.

3.2 Transações Locais + Compensações

Uma Saga (Garcia-Molina & Salem, 1987) divide a operação em transações locais. Se o passo k falhar, executam-se as transações compensatórias dos passos k−1 … 1, em ordem inversa:

Passo Ação (transação local) Compensação
1 Criar pedido PENDENTE Marcar pedido CANCELADO
2 Pré-autorizar R$ 240,00 no cartão Liberar a pré-autorização
3 Reservar 2 unidades no estoque Devolver as unidades
4 Confirmar pedido — (último passo, não precisa)

💡 Compensar não é apagar: o pedido cancelado continua existindo, com histórico. Sagas oferecem consistência eventual (propriedades ACD, sem o “I” de isolamento), então telas e relatórios precisam lidar com estados intermediários como PENDENTE.

3.3 Orquestração vs. Coreografia

flowchart LR
    subgraph Orq["Orquestração"]
        O["Orquestrador da Saga"] --> P1["Pedidos"]
        O --> P2["Pagamentos"]
        O --> P3["Estoque"]
    end
    subgraph Cor["Coreografia"]
        C1["Pedidos"] -- "PedidoCriado" --> C2["Pagamentos"]
        C2 -- "PagamentoAutorizado" --> C3["Estoque"]
        C3 -- "EstoqueEsgotado" --> C2
    end
Critério Orquestração Coreografia
Controle do fluxo Centralizado no orquestrador Distribuído em eventos
Visibilidade Fluxo legível em um só lugar Fluxo espalhado entre serviços
Acoplamento Orquestrador conhece todos Serviços só conhecem eventos
Ferramentas Temporal, Camunda, AWS Step Functions Kafka, RabbitMQ, EventBridge

📦 4. Transactional Outbox e CQRS

4.1 O Problema da Escrita Dupla (Dual Write)

Salvar o pedido no banco e publicar PedidoCriado no Kafka são duas operações em sistemas diferentes. Se o processo cair entre elas, o banco diz uma coisa e o resto do sistema nunca fica sabendo.

4.2 A Solução: Outbox na Mesma Transação

sequenceDiagram
    participant S as Serviço de Pedidos
    participant DB as Banco (pedidos + outbox)
    participant R as Relay / CDC (Debezium)
    participant K as Broker (Kafka)
    S->>DB: BEGIN, UPDATE pedidos, INSERT outbox, COMMIT
    Note over S,DB: dado e evento gravados atomicamente
    R->>DB: lê eventos com publicado = 0
    R->>K: publica eventos
    R->>DB: marca publicado = 1

O relay garante entrega pelo menos uma vez (at-least-once): se cair depois de publicar e antes de marcar, publicará de novo. Por isso os consumidores precisam ser idempotentes (registrando os IDs de eventos já processados).

4.3 CQRS (Command Query Responsibility Segregation)

Proposto por Greg Young a partir do princípio CQS de Bertrand Meyer: separar o modelo de escrita (normalizado, com as regras de negócio) do modelo de leitura (desnormalizado, otimizado para as telas). Os eventos da outbox alimentam as projeções de leitura (Elasticsearch, Redis, tabelas materializadas). Use com parcimônia: CQRS adiciona consistência eventual e infraestrutura extra, e só compensa quando os padrões de leitura e escrita são muito diferentes.


📊 5. Matriz Comparativa Sistemática

Padrão Problema que resolve Custo principal Ferramentas de mercado
Retry + Backoff + Jitter Falhas transitórias Duplicidade se a operação não for idempotente Tenacity, Polly, Resilience4j
Circuit Breaker Falhas em cascata Definir fallback e limites adequados Resilience4j, Polly, Istio
Bulkhead Esgotamento de recursos compartilhados Mais pools para configurar Resilience4j, pools dedicados
Saga Transações entre bancos distintos Consistência eventual e compensações Temporal, Camunda, Step Functions
Transactional Outbox Escrita dupla banco + broker Relay/CDC e consumidores idempotentes Debezium, Kafka Connect
CQRS Leitura e escrita com necessidades opostas Projeções e atraso de sincronização Axon, EventStoreDB, MediatR