Files
java-design-patterns/transactional-outbox
Mukul HowaleandGitHub 3dad8220bb feat: transactional outbox pattern (#3589)
* Add transactional-outbox module

Introduce a new transactional-outbox Maven module demonstrating the Transactional Outbox pattern. Adds a Spring Boot sample app and README explaining the pattern. New code includes Order and OutboxEvent entities, OrderStatus/EventStatus enums, Spring Data repositories (OrderRepository, OutboxRepository), OrderService (atomic write of order + outbox), OutboxPublisher (scheduled poll & publish), MessageBroker interface and MessageConsumer implementation, and App entrypoint. Includes unit tests (AppTest, OrderServiceTest, OutboxPublisherTest). Root pom.xml updated to register the new module. Module uses Spring Data JPA, Spring Web, Lombok, H2 and standard test dependencies.

* Add OutboxPublisher edge-case tests

Add two unit tests for OutboxPublisher: one verifies processOutboxEvents returns an empty list when there are no pending events; the other simulates a MessageBroker exception to ensure the event status is set to FAILED and the repository.save is called. Added necessary Mockito and assertion imports to support the tests.

* Use MockitoExtension for OutboxPublisher test

Replace the legacy inline/mock-based test with a JUnit5 + MockitoExtension unit test. Adds OutboxPublisherUnitTest (uses @ExtendWith(MockitoExtension.class), @Mock and @InjectMocks) that verifies broker exceptions mark events as FAILED and that the repository.save(...) is called. Removes the duplicate inline-mocking test and unused Mockito imports from OutboxPublisherTest.

* Replace Lombok @Slf4j with explicit Logger

Remove Lombok's @Slf4j and add explicit org.slf4j.Logger/LoggerFactory fields in transactional-outbox classes.
This makes logging explicit and removes reliance on Lombok's @Slf4j annotation.

* Add scheduled wrapper for outbox publishing

Introduce publishOutboxEvents() annotated with @Scheduled(fixedDelay = 5000) that delegates to the existing processOutboxEvents(). This separates the scheduling concern from the transactional processing method; processOutboxEvents() remains @Transactional and now only handles fetching and dispatching pending OutboxEvent items.

* Use UTC timestamps and add OutboxPublisher ctor

Use UTC for timestamps in OrderService and OutboxPublisher by switching LocalDateTime.now() to LocalDateTime.now(ZoneOffset.UTC). Replace Lombok @RequiredArgsConstructor on OutboxPublisher with an explicit constructor for dependency injection and remove the @Transactional annotation from processOutboxEvents. Update processedAt assignment to use UTC as well.
2026-08-30 14:02:32 +03:00
..

title, shortTitle, description, category, language, tag
title shortTitle description category language tag
Transactional Outbox Pattern in Java: Ensuring Reliable Event Publishing Transactional Outbox Learn how to implement the Transactional Outbox pattern in Java using Spring Boot and H2. Master reliable event publishing and eliminate dual-write inconsistencies in microservices. Architectural en
Spring Boot
Microservices
Event-Driven
Messaging
Persistence

Also known as

  • Outbox Pattern
  • Application Event Outbox
  • Transactional Event Outbox

Intent of Transactional Outbox Pattern

The Transactional Outbox pattern reliably publishes events in microservices architectures without requiring distributed transactions (XA/2PC). By persisting business data and event notifications in the same database transaction, it guarantees that message publishing always stays consistent with database changes.

Detailed Explanation of the Pattern with Real-World Examples

Real-world analogy

Imagine writing an important contract and placing the outgoing notice into a postal outbox tray located right next to your desk in a single action. Even if the mail courier arrives later, the document is securely staged in the outbox tray and cannot be lost. A dedicated mail clerk periodically inspects the outbox tray and delivers the letters to the post office.

In plain words

Instead of updating the database and publishing a message directly to a message broker in two separate network calls, a service writes both the business entity and an outbox event into the database within a single database transaction. A separate background process periodically reads pending outbox events and publishes them to the message broker.

Architecture Flow

+-------------------------------------------------------------+
|                      Service Boundary                       |
|                                                             |
|  +--------------------+         +------------------------+  |
|  |   Order Service    |         |   Outbox Publisher     |  |
|  +--------------------+         +------------------------+  |
|            |                                | (Polls)       |
|  (Atomic Transaction)                       v               |
|            |                    +------------------------+  |
|            +------------------> | Outbox Table (Pending) |  |
|            |                    +------------------------+  |
|            v                                | (Publishes)   |
|  +--------------------+                     v               |
|  |    Orders Table    |         +------------------------+  |
|  +--------------------+         |     Message Broker     |  |
|                                 +------------------------+  |
+-------------------------------------------------------------+

Programmatic Example (Spring Boot)

Order & Outbox Entities

The Order entity represents business data, while OutboxEvent represents the event payload staged for asynchronous publishing.

@Entity
@Table(name = "orders")
@Data
@Builder
public class Order {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String customerName;
  private String productName;
  private double amount;
  @Enumerated(EnumType.STRING)
  private OrderStatus status;
}

@Entity
@Table(name = "outbox_events")
@Data
@Builder
public class OutboxEvent {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String aggregateType;
  private String aggregateId;
  private String eventType;
  private String payload;
  @Enumerated(EnumType.STRING)
  private EventStatus status;
  private LocalDateTime createdAt;
  private LocalDateTime processedAt;
}

Atomic Transactional Write (OrderService)

The service saves the order entity and creates an outbox event in the same transactional context using Spring's @Transactional.

@Service
@RequiredArgsConstructor
public class OrderService {

  private final OrderRepository orderRepository;
  private final OutboxRepository outboxRepository;

  @Transactional
  public Order createOrder(String customerName, String productName, double amount) {
    var order = Order.builder()
        .customerName(customerName)
        .productName(productName)
        .amount(amount)
        .status(OrderStatus.CREATED)
        .createdAt(LocalDateTime.now())
        .build();

    var savedOrder = orderRepository.save(order);

    var outboxEvent = OutboxEvent.builder()
        .aggregateType("Order")
        .aggregateId(String.valueOf(savedOrder.getId()))
        .eventType("ORDER_CREATED")
        .payload(String.format("{\"orderId\":%d,\"amount\":%.2f}", savedOrder.getId(), amount))
        .status(EventStatus.PENDING)
        .createdAt(LocalDateTime.now())
        .build();

    outboxRepository.save(outboxEvent);
    return savedOrder;
  }
}

Background Polling Publisher (OutboxPublisher)

A scheduled background process polls PENDING outbox events, publishes them to the message broker, and marks their status as PROCESSED.

@Component
@RequiredArgsConstructor
public class OutboxPublisher {

  private final OutboxRepository outboxRepository;
  private final MessageBroker messageBroker;

  @Scheduled(fixedDelay = 5000)
  @Transactional
  public void processOutboxEvents() {
    List<OutboxEvent> pendingEvents = outboxRepository.findByStatus(EventStatus.PENDING);
    for (OutboxEvent event : pendingEvents) {
      messageBroker.publish("order-events", event.getPayload());
      event.setStatus(EventStatus.PROCESSED);
      event.setProcessedAt(LocalDateTime.now());
      outboxRepository.save(event);
    }
  }
}

Class Diagram

classDiagram
    class Order {
        +Long id
        +String customerName
        +String productName
        +double amount
        +OrderStatus status
    }
    class OutboxEvent {
        +Long id
        +String aggregateType
        +String aggregateId
        +String eventType
        +String payload
        +EventStatus status
        +LocalDateTime createdAt
        +LocalDateTime processedAt
    }
    class OrderService {
        +createOrder(customerName, productName, amount) Order
    }
    class OutboxPublisher {
        +processOutboxEvents() List~OutboxEvent~
    }
    class MessageBroker {
        <<interface>>
        +publish(topic, payload)
    }

    OrderService ..> Order : creates
    OrderService ..> OutboxEvent : creates
    OutboxPublisher ..> OutboxEvent : polls & updates
    OutboxPublisher --> MessageBroker : dispatches

When to Use the Transactional Outbox Pattern

Use this pattern when:

  • You need to update a database and publish messages to an event broker without data loss or inconsistent dual-writes.
  • Distributed transactions (XA 2-phase commit) are not supported, perform poorly, or add unwanted complexity.
  • You are building event-driven microservices requiring at-least-once event delivery guarantees.

Real-World Applications

  • E-commerce checkout systems emitting order creation events for billing and fulfillment services.
  • Financial transaction processing services issuing audit log events alongside database updates.
  • Microservices using Change Data Capture (CDC) like Debezium for database log mining outbox patterns.

Benefits and Trade-offs

Benefits

  • No Dual-Write Inconsistency: Prevents lost messages or phantom events caused by network/broker outages.
  • At-Least-Once Delivery: Guarantees event delivery to message consumers.
  • No Distributed Transactions: Avoids expensive and fragile XA/2PC transactions across services.

Trade-Offs

  • Near Real-time Latency: Polling intervals add slight delay before events are dispatched.
  • Duplicate Message Handling: Consumers must implement idempotent processing to handle potential message redeliveries.
  • Outbox Table Cleanup: Outbox entries must be periodically archived or purged to prevent uncontrolled table growth.

References and Credits