Skip to content
Kafka Is Fun

Enterprise Architecture · Enterprise · Topic 1 of 5

Outbox Pattern

A service cannot update its database and publish to Kafka in one transaction: Kafka cannot take part in a distributed (XA) transaction. The outbox pattern writes the event to an outbox table in the same database transaction as the business change, and a separate relay, here Debezium reading the database's transaction log, publishes it to Kafka afterwards. Delivery to Kafka is at-least-once.

OrderService.java (Outbox pattern)java
@Service
@Slf4j
public class OrderService {

    private final OrderRepository orderRepo;
    private final OutboxRepository outboxRepo;

    @Transactional  // one database transaction covers both writes
    public Order createOrder(CreateOrderRequest req) {
        // 1. Business write
        Order order = orderRepo.save(Order.from(req));

        // 2. Outbox row in the SAME transaction (insert only: Debezium does not allow updates)
        OutboxEvent outboxEvent = OutboxEvent.builder()
            .aggregateId(order.getId())
            .aggregateType("Order")
            .type("OrderCreated")
            .payload(objectMapper.writeValueAsString(new OrderCreatedEvent(order)))
            .build();
        outboxRepo.save(outboxEvent);

        return order;
        // Debezium reads the outbox insert from the database log and publishes it to Kafka
    }
}

// Outbox entity: column names match the event router's defaults
@Entity
@Table(name = "outbox")
public class OutboxEvent {
    @Id @GeneratedValue private UUID id;
    @Column(name = "aggregateid") private String aggregateId;
    @Column(name = "aggregatetype") private String aggregateType;
    private String type;
    @Column(columnDefinition = "TEXT") private String payload;
}
Debezium connector config (JSON)json
{
  "name": "outbox-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "postgres",
    "database.dbname": "orders_db",
    "table.include.list": "public.outbox",
    "transforms": "outbox",
    "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
    "transforms.outbox.table.field.event.id": "id",
    "transforms.outbox.table.field.event.key": "aggregateid",
    "transforms.outbox.table.field.event.payload": "payload",
    "transforms.outbox.route.by.field": "aggregatetype",
    "transforms.outbox.route.topic.replacement": "orders.${routedByValue}",
    "transforms.outbox.table.fields.additional.placement": "type:header:eventType"
  }
}

Sources

Checked against Spring for Apache Kafka 4.1.1, Spring Boot 4.1.1 and Kafka 4.2 documentation on 11 October 2026. Draft: not yet reviewed by a Kafka specialist, and releases move on, so confirm against the current documentation before you act on it.