Transactional Events Made Easy with Spring
How to publish events transactionally with RabbitMQ and Spring’s ApplicationEventPublisher.
It’s a good practice to use events for communication between your domain services.
Transactions in distributed systems are a hard problem and should be avoided when possible. The rule of thumb says that when you find yourself in need of a distributed transaction, your design of the service boundaries is probably suboptimal.
Instead of implementing complex and error-prone mechanisms like the two-phase commit protocol, we can use much simpler techniques such as the saga pattern. Sagas are much closer to how real-world business works, easier to maintain and reason about.
We still need transactions to make service actions atomic. Also, we need the events to respect the atomicity of the operation. An event must not be sent unless the transactional operation is completed successfully.
Spring provides a simple elegant solution for publishing events transactionally without any distributed transactional management (JTA) such as Atomikos or Bitronix.
Events
Consider the following code that collects payment in a three-step transaction and informs about it by publishing some events to the external RabbitMQ broker:
@Service @RequiredArgsConstructor public class PaymentService { private final RabbitTemplate rabbitTemplate; @Transactional public void collectPayment(Payment payment) { issuePayment(payment); rabbitTemplate.convertAndSend("paymentQueue", new PaymentIssued(payment)); validatePayment(payment); rabbitTemplate.convertAndSend("paymentQueue", new PaymentValidated(payment)); applyPayment(payment); rabbitTemplate.convertAndSend("paymentQueue", new PaymentApplied(payment)); } // ... more methods }
There is another code listening for the events and auditing them respectively:
@Component @Slf4j class PaymentAudit { @RabbitListener(queues = "paymentQueue") public void listen(Object event) { log.info("Event received: {}", event); // update the ledger // ... } }
At first glance, everything works as expected:
INFO issuing the payment INFO validating the payment INFO applying the payment INFO Event received: PaymentIssued INFO Event received: PaymentValidated INFO Event received: PaymentApplied
But what if a failure occurs in the transaction, for instance in the applyPayment
method?
Despite the fact that the transaction is rolled back, the two first events have been published. The system ends up in an inconsistent state:
INFO issuing the payment INFO validating the payment INFO applying the payment ERROR Oops! INFO Event received: PaymentIssued INFO Event received: PaymentValidated
This is not what we want. What we really want is events to respect the transaction and be published only when the transaction succeeds.
We need the events to be transactional!
Transactional Events
Spring provides a neat solution for this issue: ApplicationEventPublisher
together with @TransactionalEventListener
.
We simply change the service code in order to publish Spring’s application events instead of RabbitMQ messages:
@Service @RequiredArgsConstructor @Slf4j public class PaymentService { private final ApplicationEventPublisher eventPublisher; @Transactional public void collectPayment(Payment payment) { issuePayment(payment); eventPublisher.publishEvent(new PaymentIssued(payment)); validatePayment(payment); eventPublisher.publishEvent(new PaymentValidated(payment)); applyPayment(payment); eventPublisher.publishEvent(new PaymentApplied(payment)); } // ... more methods }
Then, we create a straightforward integration with the RabbitMQ broker:
@Component @RequiredArgsConstructor class RabbitIntegration { private final RabbitTemplate rabbitTemplate; @TransactionalEventListener public void handleEvent(PaymentEvent event) { rabbitTemplate.convertAndSend( "paymentQueue", event); } }
Now, the Spring’s transactional event listener resends the events to the broker only when the transaction finishes successfully:
INFO issuing the payment INFO validating the payment INFO applying the payment ERROR Oops!
No successful commit — no events published. Exactly what we wanted.
Conclusion
Dealing with transactions in the context of distributed systems is not a piece of cake.
Using Spring application events in your business code and integrating them at the boundaries can help you greatly with this tedious task.
Moreover, this pattern is handy in decoupling the underlying message infrastructure from the service code, too. One can easily switch the broker implementation or use the Spring events alone in a monolithic application where no external messaging is involved.
You can find the source code on GitHub.
For a more advanced example of this technique please refer to my domain-driven design microservices project.
Happy events!