Skip to content

raedbh/spring-outbox

Repository files navigation

Spring Outbox

Spring Outbox is an implementation of the transactional outbox pattern that helps Spring developers implement an efficient event-driven architecture for microservices and monolithic applications.

Implementation Key Considerations

  • When a domain event occurs, it generates one or more messages.

  • Each outbox entry represents either an event or a command.

  • An operation, part of an event, represents the action performed on the root entity (e.g., create, update, award) and helps consumers determine if deserialization is needed.

  • Decouple the outbox message producer and consumer to enable scalability and independent evolution.

  • Keep the Debezium connector simple by focusing on reading outbox entries and producing messages.

Spring Outbox in Action

Let’s consider the following business requirement:

When a customer places an order, the system should:

  1. Record the order details, marking it as paid.

  2. Notify the customer via email with an order confirmation.

  3. Synchronize the order with Shopify to keep the store updated.

Here’s an overview of the architecture for this requirement:

order placed diagram

Following are the steps to implement this design with Spring Outbox:

Note
Spring Outbox supports multiple databases and brokers. In this example, we use MySQL and RabbitMQ.
  1. Add dependencies
    Include the following dependency in your E-commerce Spring Boot application along with necessary Spring starters such as spring-boot-starter-data-jpa and spring-boot-starter-web:

    <dependency>
        <groupId>io.github.raedbh</groupId>
        <artifactId>spring-outbox-jpa</artifactId>
        <version>0.4.0</version>
    </dependency>
  2. Enable Spring Outbox for JPA
    Annotate the main application class with @EnableJpaRepositories, specifying OutboxJpaRepositoryFactoryBean as the repositoryFactoryBeanClass:

    @SpringBootApplication
    @EnableJpaRepositories(repositoryFactoryBeanClass = OutboxJpaRepositoryFactoryBean.class)
    public class EcommerceApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(EcommerceApplication.class, args);
        }
    }
  3. Configure automatic outbox table creation
    Set the following property in your application.properties (or application.yml) file to automatically create the outbox table on application startup:

    spring.outbox.rdbms.auto-create=true
  4. Define the Root Entity

    @Entity
    @Table(name = "orders")
    public class Order extends RootEntity {
    
        private @Id UUID id;
        private BigDecimal totalAmount;
        // Other fields
    
        @Override
        public UUID getId() {
            return id;
        }
    
        // Other getters / setters
    }
  5. Define the Event

    class OrderPlaced extends EventOutboxed<Order> {
    
        protected OrderPlaced(Order source, CommandOutboxed cmd) {
            super(source, cmd);
        }
    
        @Override
        public String getOperation() {
            return "payment";
        }
    }
  6. Define the Command

    public class EmailNotification implements CommandOutboxed {
    
        private String to;
        private String subject;
        private String body;
        // ...
    }
  7. Implement the order payment logic

    @Entity
    @Table(name = "orders")
    public class Order extends RootEntity {
    
        private @Id UUID id;
        private BigDecimal totalAmount;
        // Other fields
    
        Order markPaid(EmailNotification withNotification) {
    
            // Business logic here
    
            // Assign the event, which will be triggered later
            assignEvent(new OrderPlaced(this, withNotification));
    
            return this;
        }
    
        @Override
        public UUID getId() {
            return id;
        }
    
        // Other getters / setters
    }
  8. Implement the application service

    @Service
    public class OrderManagement {
    
        private final OrderRepository repository;
    
        OrderManagement(OrderRepository repository){
            this.repository = repository;
        }
    
        @Transactional
        Order placeOrder(Order order, EmailNotification emailNotification) {
    
            order.markPaid(emailNotification);
    
            return repository.save(order);
        }
    }

    When the placeOrder method is called, Spring Outbox triggers the assigned event, producing new entries (for event and command) in the outbox table.

  9. Run the Debezium connector
    The Debezium connector detects new entries in outbox table and transmits them to RabbitMQ. Follow these steps to set it up:

    • Create the queues shopify.orders and emails in RabbitMQ.

    • Replace the placeholders in the following docker run command with your specific configurations and run it to start the connector:

      docker run -d \
        -e SPRING_OUTBOX_CONNECTOR_DATABASE_HOSTNAME=<db_host> \
        -e SPRING_OUTBOX_CONNECTOR_DATABASE_DBNAME=<db> \
        -e SPRING_OUTBOX_CONNECTOR_DATABASE_USER=<db_user> \
        -e SPRING_OUTBOX_CONNECTOR_DATABASE_PASSWORD=<db_password> \
        -e SPRING_OUTBOX_CONNECTOR_OFFSET_STORAGE_FILEPATH=<outbox_offset_storage_file> \
        -e SPRING_OUTBOX_CONNECTOR_SCHEMA_HISTORY_FILEPATH=<outbox_schema_history_file> \
        -e SPRING_OUTBOX_CONNECTOR_RABBIT_MESSAGES_ORDER_PLACED_ROUTING_KEY=shopify.orders \
        -e SPRING_OUTBOX_CONNECTOR_RABBIT_MESSAGES_EMAIL_NOTIFICATION_ROUTING_KEY=emails \
        -e SPRING_RABBITMQ_HOST=<rabbit_host> \
        -e SPRING_RABBITMQ_USERNAME=<rabbit_user> \
        -e SPRING_RABBITMQ_PASSWORD=<rabbit_password> \
        --net host \
        --name spring-outbox-debezium-connector \
        raed/spring-outbox-debezium-connector-mysql-rabbit:0.4.0
      Note
      <db_user> requires at least one of the SUPER or REPLICATION CLIENT privileges to allow the connector to read from the MySQL binary log.

      After successful execution, you should see logs similar to the following, with a new message added to each queue (shopify.orders and emails):

       INFO 5095 --- [ebeziumConsumer] i.g.r.s.o.c.c.DebeziumRabbitRouteBuilder : Change processing [operation: c] [body: Struct{id=java.nio.HeapByteBuffer[pos=0 lim=16 cap=16],type=OrderPlaced,payload=java.nio.HeapByteBuffer[pos=0 lim=532 cap=532],metadata={"operation":"payment","event_entity_id":"c59d04ee-c872-4ad2-868e-2dc921ef7bd0","event_entity_type":"Order","event_occurred_at":"1735010989279"}}]
       WARN 5095 --- [ebeziumConsumer] .g.r.s.o.c.r.RabbitOutboxMessageProducer : No exchange found for OrderPlaced
       INFO 5095 --- [ebeziumConsumer] .g.r.s.o.c.r.RabbitOutboxMessageProducer : Message sent to exchange 'null' with routing key 'shopify.orders'.
       INFO 5095 --- [ebeziumConsumer] i.g.r.s.o.c.c.DebeziumRabbitRouteBuilder : Change processing [operation: c] [body: Struct{id=java.nio.HeapByteBuffer[pos=0 lim=16 cap=16],type=EmailNotification,payload=java.nio.HeapByteBuffer[pos=0 lim=132 cap=132]}]
       WARN 5095 --- [ebeziumConsumer] .g.r.s.o.c.r.RabbitOutboxMessageProducer : No exchange found for EmailNotification
       INFO 5095 --- [ebeziumConsumer] .g.r.s.o.c.r.RabbitOutboxMessageProducer : Message sent to exchange 'null' with routing key 'emails'.
  10. Consume Outbox messages
    Once messages are successfully delivered to the broker, they can be consumed by the Email Service and Shopify Synchronizer. Here’s how to set this up:

    • Add the following dependency to the Email Service and Shopify Synchronizer Spring Boot applications along with necessary Spring starters such as spring-boot-starter-amqp:

      <dependency>
          <groupId>io.github.raedbh</groupId>
          <artifactId>spring-outbox-rabbit</artifactId>
          <version>0.4.0</version>
      </dependency>
    • Implement a message handler to handle and process incoming messages in the Shopify Synchronizer:

      @Component
      class OrderPlacedHandler {
      
          @RabbitListener(queues = "shopify.orders")
          void onOrderPlaced(@OutboxMessageBody(operation = "payment") Optional<Order> order) {
              order.ifPresent(orderDetails -> {
                  // Order processing logic here: prepare request, call Shopify API, etc
              });
          }
      }
    • Similarly, for the Email Service, define a handler to process email notifications:

      @Component
      class EmailNotificationHandler {
      
          @RabbitListener(queues = "emails")
          void handleEmailNotification(@OutboxMessageBody Optional<EmailNotification> notification) {
              notification.ifPresent(emailDetails -> {
                  // Email sending logic here
              });
          }
      }

That’s all you need to get started with Spring Outbox in microservices context. For a complete example of monolith application, check out the spring-outbox-sample.

Contribution

There are several ways to contribute to Spring Outbox:

  • Open Issues: If you find bugs, or you have ideas for improvement, feel free to open an issue.

  • Start Discussions: Join ongoing conversations or start a new one in the Discussions tab to share feedback or provide suggestions.

  • Submit Pull Requests: If you have a fix or improvement, fork the repository and submit a pull request.

Note
If you’d like to work on an issue, please comment on it first and briefly describe the approach you plan to take. This ensures alignment with the project’s direction and avoids duplicate efforts.

Building

The project requires JDK 17 or higher.

To compile, test, and build the project, run:

./mvnw install

To include Testcontainers tests (require Docker), run:

./mvnw install -Dspring.profiles.active=testcontainers

Code Style

Spring Outbox follows the Spring Framework Code Style. You find here the project formatters for Eclipse and Intellij.

License

Spring Outbox is licensed under Apache 2.0 license.