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!