Что такое паттерн Transactional Outbox (Inbox)?

«Что такое паттерн Transactional Outbox (Inbox)?» — вопрос из категории Архитектура, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Transactional Outbox (иногда называемый Inbox для потребителя) — это архитектурный паттерн, гарантирующий точно-однократную (exactly-once) или хотя-бы-однократную (at-least-once) доставку сообщений/событий в распределённых системах. Он решает проблему атомарности обновления базы данных и отправки сообщения в брокер (например, RabbitMQ, Kafka).

Проблема: В микросервисной архитектуре типична операция: "сохранить заказ в БД И отправить событие OrderCreated". Если после сохранения в БД происходит сбой до отправки события, система становится несогласованной.

Решение Transactional Outbox:

  1. Атомарная запись: Сообщение сохраняется в специальную таблицу Outbox в той же транзакции, что и основное бизнес-состояние.
  2. Фоновая доставка: Отдельный фоновый процесс (Outbox Processor или Relay) периодически опрашивает таблицу Outbox, отправляет новые сообщения в брокер и помечает их как отправленные.

Пример реализации на C# (Entity Framework Core):

// 1. Модель сообщения в outbox
public class OutboxMessage
{
    public Guid Id { get; set; } // Для идемпотентности
    public DateTime OccurredOn { get; set; }
    public string Type { get; set; } // "OrderCreated"
    public string Payload { get; set; } // JSON данных события
    public DateTime? ProcessedDate { get; set; }
}

// 2. Использование в сервисе заказов
public async Task PlaceOrder(OrderDetails details)
{
    using var transaction = await _dbContext.Database.BeginTransactionAsync();

    try
    {
        // Сохраняем бизнес-объект
        var order = new Order { ... };
        _dbContext.Orders.Add(order);
        await _dbContext.SaveChangesAsync();

        // АТОМАРНО сохраняем событие в ту же БД
        var outboxMessage = new OutboxMessage
        {
            Id = Guid.NewGuid(),
            OccurredOn = DateTime.UtcNow,
            Type = "OrderCreated",
            Payload = JsonSerializer.Serialize(new 
            { 
                OrderId = order.Id,
                Total = order.Total 
            })
        };
        _dbContext.OutboxMessages.Add(outboxMessage);

        await _dbContext.SaveChangesAsync(); // Одна транзакция!
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
    // Фоновый сервис (например, Hangfire job или IHostedService) 
    // позже заберёт и отправит это сообщение.
}

Преимущества:

  • Гарантированная доставка: Событие не потеряется даже при падении сервиса сразу после коммита транзакции.
  • Согласованность: Бизнес-данные и факт генерации события всегда синхронизированы.
  • Производительность: Основная операция не блокируется на сетевой вызов к брокеру.

Варианты: Для повышения производительности опроса таблицы Outbox можно использовать механизмы, отслеживающие изменения в БД (CDC), например, Debezium или SQL Server Change Tracking.