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.
-
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.
Let’s consider the following business requirement:
When a customer places an order, the system should:
-
Record the order details, marking it as paid.
-
Notify the customer via email with an order confirmation.
-
Synchronize the order with Shopify to keep the store updated.
Here’s an overview of the architecture for this requirement:
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. |
-
Add dependencies
Include the following dependency in your E-commerce Spring Boot application along with necessary Spring starters such asspring-boot-starter-data-jpa
andspring-boot-starter-web
:<dependency> <groupId>io.github.raedbh</groupId> <artifactId>spring-outbox-jpa</artifactId> <version>0.4.0</version> </dependency>
-
Enable Spring Outbox for JPA
Annotate the main application class with@EnableJpaRepositories
, specifyingOutboxJpaRepositoryFactoryBean
as therepositoryFactoryBeanClass
:@SpringBootApplication @EnableJpaRepositories(repositoryFactoryBeanClass = OutboxJpaRepositoryFactoryBean.class) public class EcommerceApplication { public static void main(String[] args) { SpringApplication.run(EcommerceApplication.class, args); } }
-
Configure automatic outbox table creation
Set the following property in yourapplication.properties
(orapplication.yml
) file to automatically create the outbox table on application startup:spring.outbox.rdbms.auto-create=true
-
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 }
-
Define the Event
class OrderPlaced extends EventOutboxed<Order> { protected OrderPlaced(Order source, CommandOutboxed cmd) { super(source, cmd); } @Override public String getOperation() { return "payment"; } }
-
Define the Command
public class EmailNotification implements CommandOutboxed { private String to; private String subject; private String body; // ... }
-
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 }
-
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. -
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
andemails
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 theSUPER
orREPLICATION 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
andemails
):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'.
-
-
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.
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. |
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
Spring Outbox follows the Spring Framework Code Style. You find here the project formatters for Eclipse and Intellij.
Spring Outbox is licensed under Apache 2.0 license.