Open-source project
eventuate-tram/eventuate-tram-examples-customers-and-orders avatar
eventuate-tram/eventuate-tram-examples-customers-and-orders

Eventuate Tram customers and orders: a choreography saga and a CQRS view in three services

An example of Choreography-based sagas in Spring Boot/JPA microservices

720 stars318 forksJavaNOASSERTION

At a glance

What is it?
The Eventuate Tram customers and orders example is a three-service Spring Boot and JPA application that shows a choreography-based saga enforcing a customer's credit limit across an Order Service and a Customer Service, plus an Order History Service that maintains a CQRS view in MongoDB by subscribing to their domain events. Events reach the bus through the transactional outbox pattern, with the MESSAGE table written inside the same ACID transaction as the entity.
Who is it for?
The Eventuate Tram customers and orders example is worth running if you are working with sagas or CQRS and want a working reference rather than a diagram, because it puts the three services, the event flow, the outbox table and the replicated view in one small codebase with a build script that starts the whole stack.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Java, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The create-order saga is three steps and no coordinator

The saga at the centre of this example is the create-order flow, and it is a choreography-based one, which means the services coordinate through domain events rather than through a workflow engine.

The steps are short. The Order Service creates an Order in a PENDING state and publishes an OrderCreated event. The Customer Service receives that event, attempts to reserve credit for the order, and publishes either a Credit Reserved event or a CreditLimitExceeded event. The Order Service receives whichever event came back and changes the state of the order to either APPROVED or REJECTED.

There is no orchestrator in that list, and that is the point of the choreography style. Each step updates its own local database and publishes a domain event, and an event handler performs the next local transaction. The saga pattern itself, as the README describes it, is a mechanism for maintaining data consistency in a microservice architecture, and it is a sequence of transactions each of which is local to a service.

The scenario is a credit limit, which is a good choice for a reference implementation because the failure case is real and observable. A customer with a credit limit of some amount places an order for more, and the customer service declines to reserve the credit, and the order ends up REJECTED rather than sitting in an inconsistent state. The Order Service never talks to the Customer Service directly for this decision. It creates the order, publishes, and waits to be told.

The project description also links the orchestration-based saga example as the alternative to look at, and points to the microservices.io saga pattern page and a microservices patterns book for the reasoning behind why sagas are needed at all.

Events leave the database through a MESSAGE table and CDC

The mechanism that makes the choreography work is transactional publish and subscribe, provided by the Eventuate Tram framework.

The flow is three steps, and the first one is the interesting design decision. Eventuate Tram inserts events into the MESSAGE table as part of the ACID transaction that updates the JPA entity. That is the transactional outbox pattern, written as a second row in the same database rather than as a message broker publish inside a transaction that might roll back.

The second step is where the framework earns its keep. The Eventuate Tram CDC service tracks inserts into the MESSAGE table using the MySQL binlog, or the Postgres WAL, and publishes messages to Apache Kafka. No application code polls the table and no application code publishes to the broker, which means there is no window where the database commit succeeds and the message is lost.

The third step is the consumer side. A service subscribes to the events, updates its database, and possibly publishes more events. That last clause is what makes a saga possible at all, since the credit reservation step in the customer service is itself a local transaction that publishes an event the order service then consumes.

The database dependency is worth being explicit about, because it determines which infrastructure you need. The README names MySQL binlog and Postgres WAL, and the repository layout shows the build and test scripts that prove it, with separate scripts for MySQL binlog, Postgres WAL, MSSQL polling and MySQL Maven, plus a general build-and-test-everything script. So the pattern is not MySQL-only, but the CDC approach differs per database and the MSSQL path is described as polling rather than log-based.

Three services, two databases, and a read model that lives somewhere else

The application is three services, and the split is by data ownership rather than by feature.

The Order Service manages orders and persists the Order JPA entity in a MySQL, MSSQL or Postgres database. The Customer Service manages customers, persists the Customer entity in the same family of databases, and publishes Customer domain events that the Order Service consumes. Both expose REST APIs for managing their own aggregate.

The Order History Service is different in kind. It implements a REST API for querying a customer's order history, and it does not own the data. It subscribes to events published by the Order Service and the Customer Service and updates a MongoDB-based CQRS view, where each customer is stored as a MongoDB document containing information about the customer and their orders.

That is the CQRS pattern in its most direct form, as the README describes it: it maintains a queryable replica of the data by subscribing to domain events published by the services that own the data. The point is that a query which needs data from two services does not have to call both of them. The history service answers it from its own store.

The repository layout shows how the services are actually organised, with customer-service/, order-service/ and order-history-service/ directories, each with a matching canvas document and image describing its own context. There is also an api-gateway-service/ directory, which is what the usage instructions refer to when they describe using the API Gateway, and an order-history-text-search-service/ directory with its own API and web directory, which suggests a variant of the history service that adds text search.

There are also deployment directories, an aws-fargate-terraform/ directory, a skaffold.yaml, a docker-compose-env-files/ directory, and a set of helper scripts for the Kafka side, including kafka-topics.sh, kafka-consumer-groups.sh and set-consumer-group-offset.sh, plus per-database client scripts.

One Gradle command starts the whole stack

The build-and-run story is one command, and there are two variants depending on which database you want to use.

bash
 ./gradlew :end-to-end-tests:runApplicationMySQL
bash
 ./gradlew :end-to-end-tests:runApplicationPostgres

The command starts the containers on unique ports and prints out the home page URL, which is how you find the Swagger UIs and the API Gateway address afterwards. The port handling is why the curl examples in the README tell you to replace port 8080 with the correct port for the API Gateway.

Interacting with the application has two paths. You can use the Swagger UIs, which the README says you can use to create customers and orders, or you can use curl against the API Gateway. The curl example creates a customer with a credit limit and a name:

bash
$ curl -X POST --header "Content-Type: application/json" -d '{
  "creditLimit": {
    "amount": 5
  },
  "name": "Jane Doe"
}' http://localhost:8080/customers

The credit limit is a nested object with an amount, which is a hint about how the money values are modelled across the services, since a credit limit is a monetary amount and this is a pattern rather than a bare number.

The build system is Gradle, with a build.gradle, settings.gradle, a buildSrc/ directory and a gradle.properties. There is also an mvnw and mvnw.cmd in the root, which is a leftover from a Maven-based predecessor or a coexistence for a module, and a mise.toml for toolchain management. A .tours/ directory holds the guided code tour that the README refers to when it mentions a tour inside a GitHub Codespace or Visual Studio Code, and there is a .circleci/ directory for the CI configuration.

The scripts at the root give you the full matrix. _build-and-test-all.sh is the general entry point, and the individual ones let you test one database mechanism at a time, which is the sensible way to find out whether the CDC approach works in your environment.

What the repository does not show you

A reference example is useful precisely because it is small, and that means several things you will eventually need are simply not in it.

There is no error handling demonstration. The saga in this example has two outcomes, a credit reserved and a credit limit exceeded, and both are shown as happy paths through the state machine. What happens when the customer service is down, when the message arrives twice, or when the order service crashes between creating the PENDING order and receiving the response is not documented here. A choreography saga has no coordinator to retry on its behalf, so those cases have to be handled explicitly by the application, and this example does not show you how.

There is no idempotency story either. The order history service subscribes to events and updates a MongoDB view, and the README does not say what happens when the same event is delivered twice or when two events arrive out of order. This is the standard hard part of the transactional outbox approach, and the CDC delivery guarantees that the events are in the log do not by themselves make the consumer idempotent.

There is no authentication or authorisation in the picture either. The services expose REST APIs and the API Gateway routes to them, and the README does not mention how a caller is identified. The saga decides on credit, which implies a customer identity, and where that identity comes from is not described.

Finally, the project is an example in a collaboration platform rather than a product. The README opens by saying this project is part of Eventuate, described as a microservices collaboration platform, and the whole example is built on the Eventuate Tram framework for transactional publish and subscribe. The patterns are portable, but the code is a demonstration of one framework's way of doing them, which is worth remembering when you copy the shape of the solution into a codebase that does not use it.

Editorial conclusion

The Eventuate Tram customers and orders example is worth running if you are working with sagas or CQRS and want a working reference rather than a diagram, because it puts the three services, the event flow, the outbox table and the replicated view in one small codebase with a build script that starts the whole stack. It is the wrong starting point if you are not using Eventuate Tram, since the framework is what makes the messaging work, and the same two patterns are demonstrated in projects that do not depend on it. Before you build on the pattern, verify three things yourself: that the saga you copy really needs choreography, because the project points at a separate orchestration-based example for the alternative, that the CDC mechanism you rely on matches your database, since the README describes MySQL binlog and Postgres WAL tracking and the repository contains separate build and test scripts for MySQL binlog, Postgres WAL, MSSQL polling and MySQL Maven, and that the CQRS view is eventually consistent rather than authoritative, since the history service maintains a replica built from events and will lag the write model by design.

Frequently asked questions

What does the Eventuate Tram customers and orders example demonstrate?

It demonstrates two patterns: sagas, implemented as transactions that span services, and CQRS, implemented as queries that retrieve data from multiple services. It consists of three services, an Order Service, a Customer Service and an Order History Service, built with Spring Boot, JPA and the Eventuate Tram framework.

How does the choreography-based saga in this example work?

The Order Service creates an Order in a PENDING state and publishes an OrderCreated event, the Customer Service receives it and reserves credit, publishing either a Credit Reserved or a CreditLimitExceeded event, and the Order Service then sets the order to APPROVED or REJECTED. Each step is a local transaction and the services coordinate only through domain events.

How are events published with Eventuate Tram?

Eventuate Tram inserts events into a MESSAGE table as part of the ACID transaction that updates the JPA entity, its CDC service tracks those inserts using the MySQL binlog or the Postgres WAL, and publishes the messages to Apache Kafka, where subscribing services update their own databases and may publish further events.

How do I run the Eventuate Tram example?

Run ./gradlew :end-to-end-tests:runApplicationMySQL or the Postgres variant. The command starts the containers on unique ports and prints the home page URL, where you find the Swagger UIs and the API Gateway, and you can then create customers and orders with curl through the gateway.

Where does the Order History Service store its data?

It maintains a CQRS view in MongoDB, subscribing to domain events published by the Order Service and the Customer Service, and stores each customer as a MongoDB document containing information about that customer and their orders. It exposes a REST API for querying a customer's order history.

Official sources

  1. eventuate-tram/eventuate-tram-examples-customers-and-orders on GitHub
  2. Issues
  3. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/eventuate-tram-eventuate-tram-examples-customers-and-orders.svg)](https://hysenlabs.com/projects/eventuate-tram-eventuate-tram-examples-customers-and-orders)

Was this useful?