Outbox | Padrões de Projeto
- Outbox | Padrões de Projeto

Fluxo Outbox + SAGA
┌───────────────────────────────┐
│ SERVICE A │
│ (Order Service) │
└───────────────┬───────────────┘
│
(1) cria pedido
│
┌───────────────v───────────────┐
│ BANCO DO SERVICE A │
│ │
│ orders outbox │
│ - id - id │
│ - status - event_type │
│ ... - payload │
│ - status=PENDING│
└───────────────┬───────────────┘
│ (2) mesma transação:
│ grava pedido + outbox
│
┌───────────────v───────────────┐
│ OUTBOX PUBLISHER/WORKER │
│ lê outbox e publica mensagem │
└───────────────┬───────────────┘
│ (3) publica evento
v
┌───────────────────┐
│ MESSAGE BROKER │
│ (Kafka/RabbitMQ) │
└─────────┬─────────┘
│
│ (4) outros serviços consomem
┌───────────────v───────────────┐
│ SERVICE B │
│ (Payment / Stock etc.) │
└───────────────┬───────────────┘
│
processa etapa
│
publica novo evento
v
┌───────────────────┐
│ MESSAGE BROKER │
└───────────────────┘
│
v
(fluxo continua = SAGA)
Intenção
- O Outbox pattern permite publicar eventos de forma confiável. A ideia é que, quando sua API publica mensagens, ela não as envie diretamente. Em vez disso, as mensagens são persistidas em uma tabela do banco de dados (na mesma transação em que o pedido for gravado) e depois disso, um worker consome esses registros e publica essas mensagens no broker, em intervalos de tempo predefinidos.
Exemplo
- Essa classe é configurada como interceptor do EFCore e quando é chamado o SaveChanges ele entra em ação
public sealed class ConvertDomainEventsToOutboxMessagesInterceptor
: SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
DbContext? dbContext = eventData.Context;
if (dbContext is null)
{
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
var outboxMessages = dbContext.ChangeTracker
.Entries<AggregateRoot>()
.Select(x => x.Entity)
.SelectMany(aggregateRoot =>
{
var domainEvents = aggregateRoot.GetDomainEvents();
aggregateRoot.ClearDomainEvents();
return domainEvents;
})
.Select(domainEvent => new OutboxMessage
{
Id = Guid.NewGuid(),
OccurredOnUtc = DateTime.UtcNow,
Type = domainEvent.GetType().Name,
Content = JsonConvert.SerializeObject(
domainEvent,
new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
})
})
.ToList();
dbContext.Set<OutboxMessage>().AddRange(outboxMessages);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
- Depois é criado um job para realizar o envio desse evento
[DisallowConcurrentExecution]
public class ProcessOutboxMessagesJob : IJob
{
private readonly ApplicationDbContext _dbContext;
private readonly IPublisher _publisher;
public ProcessOutboxMessagesJob(ApplicationDbContext dbContext, IPublisher publisher)
{
_dbContext = dbContext;
_publisher = publisher;
}
public async Task Execute(IJobExecutionContext context)
{
List<OutboxMessage> messages = await _dbContext
.Set<OutboxMessage>()
.Where(m => m.ProcessedOnUtc == null)
.Take(20)
.ToListAsync(context.CancellationToken);
foreach (OutboxMessage outboxMessage in messages)
{
IDomainEvent? domainEvent = JsonConvert
.DeserializeObject<IDomainEvent>(
outboxMessage.Content,
new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
});
if (domainEvent is null)
{
continue;
}
await _publisher.Publish(domainEvent, context.CancellationToken);
outboxMessage.ProcessedOnUtc = DateTime.UtcNow;
}
await _dbContext.SaveChangesAsync();
}
}