Enterprise Java

Transactional Messaging with Eventuate Tram

Event-driven microservices often rely on asynchronous communication between services. However, when business logic involves modifying a database and sending an event, we face a challenge: ensuring both operations happen atomically. This is where transactional messaging and tools like Eventuate Tram come in. Let us delve into understanding how Eventuate Tram enables transactional messaging in microservice architectures.

1. What is Transactional Messaging?

Transactional messaging is a mechanism that ensures changes made to a database and the messages published to external systems (like message queues) occur as part of a single atomic transaction, preventing inconsistencies where the database update succeeds but the corresponding message is lost, or vice versa. This is especially critical in distributed microservices environments, where services rely on event-based communication for coordination. Without transactional messaging, events may be published without the associated database changes being committed, leading to potential data loss or duplication during system crashes or restarts, and resulting in services becoming inconsistent and more difficult to debug.

1.1 Problems with Traditional Queues

Traditional message brokers like RabbitMQ or Kafka don’t support atomic operations across databases and message queues. This creates the dual-write problem, where you must write to the DB and then send a message to the queue separately — and either action could fail independently. The Transactional Outbox Pattern solves this by introducing an intermediate persistent storage (an outbox table), ensuring atomicity.

1.2 What is the Transactional Outbox Pattern?

The Transactional Outbox Pattern is a design approach used to reliably publish events within the same transaction as database changes. It works by writing both the domain data (e.g., an order creation) and a corresponding event record (e.g., OrderCreatedEvent) to an outbox table as part of a single database transaction. A background process—typically a Change Data Capture (CDC) service—then monitors the outbox table and asynchronously sends the events to a message broker. This pattern ensures that the domain change and the event record are either both committed or both rolled back, thereby preserving consistency and eliminating dual-write problems in distributed systems.

1.2.1 Common Components

In a typical implementation of the Transactional Outbox Pattern, several key components work together to ensure reliable and consistent event publication. These components are responsible for capturing, storing, and delivering events as part of a fault-tolerant messaging workflow:

  • Outbox Table: A dedicated table for storing event messages before dispatch.
  • CDC Service: Monitors and dispatches outbox events (e.g., Eventuate CDC service).
  • Message Broker: Such as Kafka or RabbitMQ, where the events are finally delivered.

2. What is Eventuate Tram?

Eventuate Tram is a Java-based framework developed by Chris Richardson and the Eventuate team. It supports transactional outbox pattern implementation for microservices that need to publish domain events reliably. It allows applications to write to their relational databases and publish messages as part of a single transaction. Later, a CDC (Change Data Capture) service reads these events from an outbox table and sends them to the message broker (e.g., Kafka or RabbitMQ).

2.1 Key Features

Eventuate Tram offers a rich set of features designed to simplify the development of reliable, event-driven microservices. These features ensure strong consistency, seamless integration, and robust message delivery across distributed systems:

  • Implements the Transactional Outbox Pattern using CDC
  • Supports message brokers like Apache Kafka and RabbitMQ
  • Integrates seamlessly with Spring Boot
  • Provides automatic message correlation, retries, and durable delivery

2.2 Benefits

By adopting Eventuate Tram, developers can build systems that are more reliable, maintainable, and aligned with event-driven principles. The following benefits illustrate its value in real-world microservice architectures:

  • Atomicity: Guarantees that events are only published if the transaction commits
  • Resilience: Minimizes the risk of lost events in case of service crashes
  • Developer Productivity: Simplifies implementation of event-driven architectures

3. Code Example

This section demonstrates a complete Spring Boot application that uses Eventuate Tram to implement transactional messaging using the Transactional Outbox Pattern. When a new order is created, the OrderService persists the data in the database and publishes an OrderCreatedEvent. The event is reliably delivered via Kafka using the outbox table and CDC (Change Data Capture) service, ensuring data consistency between services.

3.1 Adding Dependencies (build.gradle)

Start by setting up the required dependencies in your build.gradle to include Spring Boot, Eventuate Tram, and Kafka.

plugins {
  id 'org.springframework.boot' version '3.2.0'
  id 'io.spring.dependency-management' version '1.1.0'
  id 'java'
}

group = 'com.example'
version = '1.0.0'
sourceCompatibility = '17'

repositories {
  mavenCentral()
  maven { url "https://maven.eventuate.io/release" }
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  implementation 'io.eventuate.tram.core:eventuate-tram-spring-boot-starter-jdbc'
  implementation 'io.eventuate.tram.core:eventuate-tram-spring-boot-starter-kafka'
  runtimeOnly 'org.postgresql:postgresql'
}

3.2 Order.java

The Order class is a simple JPA entity annotated with @Entity, representing an order in a transactional system. It contains three fields: id, product, and status. The id field is annotated with @Id and @GeneratedValue, indicating that it serves as the primary key and its value will be automatically generated. The product field holds the name or type of product ordered, while the status field represents the current state of the order (e.g., “PENDING”, “CREATED”, or “APPROVED”). Standard getter and setter methods are provided for each field, allowing the entity to be easily persisted and manipulated in a Spring Boot application, especially when used with Eventuate Tram for transactional messaging in distributed microservices.

package com.example.app;

import jakarta.persistence.*;

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

  private String product;
  private String status;

  public Long getId() { return id; }
  public String getProduct() { return product; }
  public void setProduct(String product) { this.product = product; }
  public String getStatus() { return status; }
  public void setStatus(String status) { this.status = status; }
}

3.3 OrderRepository.java

The OrderRepository interface extends JpaRepository<Order, Long>, providing built-in CRUD operations and JPA-based data access functionality for the Order entity. By extending JpaRepository, this repository inherits methods such as save(), findById(), findAll(), and deleteById(), eliminating the need to write boilerplate code. This interface acts as a bridge between the application and the underlying database, allowing seamless integration with Spring Data JPA and enabling the Order entity to participate in transactional operations, including those managed by Eventuate Tram for consistent messaging in a microservices architecture.

package com.example.app;

import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {
}

3.4 OrderCreatedEvent.java

The OrderCreatedEvent class implements the DomainEvent interface and represents an event that is published when a new order is created. It contains a single field, product, which holds the name of the ordered product. The class provides a default constructor for deserialization and a parameterized constructor to initialize the event with a specific product value. Additionally, a getter method is provided to access the product information. This event is typically used in transactional messaging scenarios facilitated by Eventuate Tram, allowing services to react to domain events in a decoupled and reliable manner within a microservices-based system.

package com.example.app;

import io.eventuate.tram.events.common.DomainEvent;

public class OrderCreatedEvent implements DomainEvent {
  private String product;

  public OrderCreatedEvent() {}
  public OrderCreatedEvent(String product) { this.product = product; }
  public String getProduct() { return product; }
}

3.5 OrderService.java

The OrderService class is a Spring @Service component that handles the business logic for creating orders. It uses constructor injection to receive instances of OrderRepository and DomainEventPublisher. The core method, createOrder, is marked as @Transactional to ensure that both the database operation and the event publication occur atomically. Inside the method, a new Order object is created with the given product, its status is set to "CREATED", and it is persisted using the repository. After the order is saved, an OrderCreatedEvent is published using Eventuate Tram’s DomainEventPublisher, ensuring that the event is only dispatched if the transaction commits successfully. This setup supports reliable and consistent transactional messaging across microservices.

package com.example.app;

import io.eventuate.tram.events.publisher.DomainEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collections;

@Service
public class OrderService {

  private final OrderRepository repository;
  private final DomainEventPublisher eventPublisher;

  public OrderService(OrderRepository repository, DomainEventPublisher eventPublisher) {
    this.repository = repository;
    this.eventPublisher = eventPublisher;
  }

  @Transactional
  public Order createOrder(String product) {
    Order order = new Order();
    order.setProduct(product);
    order.setStatus("CREATED");
    repository.save(order);

    eventPublisher.publish(Order.class, order.getId(),
      Collections.singletonList(new OrderCreatedEvent(product)));

    return order;
  }
}

3.6 OrderController.java

The OrderController class is a Spring @RestController that handles HTTP requests related to order creation. It is mapped to the /orders endpoint using @RequestMapping. The controller relies on constructor injection to access the OrderService. It defines a @PostMapping method named create, which accepts a JSON payload containing a product key. The method extracts the product name from the request body and delegates the order creation logic to the OrderService. Once the order is created, it returns a ResponseEntity with the newly created Order object. This controller enables RESTful interaction with the service and triggers transactional messaging via Eventuate Tram by indirectly publishing domain events through the service layer.

package com.example.app;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/orders")
public class OrderController {

  private final OrderService service;

  public OrderController(OrderService service) {
    this.service = service;
  }

  @PostMapping
  public ResponseEntity<Order> create(@RequestBody Map<String, String> payload) {
    String product = payload.get("product");
    return ResponseEntity.ok(service.createOrder(product));
  }
}

3.7 OrderEventConsumer.java

The OrderEventConsumer class is a Spring @Component that defines how domain events are handled after being published. It uses Eventuate Tram’s DomainEventHandlers builder to register an event handler for the OrderCreatedEvent associated with the aggregate type "com.example.app.Order". The domainEventHandlers() method returns a DomainEventHandlers instance, specifying that the handleOrderCreatedEvent method should be invoked when an OrderCreatedEvent is received. The event handler method, handleOrderCreatedEvent, prints out a simple log message displaying the product name from the event. This setup ensures that the consumer service can react to events reliably and asynchronously as part of a transactional messaging flow enabled by Eventuate Tram.

package com.example.app;

import io.eventuate.tram.events.subscriber.DomainEventEnvelope;
import io.eventuate.tram.events.subscriber.DomainEventHandler;
import io.eventuate.tram.events.subscriber.DomainEventHandlers;
import org.springframework.stereotype.Component;

@Component
public class OrderEventConsumer {

  public DomainEventHandlers domainEventHandlers() {
    return DomainEventHandlers
      .builder()
      .aggregateType("com.example.app.Order")
      .onEvent(OrderCreatedEvent.class, this::handleOrderCreatedEvent)
      .build();
  }

  private void handleOrderCreatedEvent(DomainEventEnvelope<OrderCreatedEvent> event) {
    System.out.println("Consumed OrderCreatedEvent for product: " + event.getEvent().getProduct());
  }
}

3.8 EventConfiguration.java

The EventConfiguration class is a Spring @Configuration that sets up the infrastructure required for consuming domain events using Eventuate Tram. It defines a @Bean method that creates a DomainEventDispatcher by invoking the make method on the provided DomainEventDispatcherFactory. This method takes a dispatcher name (in this case, "orderEventDispatcher") and the event handlers defined in OrderEventConsumer. The resulting dispatcher is responsible for routing incoming domain events to their respective handler methods. This configuration enables automatic wiring and activation of the event handling mechanism, ensuring seamless transactional messaging and eventual consistency in a microservices environment.

package com.example.app;

import io.eventuate.tram.events.subscriber.DomainEventDispatcher;
import io.eventuate.tram.events.subscriber.DomainEventDispatcherFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class EventConfiguration {

  @Bean
  public DomainEventDispatcher domainEventDispatcher(OrderEventConsumer consumer,
                                                     DomainEventDispatcherFactory factory) {
    return factory.make("orderEventDispatcher", consumer.domainEventHandlers());
  }
}

3.9 EventuateApp.java

The EventuateApp class serves as the entry point for the Spring Boot application and is annotated with @SpringBootApplication, which enables component scanning, auto-configuration, and configuration property support. The main method calls SpringApplication.run, which bootstraps the application context and starts the embedded web server. This class brings together all components, including the domain model, service layer, event publisher, consumer, and Eventuate Tram infrastructure, enabling transactional messaging and reliable event-driven communication across microservices.

package com.example.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class EventuateApp {
  public static void main(String[] args) {
    SpringApplication.run(EventuateApp.class, args);
  }
}

3.10 application.yml

Basic configuration for database, JPA, and Kafka integration.

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/eventuate
    username: postgres
    password: password
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

eventuate:
  database:
    schema: public

kafka:
  bootstrap-servers: localhost:9092

This configuration snippet sets up the essential Spring Boot properties for integrating a microservice with PostgreSQL, Kafka, and Eventuate Tram. Under the spring.datasource section, it defines the JDBC URL, username, and password required to connect to the PostgreSQL database named eventuate. The spring.jpa block configures JPA to automatically update the schema on startup and enables SQL query logging. The eventuate.database.schema is set to public, indicating the default schema used by Eventuate Tram for its internal tables such as the outbox. Lastly, the kafka.bootstrap-servers property specifies the address of the local Kafka broker, enabling the application to publish and consume domain events reliably as part of the transactional outbox pattern.

3.11 docker-compose.yml

This file sets up Zookeeper, Kafka, PostgreSQL, and the Eventuate CDC service for local development and testing. It is typically defined in a Docker Compose file to simplify containerized setup and orchestration.

version: '3'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.0.1
    ports:
      - "2181:2181"
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181

  kafka:
    image: confluentinc/cp-kafka:7.0.1
    ports:
      - "9092:9092"
    depends_on:
      - zookeeper
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

  postgres:
    image: postgres:14
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: eventuate
    ports:
      - "5432:5432"

  cdc-service:
    image: eventuateio/eventuate-cdc-service:0.14.0.RELEASE
    environment:
      EVENTUATE_CDC_READER_NAME: EventuateTram
      EVENTUATE_CDC_TYPE: eventuate-tram
      EVENTUATE_CDC_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
      EVENTUATE_CDC_DB_DRIVER: org.postgresql.Driver
      EVENTUATE_CDC_DB_URL: jdbc:postgresql://postgres:5432/eventuate
      EVENTUATE_CDC_DB_USER_NAME: postgres
      EVENTUATE_CDC_DB_PASSWORD: password
      EVENTUATE_OUTBOX_ID: 1
    depends_on:
      - kafka
      - postgres

With this setup, your microservices architecture ensures reliable, exactly-once event publishing using the Transactional Outbox Pattern and decoupled communication via Kafka, orchestrated through Eventuate Tram.

3.12 Code Run and Demo

First, start the infrastructure services using Docker Compose by running the command: docker-compose up. Once the services are up, run the Spring Boot application using the command: ./gradlew bootRun. After the application starts, you can trigger a request to create an order by sending a POST request to http://localhost:8080/orders with the payload {"product":"Xbox Series X"} using curl:

curl -X POST http://localhost:8080/orders \
  -H "Content-Type: application/json" \
  -d '{"product":"Xbox Series X"}'

If everything is set up correctly, you will see the following output in the console:

Consumed OrderCreatedEvent for product: Xbox Series X

4. Conclusion

Eventuate Tram bridges the gap between reliable messaging and transactional consistency in microservices. By implementing the transactional outbox pattern and integrating CDC, it ensures that database updates and event dispatches occur reliably and atomically. This approach prevents data loss, improves fault tolerance, and simplifies event-driven architecture in Spring Boot applications.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button