Pular para conteúdo

Outbox Pattern

Visão Geral

O Outbox Pattern garante que Domain Events sejam publicados no Kafka/Redpanda de forma confiável, mesmo em caso de falhas na infraestrutura de mensageria.

Problema que resolve: Garantir que eventos gerados por um Aggregate sejam sempre publicados, evitando inconsistências entre o banco de dados e o barramento de eventos.

Implementação: Eventos são salvos em uma tabela outbox_messages na mesma transação do banco de dados. Um background service processa e publica esses eventos no Kafka com retry automático.

Conceitos Fundamentais

O Problema

Cenário sem Outbox:

1. Aggregate salva dados no banco ✅
2. Tenta publicar evento no Kafka ❌ (Kafka fora do ar)
3. Evento nunca é publicado
4. Workers nunca processam → Cliente não recebe notificação

Resultado: Inconsistência entre estado do banco e eventos publicados.

A Solução

Cenário com Outbox:

1. Aggregate salva dados no banco ✅
2. Evento salvo em tabela Outbox ✅ (mesma transação!)
3. Commit da transação ✅
4. Background worker lê Outbox periodicamente
5. Publica eventos no Kafka com retry automático
6. Evento marcado como processado

Resultado: Garantia de que todo evento será publicado eventualmente.

Garantias

  • At-Least-Once Delivery: Evento será publicado pelo menos uma vez
  • Atomicidade: Dados e eventos salvos na mesma transação
  • Resiliência: Falhas no Kafka não impedem a operação
  • Auditoria: Histórico completo de todos os eventos

Implementação no Santa Mão

Fluxo Completo

1. Aggregate gera evento

public class Order : BaseAggregateRoot
{
    public void Create(Guid userId, Guid requestId)
    {
        // Lógica de negócio
        Id = Guid.NewGuid();
        Status = OrderStatus.Pending;

        // Adicionar evento
        AddDomainEvent(new OrderCreatedEvent(Id, userId, requestId));
    }
}

2. Evento automaticamente salvo no Outbox

// EventsInterceptor captura eventos durante SaveChanges
// e salva na tabela outbox_messages na MESMA transação
await _unitOfWork.CommitAsync(cancellationToken);

3. Background worker processa

// OutboxProcessorService roda a cada 30 segundos
// 1. Busca eventos pendentes
// 2. Publica no Redpanda via MassTransit
// 3. Marca como processado

Estrutura da Tabela Outbox

CREATE TABLE outbox_messages (
    id UUID PRIMARY KEY,
    event_type VARCHAR(500) NOT NULL,
    topic VARCHAR(500) NOT NULL,
    payload JSONB NOT NULL,
    headers JSONB NOT NULL,
    status INTEGER NOT NULL,        -- 0=Pending, 1=Processed, 2=Failed
    created_at TIMESTAMP NOT NULL,
    processed_at TIMESTAMP NULL,
    retry_count INTEGER DEFAULT 0,
    error_message VARCHAR(2000) NULL,
    locked_by VARCHAR(100) NULL,    -- Para distribuição
    locked_until TIMESTAMP NULL,
    partition_key INTEGER NOT NULL
);

Configuração

appsettings.json:

{
  "OutboxConfiguration": {
    "Enabled": true,
    "ProcessingIntervalSeconds": 30,
    "BatchSize": 100,
    "MaxRetries": 3,
    "RetentionDays": 7,
    "MaxDegreeOfParallelism": 4,
    "EnableDistributedProcessing": true
  }
}

Parâmetros principais:

Parâmetro Descrição Padrão
ProcessingIntervalSeconds Intervalo entre processamentos 30s
BatchSize Eventos processados por vez 100
MaxRetries Tentativas antes de marcar como falha 3
RetentionDays Dias para manter eventos processados 7
MaxDegreeOfParallelism Processamento paralelo 4

Como Usar no Código

Passo 1: Criar o evento

public class OrderCreatedEvent : BaseEvent
{
    public OrderCreatedEvent(Guid orderId, Guid userId, Guid requestId)
        : base(Guid.NewGuid())
    {
        OrderId = orderId;
        UserId = userId;
        RequestId = requestId;
    }

    public Guid OrderId { get; }
    public Guid UserId { get; }
    public Guid RequestId { get; }

    public override string GetTopic() => "santa-mao.order.created";
}

Passo 2: Adicionar evento no Aggregate

public class Order : BaseAggregateRoot
{
    public void Create(Guid userId, Guid requestId)
    {
        Id = Guid.NewGuid();
        UserId = userId;
        Status = OrderStatus.Pending;

        // Evento será automaticamente salvo no Outbox
        AddDomainEvent(new OrderCreatedEvent(Id, userId, requestId));
    }
}

Passo 3: Salvar (Outbox automático)

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand>
{
    public async Task Handle(CreateOrderCommand request, CancellationToken ct)
    {
        var order = Order.Create(request.UserId, request.RequestId);

        await _orderRepository.AddAsync(order, ct);

        // Salva Order + OutboxMessages na MESMA transação
        await _unitOfWork.CommitAsync(ct);

        // Pronto! Evento será publicado automaticamente
    }
}

Benefícios

Confiabilidade - Eventos nunca são perdidos
Consistência - Dados e eventos na mesma transação
Resiliência - Sistema continua funcionando mesmo com Kafka offline
Auditoria - Histórico completo de eventos
Transparência - Desenvolvedores só precisam adicionar eventos no Aggregate
Retry Automático - Sistema retenta automaticamente eventos falhados

Quando Usar e Quando NÃO Usar

✅ Usar Outbox Pattern Quando:

  • Eventos precisam ser publicados com garantia de entrega
  • Consistência entre banco e eventos é crítica
  • Sistema distribuído com múltiplos consumers
  • Auditoria de eventos é necessária

❌ NÃO Usar Outbox Pattern Quando:

  • Sistema simples sem eventos críticos
  • Perda ocasional de eventos é aceitável
  • Overhead de processamento assíncrono não é aceitável

Relacionamento com Outros Padrões

Outbox Pattern trabalha em conjunto com:

  • DDD - Captura Domain Events dos Aggregates
  • CQRS - Garante que Commands publiquem eventos de forma confiável
  • EDA - Fornece garantia de entrega para arquitetura orientada a eventos

Fluxo Integrado:

Aggregate Root (DDD) → Domain Event → Outbox Table → Background Worker → Redpanda (EDA) → Workers

Troubleshooting

Problema: Eventos não sendo processados

  1. Verificar se Enabled está true no appsettings
  2. Verificar logs do OutboxProcessorService
  3. Verificar conexão com Kafka

Problema: Muitos eventos falhados

  1. Ver error_message na tabela
  2. Verificar conectividade com Kafka
  3. Reprocessar manualmente se necessário

Conclusão

O Outbox Pattern no Santa Mão garante que nenhum evento crítico seja perdido, mesmo em cenários de falha.

Implementação:

✅ Transparente para desenvolvedores
✅ Processamento automático em background
✅ Retry automático em caso de falhas
✅ Auditoria completa de eventos

A solução balanceia simplicidade de uso com robustez operacional, permitindo que o time foque em regras de negócio enquanto a infraestrutura garante a entrega confiável dos eventos.


Última atualização: Novembro 2025
Maintainers: Equipe de Arquitetura Santa Mão