DEV Community

Allan Roberto
Allan Roberto

Posted on

Implementing the Transactional Outbox Pattern with Quarkus

Introduction

In distributed systems, one of the hardest problems is guaranteeing data consistency when publishing events.

Imagine the following scenario:

  1. Your API saves data in the database.
  2. It publishes an event to a message broker (Kafka, RabbitMQ, etc).

What happens if:

  • The database transaction succeeds
  • But the event fails to publish?

Now your system is in an inconsistent state.

This is exactly the problem the Transactional Outbox Pattern solves.

In this article, we will explore:

  • The problem of dual writes
  • How the Outbox Pattern solves it
  • A practical implementation using Quarkus
  • A working sample project

Repository:

👉 https://github.com/allanroberto18/outbox-sample-api


The Dual Write Problem

When an application performs two operations:

1️⃣ Save data
2️⃣ Publish an event

We call this a dual write problem.

Example:

@Transactional
public void createOrder(Order order) {

    orderRepository.persist(order);

    eventPublisher.publish(
        new OrderCreatedEvent(order.getId())
    );
}
Enter fullscreen mode Exit fullscreen mode

If the event fails to publish, the database is already committed.

This leads to:

❌ Lost events
❌ Inconsistent services
❌ Broken microservice workflows


The Outbox Pattern

The Outbox Pattern solves this problem by storing events in the same database transaction as the business data.

Instead of publishing directly to a broker, we do this:

1️⃣ Save business data
2️⃣ Save event in an outbox table
3️⃣ A background worker publishes the event


Flow

  • API receives request
  • Database transaction saves:
  • Business entity
  • Outbox event
  • Worker reads events from Outbox
  • Events are published to message broker
  • Event marked as processed

This guarantees event delivery without losing consistency.

The pattern ensures events are published only if the database transaction succeeds.


Project Overview

This repository demonstrates a simple implementation using Quarkus.

👉 https://github.com/allanroberto18/outbox-sample-api

Technologies used:

  • Quarkus
  • Java
  • JPA / Hibernate
  • Transactional Outbox Pattern

Quarkus is a Java framework optimized for cloud-native applications and microservices.


Creating the Outbox Event

First we create a table responsible for storing events.

Example entity:

@Entity
@Table(name = "outbox_event")
public class OutboxEvent {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  public Long id;

  @Column(name = "event_type", nullable = false, length = 120)
  public String eventType;

  @Column(name = "payload", nullable = false, columnDefinition = "TEXT")
  public String payload;

  @Enumerated(EnumType.STRING)
  @Column(nullable = false, length = 20)
  public OutboxEventStatus status;

  @Column(nullable = false)
  public int attempts;

  @Column(name = "error_message", length = 1000)
  public String errorMessage;

  @Column(name = "last_error_at")
  public LocalDateTime lastErrorAt;

  @Column(name = "next_attempt_at")
  public LocalDateTime nextAttemptAt;

  @Column(name = "max_attempts", nullable = false)
  public int maxAttempts;

  @Column(name = "created_at", nullable = false)
  public LocalDateTime createdAt;

  @Column(name = "processed_at")
  public LocalDateTime processedAt;
}
Enter fullscreen mode Exit fullscreen mode

This table acts as a buffer between the database and the message broker.


Saving Business Data + Event

Inside the same transaction we save:

  • The entity
  • The outbox event

Example:

// from RegisterUserUseCase
@Transactional
public UserResponse execute(CreateUserRequest request) {
  if (userRepository.existsByUsername(request.username())) {
    throw new DuplicateUsernameException(request.username());
  }

  UserEntity user = new UserEntity();
  user.firstName = request.firstName();
  user.lastName = request.lastName();
  user.username = request.username();
  user.password = passwordHasher.hash(request.password());
  user.enabled = false;

  try {
    userRepository.persist(user);
    userRepository.flush();
  } catch (PersistenceException ex) {
    if (isUniqueViolation(ex)) {
      throw new DuplicateUsernameException(request.username());
    }  
    throw ex;
  }

  // save outbox
  userActivationOutboxService.publishUserActivationEmail(user);

  return UserResponse.fromEntity(user);
}

// from UserActivationOutboxService
public void publishUserActivationEmail(UserEntity user) {
  OutboxEventEntity outboxEvent = new OutboxEventEntity();
  outboxEvent.eventType = EVENT_TYPE_USER_ACTIVATION_EMAIL;
  outboxEvent.payload = toPayload(user);
  outboxEvent.maxAttempts = maxAttempts;
  outboxRepository.persist(outboxEvent);
}
Enter fullscreen mode Exit fullscreen mode

Now both operations happen atomically.

If the transaction fails:

❌ No user saved
❌ No event created


Event Publisher Worker

A background job periodically reads unprocessed events.

Example:

@ApplicationScoped
public class OutboxDispatcherService {

  @Inject
  DispatchOutboxUseCase dispatchOutboxUseCase;

  @Scheduled(every = "{outbox.dispatch.every}")
  @RunOnVirtualThread
  void scheduledDispatch() {
    dispatchPending();
  }

  public void dispatchPending() {
    dispatchOutboxUseCase.execute();
  }
}
Enter fullscreen mode Exit fullscreen mode

This worker:

1️⃣ Reads pending events
2️⃣ Publishes them
3️⃣ Marks them as processed


Benefits of the Outbox Pattern

Using this pattern provides several advantages.

Reliability

Events are not lost.

Consistency

Database and event stream stay synchronized.

Fault Tolerance

If the broker is down, events stay in the database.

Scalability

Workers can scale horizontally.


Running the Project

Clone the repository:

git clone https://github.com/allanroberto18/outbox-sample-api
Enter fullscreen mode Exit fullscreen mode

Run Quarkus in dev mode:

./mvn quarkus:dev
Enter fullscreen mode Exit fullscreen mode

Quarkus dev mode enables live coding, allowing code changes without restarting the application.


When Should You Use the Outbox Pattern?

Use it when:

  • Building microservices
  • Publishing domain events
  • Integrating with Kafka / RabbitMQ
  • Ensuring eventual consistency

Avoid it when:

  • Systems are simple
  • No asynchronous communication exists

Final Thoughts

The Transactional Outbox Pattern is one of the most important patterns for building reliable distributed systems.

It solves the dual write problem and ensures:

✔ Consistent state
✔ Reliable event publishing
✔ Resilient microservices

If you want to explore a practical implementation, check the repository:

👉 https://github.com/allanroberto18/outbox-sample-api

Top comments (0)