← Назад к вопросам

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

2.0 Middle🔥 121 комментариев
#SOLID и паттерны проектирования#Spring Boot и Spring Data

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Паттерн Outbox

Outbox паттерн — это архитектурный паттерн, используемый в распределенных системах для обеспечения надежной доставки сообщений при асинхронной коммуникации. Паттерн решает проблему потери данных, когда операция в базе данных успешна, но сообщение не отправляется в message broker.

Проблема, которую решает Outbox

Сценарий без Outbox (уязвимость):

1. Приложение 1 обновляет базу данных (заказ создан)
2. Приложение 1 отправляет событие в message broker (RabbitMQ, Kafka)
3. ❌ Сбой перед отправкой или во время отправки
4. Результат: БД обновлена, но событие потеряно
5. Приложение 2 не узнает о новом заказе

Обеспечение атомарности:

Outbox гарантирует, что если транзакция в БД успешна, то сообщение гарантированно будет доставлено.

Как работает Outbox

Архитектура паттерна

┌─────────────────────────────────────────┐
│ Приложение (Пример: Order Service)      │
└────────────────┬────────────────────────┘
                 │
         ┌───────┴───────┐
         │               │
    ┌────▼─────┐   ┌─────▼──────┐
    │  orders  │   │   outbox   │
    │  table   │   │   table    │
    └──────────┘   └──────┬─────┘
         ▲                │
         │                │
  ┌──────┴────────────────▼────────┐
  │ ACID Transaction (одна транзакция)
  │  - INSERT into orders
  │  - INSERT into outbox
  └────────────────────────────────┘
         │
         │
    ┌────▼──────────────┐
    │ Outbox Poller     │ (фоновый процесс)
    │ (читает outbox)   │
    └────┬─────────────┘
         │
  ┌──────▼──────────────┐
  │  Message Broker    │
  │ (RabbitMQ, Kafka)  │
  └────┬───────────────┘
       │
  ┌────▼─────────────────┐
  │ Other Services       │
  │ (получают события)   │
  └──────────────────────┘

Реализация Outbox в Java

1. Создание Outbox таблицы

CREATE TABLE outbox (
    id UUID PRIMARY KEY,
    aggregate_id UUID NOT NULL,
    aggregate_type VARCHAR(255) NOT NULL,
    event_type VARCHAR(255) NOT NULL,
    payload JSON NOT NULL,
    created_at TIMESTAMP NOT NULL,
    published_at TIMESTAMP,
    published BOOLEAN DEFAULT FALSE
);

CREATE INDEX idx_outbox_published ON outbox(published, created_at);

2. Domain Event класс

public abstract class DomainEvent {
    private UUID aggregateId;
    private LocalDateTime createdAt;
    private String eventType;
    
    public DomainEvent(UUID aggregateId) {
        this.aggregateId = aggregateId;
        this.createdAt = LocalDateTime.now();
        this.eventType = this.getClass().getSimpleName();
    }
    
    public UUID getAggregateId() {
        return aggregateId;
    }
    
    public LocalDateTime getCreatedAt() {
        return createdAt;
    }
    
    public String getEventType() {
        return eventType;
    }
}

public class OrderCreatedEvent extends DomainEvent {
    private UUID orderId;
    private String customerName;
    private BigDecimal totalAmount;
    
    public OrderCreatedEvent(UUID orderId, String customerName, BigDecimal totalAmount) {
        super(orderId);
        this.orderId = orderId;
        this.customerName = customerName;
        this.totalAmount = totalAmount;
    }
    
    public UUID getOrderId() {
        return orderId;
    }
    
    public String getCustomerName() {
        return customerName;
    }
    
    public BigDecimal getTotalAmount() {
        return totalAmount;
    }
}

3. Outbox репозиторий

@Repository
public class OutboxRepository {
    private final JdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;
    
    @Autowired
    public OutboxRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
        this.jdbcTemplate = jdbcTemplate;
        this.objectMapper = objectMapper;
    }
    
    public void save(DomainEvent event) {
        String eventPayload = objectMapper.writeValueAsString(event);
        
        String sql = "INSERT INTO outbox (id, aggregate_id, aggregate_type, " +
                "event_type, payload, created_at, published) " +
                "VALUES (?, ?, ?, ?, ?, ?, false)";
        
        jdbcTemplate.update(sql,
            UUID.randomUUID(),
            event.getAggregateId(),
            "Order",
            event.getEventType(),
            eventPayload,
            event.getCreatedAt()
        );
    }
    
    public List<OutboxEvent> findUnpublished() {
        String sql = "SELECT id, aggregate_id, event_type, payload, created_at " +
                "FROM outbox WHERE published = false ORDER BY created_at ASC";
        
        return jdbcTemplate.query(sql, (rs, rowNum) -> new OutboxEvent(
            UUID.fromString(rs.getString("id")),
            UUID.fromString(rs.getString("aggregate_id")),
            rs.getString("event_type"),
            rs.getString("payload"),
            rs.getTimestamp("created_at").toLocalDateTime()
        ));
    }
    
    public void markAsPublished(UUID eventId) {
        String sql = "UPDATE outbox SET published = true, published_at = NOW() " +
                "WHERE id = ?";
        jdbcTemplate.update(sql, eventId);
    }
}

public class OutboxEvent {
    private UUID id;
    private UUID aggregateId;
    private String eventType;
    private String payload;
    private LocalDateTime createdAt;
    
    public OutboxEvent(UUID id, UUID aggregateId, String eventType, String payload, LocalDateTime createdAt) {
        this.id = id;
        this.aggregateId = aggregateId;
        this.eventType = eventType;
        this.payload = payload;
        this.createdAt = createdAt;
    }
    
    // getters
}

4. Order Service с Outbox

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final OutboxRepository outboxRepository;
    
    @Autowired
    public OrderService(OrderRepository orderRepository, OutboxRepository outboxRepository) {
        this.orderRepository = orderRepository;
        this.outboxRepository = outboxRepository;
    }
    
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        UUID orderId = UUID.randomUUID();
        
        // Создаем заказ в базе данных
        Order order = new Order(
            orderId,
            request.getCustomerName(),
            request.getTotalAmount()
        );
        orderRepository.save(order);
        
        // Сохраняем событие в Outbox (в той же транзакции)
        DomainEvent event = new OrderCreatedEvent(
            orderId,
            request.getCustomerName(),
            request.getTotalAmount()
        );
        outboxRepository.save(event);
        
        // Если оба INSERT успешны, транзакция коммитится
        // Если что-то сломается, оба откатываются
        return order;
    }
}

5. Outbox Poller (фоновый процесс)

@Component
public class OutboxPoller {
    private final OutboxRepository outboxRepository;
    private final MessagePublisher messagePublisher;
    private static final Logger logger = LoggerFactory.getLogger(OutboxPoller.class);
    
    @Autowired
    public OutboxPoller(OutboxRepository outboxRepository, MessagePublisher messagePublisher) {
        this.outboxRepository = outboxRepository;
        this.messagePublisher = messagePublisher;
    }
    
    @Scheduled(fixedRate = 1000) // Каждую секунду
    public void pollAndPublish() {
        List<OutboxEvent> unpublishedEvents = outboxRepository.findUnpublished();
        
        for (OutboxEvent event : unpublishedEvents) {
            try {
                // Отправляем в message broker
                messagePublisher.publish(event);
                
                // Отмечаем как опубликованное
                outboxRepository.markAsPublished(event.getId());
                
                logger.info("Published event: {} for aggregate: {}",
                    event.getEventType(), event.getAggregateId());
            } catch (Exception e) {
                logger.error("Failed to publish event: {}", event.getId(), e);
                // Повторная попытка в следующем цикле
            }
        }
    }
}

6. Message Publisher

@Service
public class MessagePublisher {
    private final RabbitTemplate rabbitTemplate;
    private static final String EXCHANGE = "orders.events";
    
    @Autowired
    public MessagePublisher(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }
    
    public void publish(OutboxEvent event) {
        String routingKey = event.getEventType().toLowerCase();
        rabbitTemplate.convertAndSend(
            EXCHANGE,
            routingKey,
            event.getPayload()
        );
    }
}

Гарантии Outbox паттерна

✅ At-Least-Once Delivery:
   - Событие в Outbox гарантирует, что оно будет отправлено
   - Может быть отправлено несколько раз (idempotency требуется)

✅ Atomicity:
   - Создание сущности и запись события атомарны
   - Если одно не удалось, откатывается всё

✅ Eventual Consistency:
   - Другие сервисы в конечном итоге получат событие
   - Может быть задержка, но консистентность гарантирована

Обработка сообщений идемпотентно

@Service
public class OrderEventListener {
    private final ProcessedEventRepository processedEventRepository;
    private final NotificationService notificationService;
    
    @RabbitListener(queues = "order-events.queue")
    public void handleOrderCreated(OrderCreatedEvent event) {
        // Проверяем, не обработано ли уже это событие
        if (processedEventRepository.exists(event.getAggregateId())) {
            return; // Уже обработано, игнорируем
        }
        
        // Обрабатываем событие
        notificationService.sendConfirmationEmail(
            event.getCustomerName(),
            event.getTotalAmount()
        );
        
        // Отмечаем как обработанное
        processedEventRepository.save(event.getAggregateId());
    }
}

Альтернативы Outbox

1. Change Data Capture (CDC)

Добавить триггер в БД, который следит за изменениями
Использовать инструменты типа Debezium

2. Event Sourcing

Вместо хранения состояния, хранишь последовательность событий
Любое изменение = новое событие
Дороже в реализации, но полная история

Преимущества и недостатки

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

  • Гарантирует доставку сообщений
  • Простая реализация
  • Не требует изменения БД архитектуры

Недостатки:

  • Дополнительная таблица (storage overhead)
  • Нужна фоновая задача (poller)
  • Возможны дубликаты сообщений (нужна идемпотентность)
  • Может быть задержка в доставке

Выводы

  • Outbox паттерн решает проблему потери событий
  • Атомарность: событие и данные сохраняются вместе
  • Poller: фоновый процесс, который отправляет события
  • Идемпотентность: обработчик должен уметь обрабатывать дубликаты
  • At-Least-Once: гарантирует минимум одну доставку
  • Используется в микросервисных архитектурах для надежной коммуникации
Что такое паттерн Outbox? | PrepBro