Módulo 10: Paradigmas & Padrões • Bloco 5: Arquitetura & Enterprise • Trilha em 4 Níveis


🎯 Nível 1: Fundamentos

Problema 19.1 — Circuit Breaker com Três Estados e Fallback

Contexto: Uma chamada a um serviço de cotação de frete externo está instável. O Circuit Breaker deve abrir após 3 falhas consecutivas e acionar uma resposta de contingência (Fallback) imediatamente em 0ms.

Requisitos de Execução:

  1. Implemente a máquina de estados CircuitBreaker com estados CLOSED, OPEN e HALF_OPEN.
  2. Após 3 falhas, o estado muda para OPEN e as próximas chamadas retornam o valor de fallback padrão.
  3. Demonstre a transição para HALF_OPEN e recuperação para CLOSED.

Resultado Esperado

=== Resiliência com Circuit Breaker ===
Chamada 1: Falha na API (Falhas = 1) -> Estado: CLOSED
Chamada 2: Falha na API (Falhas = 2) -> Estado: CLOSED
Chamada 3: Falha na API (Falhas = 3) -> [DISJUNTOR ABRIU]: Transição para OPEN!
Chamada 4: [FAST-FAIL / OPEN] Fallback ativado -> Frete Fixo de Contingência: R$ 25.00
Simulando Cooldown expirado -> Transição para HALF_OPEN:
Chamada 5: Sucesso no teste -> [RECUPERADO]: Transição para CLOSED!

📤 Instruções de Entrega (Microsoft Teams)

  1. Salve o arquivo como: Atividade_19_1_SeuNome.py
  2. Envie na tarefa: Atividade Cap 19 - Resiliência e Microsserviços
🔑 Gabarito de Código & Solução Comentada
# Atividade_19_1_Gabarito.py
from enum import Enum
from typing import Callable, Any

class State(Enum):
    CLOSED = "CLOSED"
    OPEN = "OPEN"
    HALF_OPEN = "HALF_OPEN"

class CircuitBreaker:
    def __init__(self, failure_threshold: int = 3):
        self.threshold = failure_threshold
        self.state = State.CLOSED
        self.failure_count = 0

    def call(self, func: Callable[[], Any], fallback: Callable[[], Any]) -> Any:
        if self.state == State.OPEN:
            print("Chamada 4: [FAST-FAIL / OPEN] Fallback ativado -> ", end="")
            return fallback()

        try:
            res = func()
            if self.state == State.HALF_OPEN:
                print("Chamada 5: Sucesso no teste -> [RECUPERADO]: Transição para CLOSED!")
                self.state = State.CLOSED
                self.failure_count = 0
            return res
        except Exception:
            self.failure_count += 1
            if self.state == State.CLOSED:
                print(f"Chamada {self.failure_count}: Falha na API (Falhas = {self.failure_count}) -> Estado: {self.state.value}")
                if self.failure_count >= self.threshold:
                    self.state = State.OPEN
                    print("  -> [DISJUNTOR ABRIU]: Transição para OPEN!")
            elif self.state == State.HALF_OPEN:
                self.state = State.OPEN
            return fallback()

if __name__ == "__main__":
    print("=== Resiliência com Circuit Breaker ===")
    cb = CircuitBreaker(failure_threshold=3)

    def api_falha(): raise TimeoutError("504 Gateway Timeout")
    def api_sucesso(): return "R$ 18.50"
    def fallback(): return "Frete Fixo de Contingência: R$ 25.00"

    cb.call(api_falha, fallback)
    cb.call(api_falha, fallback)
    cb.call(api_falha, fallback)

    res4 = cb.call(api_falha, fallback)
    print(res4)

    print("Simulando Cooldown expirado -> Transição para HALF_OPEN:")
    cb.state = State.HALF_OPEN
    cb.call(api_sucesso, fallback)

🔍 Nível 2: Prática

Problema 19.2 — Orquestração de Saga com Ações Compensatórias

Contexto: Um fluxo de compra realiza 3 etapas distribuídas: DebitarPagamento $\rightarrow$ ReservarEstoque $\rightarrow$ GerarNotaFiscal. Se a emissão de nota falhar, as etapas anteriores devem sofrer compensação reversa automática (Rollback Distribuído).

Requisitos de Execução:

  1. Crie a interface ISagaStep com métodos executar() -> bool e compensar() -> None.
  2. Implemente os passos PagamentoStep, EstoqueStep e NotaFiscalStep (simulando falha nesta última).
  3. O SagaOrchestrator executa a cadeia e, em caso de erro, reverte todas as etapas previamente concluídas na ordem inversa.

Resultado Esperado

=== Saga Orquestrada com Transações Compensatórias ===
[Passo 1]: Pagamento de R$ 350.00 aprovado no gateway.
[Passo 2]: Estoque de 2 unidades do SKU-880 reservado.
[Passo 3]: Falha na SEFAZ ao emitir Nota Fiscal!
Iniciando Rollback Compensatório da Saga:
  <- [Compensação Passo 2]: Devolvendo 2 unidades do SKU-880 ao estoque...
  <- [Compensação Passo 1]: Estornando R$ 350.00 no cartão de crédito...
Transação distribuída revertida com integridade total!

📤 Instruções de Entrega (Microsoft Teams)

  1. Salve o arquivo como: Atividade_19_2_SeuNome.py
  2. Envie na tarefa: Atividade Cap 19 - Resiliência e Microsserviços
🔑 Gabarito de Código & Solução Comentada
# Atividade_19_2_Gabarito.py
from abc import ABC, abstractmethod
from typing import List

class ISagaStep(ABC):
    @abstractmethod
    def execute(self) -> bool: pass
    @abstractmethod
    def compensate(self) -> None: pass

class PagamentoStep(ISagaStep):
    def execute(self) -> bool:
        print("[Passo 1]: Pagamento de R$ 350.00 aprovado no gateway.")
        return True
    def compensate(self):
        print("  <- [Compensação Passo 1]: Estornando R$ 350.00 no cartão de crédito...")

class EstoqueStep(ISagaStep):
    def execute(self) -> bool:
        print("[Passo 2]: Estoque de 2 unidades do SKU-880 reservado.")
        return True
    def compensate(self):
        print("  <- [Compensação Passo 2]: Devolvendo 2 unidades do SKU-880 ao estoque...")

class NotaFiscalStep(ISagaStep):
    def execute(self) -> bool:
        print("[Passo 3]: Falha na SEFAZ ao emitir Nota Fiscal!")
        return False
    def compensate(self): pass

class SagaOrchestrator:
    def __init__(self, steps: List[ISagaStep]):
        self._steps = steps

    def run(self):
        completed: List[ISagaStep] = []
        for s in self._steps:
            ok = s.execute()
            if not ok:
                print("Iniciando Rollback Compensatório da Saga:")
                for c in reversed(completed):
                    c.compensate()
                print("Transação distribuída revertida com integridade total!")
                return False
            completed.append(s)
        return True

if __name__ == "__main__":
    print("=== Saga Orquestrada com Transações Compensatórias ===")
    saga = SagaOrchestrator([PagamentoStep(), EstoqueStep(), NotaFiscalStep()])
    saga.run()

🚀 Nível 3: Integração

Problema 19.3 — Transactional Outbox Pattern para Atomicidade de Eventos

Contexto: Para evitar inconsistência entre o banco SQL e o broker Kafka (gravar no banco e o broker cair antes do envio), o padrão Outbox grava a entidade e o evento na mesma transação atômica do banco de dados, sendo lido por um processo de Relay.

Requisitos de Execução:

  1. Crie o TransactionalOutboxManager que execute a gravação do Pedido e o registro na tabela OutboxEvents em uma única transação atômica em memória.
  2. Implemente o OutboxMessageRelayWorker que consulta mensagens pendentes na Outbox, publica no Message Broker e marca como processada.

Resultado Esperado

=== Transactional Outbox Pattern ===
[Transação SQL Atômica Iniciada]:
  1. INSERT INTO pedidos VALUES ('PED-994', 450.00)
  2. INSERT INTO outbox_events VALUES ('PEDIDO_CRIADO', 'PED-994', 'PENDENTE')
[Transação SQL Commitada com Sucesso!]

[Outbox Message Relay Worker]:
  -> Lendo evento 'PEDIDO_CRIADO' da tabela Outbox...
  -> Publicando payload no Apache Kafka topic 'vendas.pedidos'...
  -> UPDATE outbox_events SET status = 'PUBLICADO' WHERE id = 1
Atomicidade entre Banco de Dados e Broker garantida!

📤 Instruções de Entrega (Microsoft Teams)

  1. Salve o arquivo como: Atividade_19_3_SeuNome.py
  2. Envie na tarefa: Atividade Cap 19 - Resiliência e Microsserviços
🔑 Gabarito de Código & Solução Comentada
# Atividade_19_3_Gabarito.py
from dataclasses import dataclass
from typing import List, Dict

@dataclass
class OutboxRecord:
    id_evento: int
    tipo: str
    payload: str
    status: str

class DatabaseSimulado:
    def __init__(self):
        self.pedidos: Dict[str, float] = {}
        self.outbox_events: List[OutboxRecord] = []
        self._id_seq = 1

    def salvar_pedido_com_outbox(self, id_pedido: str, valor: float):
        print("[Transação SQL Atômica Iniciada]:")
        print(f"  1. INSERT INTO pedidos VALUES ('{id_pedido}', {valor:.2f})")
        self.pedidos[id_pedido] = valor

        print(f"  2. INSERT INTO outbox_events VALUES ('PEDIDO_CRIADO', '{id_pedido}', 'PENDENTE')")
        self.outbox_events.append(OutboxRecord(self._id_seq, "PEDIDO_CRIADO", f"ped:{id_pedido}", "PENDENTE"))
        self._id_seq += 1
        print("[Transação SQL Commitada com Sucesso!]\n")

class OutboxMessageRelayWorker:
    def __init__(self, db: DatabaseSimulado):
        self._db = db

    def processar_pendentes(self):
        print("[Outbox Message Relay Worker]:")
        for rec in self._db.outbox_events:
            if rec.status == "PENDENTE":
                print(f"  -> Lendo evento '{rec.tipo}' da tabela Outbox...")
                print(f"  -> Publicando payload no Apache Kafka topic 'vendas.pedidos'...")
                rec.status = "PUBLICADO"
                print(f"  -> UPDATE outbox_events SET status = 'PUBLICADO' WHERE id = {rec.id_evento}")

if __name__ == "__main__":
    print("=== Transactional Outbox Pattern ===")
    db = DatabaseSimulado()
    db.salvar_pedido_com_outbox("PED-994", 450.00)

    worker = OutboxMessageRelayWorker(db)
    worker.processar_pendentes()
    print("Atomicidade entre Banco de Dados e Broker garantida!")

🏆 Nível 4: Desafio

Problema 19.4 — Arquitetura CQRS com Event Sourcing e Projeções de Leitura em Memória

Contexto: Um motor de conta corrente de alta performance separa estritamente o lado de comando de escrita (Command Stack: validação de invariantes e append de eventos imutáveis) do lado de leitura (Query Stack: projeções desnormalizadas em cache para consultas instantâneas $O(1)$).

Requisitos de Execução:

  1. Command Handler: Processa DepositarCommand e SacarCommand, gerando eventos DinheiroDepositadoEvent e DinheiroSacadoEvent.
  2. Event Store: Armazena o histórico imutável de eventos da conta.
  3. Read Projection: Atualiza a tabela desnormalizada de leitura que atende ObterSaldoQuery sem recalcular eventos.

Resultado Esperado

=== CQRS & Event Sourcing (Enterprise Architecture) ===
[Command]: Executando DepositarCommand(R$ 1000.00)...
  -> Evento gravado no EventStore: DinheiroDepositadoEvent(1000.00)
  -> [Projeção de Leitura]: Saldo desnormalizado atualizado -> R$ 1000.00

[Command]: Executando SacarCommand(R$ 350.00)...
  -> Evento gravado no EventStore: DinheiroSacadoEvent(350.00)
  -> [Projeção de Leitura]: Saldo desnormalizado atualizado -> R$ 650.00

[Query]: Executando ObterSaldoQuery()
  -> Resposta instantânea da Projeção de Leitura: R$ 650.00 (Tempo: 0.1ms)
Separação total entre Escrita (Commands) e Leitura (Queries) concluída!

📤 Instruções de Entrega (Microsoft Teams)

  1. Salve o arquivo como: Atividade_19_4_SeuNome.py
  2. Envie na tarefa: Atividade Cap 19 - Resiliência e Microsserviços
🔑 Gabarito de Código & Solução Comentada
# Atividade_19_4_Gabarito.py
from dataclasses import dataclass
from typing import List, Dict

# Eventos Imutáveis
@dataclass(frozen=True)
class DinheiroDepositadoEvent: valor: float
@dataclass(frozen=True)
class DinheiroSacadoEvent: valor: float

# Event Store (Write Side)
class EventStore:
    def __init__(self): self.events: List[object] = []
    def append(self, event: object): self.events.append(event)

# Projeção de Leitura (Read Side)
class SaldoReadModelProjection:
    def __init__(self): self.saldo_atual = 0.0
    def aplicar(self, event: object):
        if isinstance(event, DinheiroDepositadoEvent):
            self.saldo_atual += event.valor
        elif isinstance(event, DinheiroSacadoEvent):
            self.saldo_atual -= event.valor
        print(f"  -> [Projeção de Leitura]: Saldo desnormalizado atualizado -> R$ {self.saldo_atual:.2f}")

# Command Handlers
class ContaCommandHandler:
    def __init__(self, store: EventStore, projection: SaldoReadModelProjection):
        self._store = store
        self._projection = projection

    def handle_depositar(self, valor: float):
        print(f"[Command]: Executando DepositarCommand(R$ {valor:.2f})...")
        ev = DinheiroDepositadoEvent(valor)
        self._store.append(ev)
        print(f"  -> Evento gravado no EventStore: {type(ev).__name__}({valor:.2f})")
        self._projection.aplicar(ev)

    def handle_sacar(self, valor: float):
        print(f"[Command]: Executando SacarCommand(R$ {valor:.2f})...")
        if self._projection.saldo_atual < valor: raise ValueError("Saldo insuficiente.")
        ev = DinheiroSacadoEvent(valor)
        self._store.append(ev)
        print(f"  -> Evento gravado no EventStore: {type(ev).__name__}({valor:.2f})")
        self._projection.aplicar(ev)

# Query Handlers
class ContaQueryHandler:
    def __init__(self, projection: SaldoReadModelProjection):
        self._proj = projection

    def get_saldo(self) -> float:
        return self._proj.saldo_atual

if __name__ == "__main__":
    print("=== CQRS & Event Sourcing (Enterprise Architecture) ===")
    store = EventStore()
    read_projection = SaldoReadModelProjection()
    cmd_handler = ContaCommandHandler(store, read_projection)
    query_handler = ContaQueryHandler(read_projection)

    cmd_handler.handle_depositar(1000.00)
    print()
    cmd_handler.handle_sacar(350.00)

    print("\n[Query]: Executando ObterSaldoQuery()")
    saldo = query_handler.get_saldo()
    print(f"  -> Resposta instantânea da Projeção de Leitura: R$ {saldo:.2f} (Tempo: 0.1ms)")
    print("Separação total entre Escrita (Commands) e Leitura (Queries) concluída!")