/eventuate-tram-examples-java-spring-todo-list

A todo list application implemented using Spring Boot-based microservices and the Eventuate Tram framework

Primary LanguageJavaOtherNOASSERTION

Todo List example application

This example demonstrates how to develop microservices with Spring Boot, JPA, Apache Kafka, ElasticSearch and the Eventuate Tram framework.

The problem: atomically updating data and publishing events/messages

It's challenging to atomically update a data (e.g. a Domain-Driven design aggregate) and publish a message, such as a domain event. The traditional approach of using 2PC/JTA isn't a good fit for modern applications.

The Eventuate™ Tram framework implements an alternative mechanism based on the Application Events pattern. When an application creates or updates data, as part of that ACID transaction, it inserts an event into an EVENTS or MESSAGES table. A separate CDC process publishes those events to a message broker, such as Apache Kafka.

About the Todo list application

The Todo List application, which lets users maintain a todo list, is the hello world application for the Eventuate™ Tram framework. It shows how use Eventuate Tram to

  • reliably publish domain events as part of a database transaction that updates an aggregate.
  • consume domain events to update a CQRS view view

When a user creates or updates a todo, the application publishes a domain event. An event handler, subscribes to those events and updates an ElasticSearch-based CQRS view.

Todo list architecture

The Todo List application is built using

  • Java
  • JPA
  • Eventuate Tram
  • Spring Boot
  • MySQL
  • ElasticSearch
  • Apache Kafka

The following diagram shows the application's architecture.

TODO architecture

The application consists of two services:

  • Todo Service - implements the REST endpoints for creating, updating and deleting todos. The service persists the Todo JPA entity in MySQL. Using Eventuate Tram, it publishes Todo domain events that are consumed by the Todo View Service.

  • Todo View Service - implements a REST endpoint for querying the todos. It maintains a CQRS view of the todos in ElasticSearch.

The Todo Service publishes events using Eventuate Tram. Eventuate Tram inserts events into the MESSAGE table as part of the ACID transaction that updates the TODO table. The Eventuate Tram CDC service tracks inserts into the MESSAGE table using the MySQL binlog and publishes messages to Apache Kafka. The Todo View Service subscribes to the events and updates ElasticSearch.

Two flavors of the application: monolithic and microservices

There are two versions of the application:

  • single-module - a single module Gradle project for a monolithic version of the application. It is the easiest to get started with.
  • multi-module - a multi-module Gradle project for the microservices-based version of the application. It consists of a todo-service, which creates and updates Todos, and todo-view-service, which maintains a CQRS view view in ElasticSearch

How it works

The Todo application uses the Eventuate Tram framework to publish and consume domain events.

Domain event publisher

The TodoCommandService publishes an event when it creates, updates, or deletes a Todo. It uses the DomainEventPublisher, which is implemented by the Eventuate Tram framework. DomainEventPublisher publishes the event as part of the transaction that updates the database. If the transactions commits the event will be published. Conversely, if the transaction is rolled back, then the event is not published.

@Service
@Transactional
public class TodoCommandService {

  @Autowired
  private TodoRepository todoRepository;

  @Autowired
  private DomainEventPublisher domainEventPublisher;

  public Todo create(CreateTodoRequest createTodoRequest) {
    Todo todo = new Todo(createTodoRequest.getTitle(), createTodoRequest.isCompleted(), createTodoRequest.getOrder());
    todo = todoRepository.save(todo);

    publishTodoEvent(todo, new TodoCreated(todo.getTitle(), todo.isCompleted(), todo.getExecutionOrder()));

    return todo;
  }

  private void publishTodoEvent(Todo todo, DomainEvent... domainEvents) {
    domainEventPublisher.publish(Todo.class, todo.getId(), asList(domainEvents));
  }

  ...

Domain event consumer

The CQRS view code subscribes to domain events published by the TodoCommandService. It defines DomainEventDispatcher @Bean to invoke the event handlers defined by TodoEventConsumer. The DomainEventDispatcher class is provided by the Eventuate Tram framework. It handles message de-duplication to ensure that the event handlers are idempotent.

@Configuration
public class TodoViewConfiguration {

  @Bean
  public DomainEventDispatcher domainEventDispatcher(TodoEventConsumer todoEventConsumer, MessageConsumer messageConsumer) {
    return new DomainEventDispatcher("todoServiceEvents", todoEventConsumer.domainEventHandlers(), messageConsumer);
  }

The TodoEventConsumer defines the event handlers, which update Elasticsearch.

public class TodoEventConsumer {

  @Autowired
  private TodoViewService todoViewService;

  public DomainEventHandlers domainEventHandlers() {
    return DomainEventHandlersBuilder
            .forAggregateType(Todo.class.getName())
            .onEvent(TodoCreated.class, dee -> {
              TodoCreated todoCreated = dee.getEvent();
              todoViewService.index(new TodoView(dee.getAggregateId(),
                  todoCreated.getTitle(), todoCreated.isCompleted(), todoCreated.getExecutionOrder()));
            })

Got questions?

Don't hesitate to create an issue or see

Don't forget to take a look at the other Eventuate Tram examples:

Building and running

Note: you do not need to install Gradle since it will be downloaded automatically. You just need to have Java 8 installed.

First, build the application

./gradlew assemble

Next, launch the services using Docker Compose:

export DOCKER_HOST_IP=...
docker-compose -f docker-compose-eventuate-mysql.yml build
docker-compose -f docker-compose-eventuate-mysql.yml up -d

Note:

  1. You can also run the Postgres version using docker-compose-eventuate-postgres.yml
  2. You need to set DOCKER_HOST_IP before running Docker Compose. This must be an IP address or resolvable hostname. It cannot be localhost. See this guide to setting DOCKER_HOST_IP for more information.

Using the application

Once the application has started, you can use the application via the Swagger UI.

If you are running the multi-module version:

  • http://${DOCKER_HOST_IP}:8081/swagger-ui.html - the command-side service
  • http://${DOCKER_HOST_IP}:8082/swagger-ui.html - the query-side service

If you are running the single-module version:

  • http://${DOCKER_HOST_IP}:8080/swagger-ui.html - the monolithic application

Got questions?

Don't hesitate to create an issue or see