Eventuate Tram Sagas

The Eventuate Tram Saga framework is a saga orchestration framework for Java microservices that use JDBC/JPA. A saga is a mechanism for maintaining data consistency across multiple services in microservice architecture without using distributed transactions. A saga consists of a series of a local transactions.

There are two different ways of coordinating a saga:

  • choreography - the saga’s participants exchange events

  • orchestration - a centralized orchestrator uses request/asynchronous reply-style messaging to tell the participants what to do

Choreography works well for simple sagas but for more complex sagas, orchestration is often easier to understand.

Eventuate Tram Saga is described in more detail in my book Microservice Patterns. It is built on the Eventuate Tram framework, which enables an application to atomically update a database and publish a message without using JTA.

Learn more

To learn more:

Using Eventuate Tram Saga

There are four parts to using Eventuate Tram Saga to implement an orchestration-based saga:

  1. Specifying Maven/Gradle dependencies

  2. Writing the saga orchestrator

  3. Writing each saga participant

  4. Running the Eventuate Tram CDC service

Maven/Gradle artifacts

The latest version is:

Spring/Micronaut

eventuate tram sagas bom

Quarkus

eventuate tram sagas quarkus bom

In gradle.properties:

eventuateTramSagasVersion=LATEST_VERSION
eventuateTramSagasQuarkusVersion=LATEST_VERSION

In build.gradle, specify these Maven repositories:

repositories {
    mavenCentral()
}
Saga orchestrator dependencies

If you are writing a Saga orchestrator add this dependency to your project:

Spring:

dependency {

  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-orchestration-simple-dsl:$eventuateTramSagasVersion"

}

Micronaut:

dependency {

  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-micronaut-orchestration-simple-dsl:$eventuateTramSagasVersion"

}

Quarkus:

dependency {
  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-quarkus-orchestration-simple-dsl:$eventuateTramSagasQuarkusVersion"
}
Saga participant dependencies

If you are writing a saga participant then add this dependency:

Spring:

dependency {

  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-participant:$eventuateTramSagasVersion"

}

Micronaut:

dependency {

  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-micronaut-participant:$eventuateTramSagasVersion"

}

Quarkus:

dependency {
  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-quarkus-participant:$eventuateTramSagasQuarkusVersion"
}
Eventuate Tram dependencies

You must also include one of the Eventuate Tram or Eventuate Tram Quarkus 'implementation' artifacts:

dependencies {

  compile "io.eventuate.tram.core:eventuate-tram-<framework>-producer-jdbc:$eventuateTramVersion"
  compile "io.eventuate.tram.core:eventuate-tram-<framework>-consumer-<message-broker>:$eventuateTramVersion"

  // In-memory JDBC database and in-memory messaging for testing
  testCompile `io.eventuate.tram.core:eventuate-tram-<framework>-in-memory:$eventuateTramVersion`

  // Saga orchestrator only

  testCompile "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-testing-support:$eventuateTramSagasVersion"

}

where

  • framework is spring, micronaut or quarkus

  • message-broker is kafka, apachemq, rabbitmq or redis.

Note: micronaut and quarkus only support kafka

Eventuate BOM

eventuate platform dependencies

You can use the Eventuate BOM to avoid needing to specify the artifact versions:

dependencies {
    implementation(platform("io.eventuate.platform:eventuate-platform-dependencies:$eventuateBomVersion"))
}

You can then specify artifacts as follows:

dependencies {
  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-micronaut-participant"
}

Writing an orchestrator

The Customers and Orders (Spring) uses a saga to create an Order in the Order Service and reserve credit in the Customer Service. The CreateOrderSaga consists of the following three steps:

  1. The CreateOrderSaga is instantiated after the Order is created. Consequently, the first step is simply a compensating transaction, which is executed in the credit cannot be reserved to reject the order.

  2. Requests the CustomerService to reserve credit for the order. If the reservation is success, the next step is executed. Otherwise, the compensating transactions are executed to roll back the saga.

  3. Approves the order, if the credit is reserved.

Here are the versions for Micronaut and Quarkus:

Writing the saga orchestrator class

Here is part of the definition of CreateOrderSaga.

public class CreateOrderSaga implements SimpleSaga<CreateOrderSagaData> {

  private SagaDefinition<CreateOrderSagaData> sagaDefinition =
          step()
            .withCompensation(this::reject)
          .step()
            .invokeParticipant(this::reserveCredit)
          .step()
            .invokeParticipant(this::approve)
          .build();


  @Override
  public SagaDefinition<CreateOrderSagaData> getSagaDefinition() {
    return this.sagaDefinition;
  }


  private CommandWithDestination reserveCredit(CreateOrderSagaData data) {
    long orderId = data.getOrderId();
    Long customerId = data.getOrderDetails().getCustomerId();
    Money orderTotal = data.getOrderDetails().getOrderTotal();
    return send(new ReserveCreditCommand(customerId, orderId, orderTotal))
            .to("customerService")
            .build();

...

The reserveCredit() creates a message to send to the Customer Service to reserve credit.

Configuring the application context for a saga orchestrator
Spring
@Configuration
...
@Import({SagaOrchestratorConfiguration.class,
...
TramMessageProducerJdbcConfiguration.class,
EventuateTramKafkaMessageConsumerConfiguration.class
})
public class OrderConfiguration {
...

Instead of explicitly @Import-ing configuration classes you can rely on the auto-configuration provided by eventuate-tram-sagas-spring-orchestration-simple-dsl-starter:

dependencies {
  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-orchestration-simple-dsl-starter:$eventuateTramSagasVersion"
}
Micronaut

Just add following dependencies

dependencies {
  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-micronaut-orchestration:$eventuateTramSagasVersion"
  compile "io.eventuate.tram.core:eventuate-tram-micronaut-producer-jdbc:$eventuateTramVersion"
  compile "io.eventuate.tram.core:eventuate-tram-micronaut-consumer-kafka:$eventuateTramVersion"
}
Quarkus

Just add following dependencies

dependencies {
  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-quarkus-orchestration:$eventuateTramSagasVersion"
  compile "io.eventuate.tram.core:eventuate-tram-quarkus-producer-jdbc:$eventuateTramVersion"
  compile "io.eventuate.tram.core:eventuate-tram-quarkus-consumer-kafka:$eventuateTramVersion"
}
Creating a saga orchestrator

The OrderService creates the saga using SagaInstanceFactory:

Spring
public class OrderService {

  @Autowired
  private SagaInstanceFactory sagaInstanceFactory;

  @Autowired
  private OrderRepository orderRepository;

  @Transactional
  public Order createOrder(OrderDetails orderDetails) {
    ResultWithEvents<Order> oe = Order.createOrder(orderDetails);
    Order order = oe.result;
    orderRepository.save(order);
    CreateOrderSagaData data = new CreateOrderSagaData(order.getId(), orderDetails);

    sagaInstanceFactory.create(createOrderSaga, data);

    return order;
  }

}
Micronaut/Quarkus
public class OrderService {

  @Inject
  private SagaInstanceFactory sagaInstanceFactory;

  @PersistenceContext
  private EntityManager entityManager;

  @Transactional
  public Order createOrder(OrderDetails orderDetails) {
    CreateOrderSagaData data = new CreateOrderSagaData(orderDetails);
    sagaInstanceFactory.create(createOrderSaga, data);
    return entityManager.find(Order.class, data.getOrderId());
  }

}

Writing a saga participant

Here is the CustomerCommandHandler, which handles the command to reserve credit:

Spring
public class CustomerCommandHandler {

  @Autowired
  private CustomerRepository customerRepository;

  public CommandHandlers commandHandlerDefinitions() {
    return SagaCommandHandlersBuilder
            .fromChannel("customerService")
            .onMessage(ReserveCreditCommand.class, this::reserveCredit)
            .build();
  }

  public Message reserveCredit(CommandMessage<ReserveCreditCommand> cm) {
     ...
  }
  ...
Micronaut/Quarkus
public class CustomerCommandHandler {

  @PersistenceContext
  private EntityManager entityManager;

  public CommandHandlers commandHandlerDefinitions() {
    return SagaCommandHandlersBuilder
            .fromChannel("customerService")
            .onMessage(ReserveCreditCommand.class, this::reserveCredit)
            .build();
  }

  public Message reserveCredit(CommandMessage<ReserveCreditCommand> cm) {
     ...
  }

}
  ...

Configuring the application context for a saga participant

Spring
@Configuration
@Import({
  SagaParticipantConfiguration.class,
  ...
  TramMessageProducerJdbcConfiguration.class,
  EventuateTramKafkaMessageConsumerConfiguration.class
})
...
@EnableAutoConfiguration
public class CustomerConfiguration {
  ....

Instead of explicitly @Import-ing SagaParticipantConfiguration you can rely on the auto-configuration provided by eventuate-tram-sagas-spring-participant-starter:

dependencies {
  compile "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-participant-starter:$eventuateTramSagasVersion"
}
Micronaut

TBD

Running the CDC service

In addition to a database and message broker, you will need to run the Eventuate Tram CDC service. It reads messages and events inserted into the database and publishes them to Apache Kafka. It is written using Spring Boot. The easiest way to run this service during development is to use Docker Compose. The Eventuate Tram Code Basic examples project has an example docker-compose.yml file.