Implementing the Outbox Pattern with Kafka and Spring Boot

Vitali RVitali R
3 min read

The Outbox Pattern is a crucial architectural solution for ensuring reliable event publication in distributed systems. It enables decoupling between transactional data changes and event-driven systems like Kafka, ensuring consistency without requiring distributed transactions.

This article demonstrates how to implement the Outbox Pattern using:

  • Java 21
  • Spring Boot 3.x
  • Apache Kafka
  • PostgreSQL (as the database)
  • Maven
  • JUnit 5

💡 What Is the Outbox Pattern?

Instead of publishing an event directly to Kafka within the same transaction where the database is modified, the event is written to a local "outbox" table in the same transaction. A background process (poller or Debezium CDC) then reads the outbox table and publishes the event to Kafka.


📦 Project Setup

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>outbox-kafka</artifactId>
    <version>1.0.0</version>
    <properties>
        <java.version>21</java.version>
        <spring.boot.version>3.2.0</spring.boot.version>
    </properties>
    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- Kafka -->
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>

        <!-- PostgreSQL -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

🗃️ Database Entities

Outbox Entity

@Entity
@Table(name = "outbox")
public class OutboxEvent {
    @Id
    private UUID id;

    private String aggregateType;
    private String aggregateId;
    private String eventType;

    @Lob
    private String payload;

    private Instant createdAt;
}

Business Entity

@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    private String status;
}

⚙️ Service and Outbox Write

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final OutboxRepository outboxRepository;

    public OrderService(OrderRepository orderRepository, OutboxRepository outboxRepository) {
        this.orderRepository = orderRepository;
        this.outboxRepository = outboxRepository;
    }

    @Transactional
    public Order createOrder() {
        Order order = new Order();
        order.setStatus("CREATED");
        orderRepository.save(order);

        OutboxEvent event = new OutboxEvent();
        event.setId(UUID.randomUUID());
        event.setAggregateType("Order");
        event.setAggregateId(order.getId().toString());
        event.setEventType("OrderCreated");
        event.setPayload("{"orderId": " + order.getId() + "}");
        event.setCreatedAt(Instant.now());
        outboxRepository.save(event);

        return order;
    }
}

🌀 Polling Publisher

@Component
public class OutboxPublisher {
    private final OutboxRepository outboxRepository;
    private final KafkaTemplate<String, String> kafkaTemplate;

    @Scheduled(fixedRate = 5000)
    public void publishOutboxEvents() {
        List<OutboxEvent> events = outboxRepository.findTop10ByOrderByCreatedAtAsc();

        for (OutboxEvent event : events) {
            kafkaTemplate.send("order-events", event.getAggregateId(), event.getPayload());
            outboxRepository.delete(event); // Delete after publishing
        }
    }
}

🧪 Unit and Integration Tests

OrderServiceTest.java

@ExtendWith(SpringExtension.class)
@SpringBootTest
class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OutboxRepository outboxRepository;

    @Test
    void testOrderCreationWritesOutboxEvent() {
        Order order = orderService.createOrder();
        List<OutboxEvent> events = outboxRepository.findByAggregateId(order.getId().toString());

        assertFalse(events.isEmpty());
        assertEquals("OrderCreated", events.get(0).getEventType());
    }
}

OutboxPublisherTest.java

@SpringBootTest
class OutboxPublisherTest {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @Autowired
    private OutboxPublisher outboxPublisher;

    @Autowired
    private OutboxRepository outboxRepository;

    @Test
    void testPublishingToKafka() {
        OutboxEvent event = new OutboxEvent();
        event.setId(UUID.randomUUID());
        event.setAggregateType("Order");
        event.setAggregateId("123");
        event.setEventType("OrderCreated");
        event.setPayload("{"orderId": 123}");
        event.setCreatedAt(Instant.now());

        outboxRepository.save(event);
        outboxPublisher.publishOutboxEvents();

        List<OutboxEvent> remaining = outboxRepository.findByAggregateId("123");
        assertTrue(remaining.isEmpty());
    }
}

🧵 Conclusion

The Outbox Pattern is an excellent solution when you want to guarantee atomicity between database operations and message publishing in a microservices environment.

By combining Spring Boot, Kafka, and a relational database, we’ve implemented a resilient and production-grade foundation for eventual consistency.


📚 Further Reading


0
Subscribe to my newsletter

Read articles from Vitali R directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vitali R
Vitali R