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

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

Ответ

Transactional Outbox — это архитектурный паттерн, используемый в системах, основанных на событиях (Event-Driven Architecture), для гарантированной доставки сообщений при наличии транзакций базы данных. Он решает проблему атомарности обновления состояния приложения и публикации соответствующего события.

Проблема: В распределённой системе типичная операция «сохранить данные в БД и отправить событие в брокер сообщений (Kafka, RabbitMQ)» не является атомарной. Может произойти сбой между этими шагами, приводящий к несогласованности: данные сохранены, но событие не отправлено, или наоборот.

Решение: Паттерн предлагает использовать таблицу в БД как промежуточный буфер (Outbox).

Как это работает:

  1. В рамках одной транзакции с основными бизнес-данными приложение сохраняет событие в специальную таблицу Outbox (обычно с полями Id, Type, Payload, CreatedAt, Processed).
  2. Транзакция фиксируется. Гарантируется, что либо и данные, и событие сохранены, либо ни то, ни другое.
  3. Отдельный фоновый процесс (Outbox Processor / Publisher) периодически опрашивает таблицу Outbox на наличие необработанных сообщений.
  4. Для каждого такого сообщения процессор публикует его в брокер сообщений и помечает сообщение как обработанное (или удаляет его).

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

// 1. Модель сообщения Outbox
public class OutboxMessage
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public string Type { get; set; } // e.g., "OrderCreated"
    public string Payload { get; set; } // JSON сериализованное событие
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime? ProcessedAt { get; set; }
}

// 2. Сохранение в рамках транзакции
public async Task PlaceOrder(Order order, AppDbContext context)
{
    using var transaction = await context.Database.BeginTransactionAsync();
    try
    {
        // Сохраняем бизнес-сущность
        context.Orders.Add(order);
        await context.SaveChangesAsync();

        // Сохраняем событие в Outbox в той же транзакции
        var outboxMessage = new OutboxMessage
        {
            Type = "OrderPlaced",
            Payload = JsonSerializer.Serialize(new OrderPlacedEvent(order.Id))
        };
        context.OutboxMessages.Add(outboxMessage);

        await context.SaveChangesAsync();
        await transaction.CommitAsync(); // Всё или ничего
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

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

  • Гарантированная доставка: Решает проблему атомарности «БД -> Брокер».
  • Устойчивость к сбоям: Сообщения не теряются при падении приложения между шагами.
  • Отделение ответственности: Основная бизнес-логика не заботится о деталях доставки в брокер.

Недостатки / Компромиссы:

  • Сложность: Требует дополнительной таблицы и фонового сервиса.
  • Задержка (Near Real-Time): События публикуются не мгновенно, а с задержкой опроса процессора.
  • Возможные дубликаты: Процессор должен быть идемпотентным на случай повторной публикации одного и того же сообщения.