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.
@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;
}{
"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
- Outbox event router (Debezium) (opens in a new tab)
- Reliable microservices data exchange with the outbox pattern (Debezium) (opens in a new tab)
- Delivery guarantees (Debezium) (opens in a new tab)
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.