Using Spring Application Events within Transactional Contexts
Tags: springboot, springevents, java, transactional
Published: 2023-12-20 13:05:31

This post is inspired by [this talk](https://youtu.be/h8TWQM6fKNQ?si=zv7-S_MFMDAgJU9L) given by Bartłomiej Słota. I highly recommend checking it out.

Application events are a great way of communicating between beans defined in the same application or bounded context.

It is a great way of implementing the **[Observer Design Pattern](https://dev.to/ishansoni22/the-observer-design-pattern-51bi)**. "_Hey, a customer is created. If anybody is interested, here is the event. Do whatever you want to do with it!_"

In Spring, you can publish events using the **ApplicationEventPublisher**, which is the super interface for ApplicationContext and is the abstraction around the Spring event bus. In the latest versions of Spring, you can simply use POJOs as events.

**Example**: Create a customer and then generate a verification token that will be sent to their email ID using which they can verify their account. These two should be independent processes, i.e., creating a customer and generating a verfication token should happen in two separate transactions!

The Customer entity and the CustomerJpaRepository

    @Entity
    public class Customer {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private String email;
        private String verificationToken;
        private boolean verified;

        public Customer(String name, String email) {
            this.name = name;
            this.email = email;
        }

        public void setVerificationToken(String generatedToken) {
            this.verificationToken = generatedToken;
        }

        public boolean verifyCustomer(String providedToken) {
            if (StringUtils.hasLength(this.verificationToken) && this.verificationToken.equals(providedToken)) {
                this.verified = true;
            }
            return this.verified;
        }
        //Getters and Setters
    }
-----

    @Repository
    public interface CustomerJpaRepository extends JpaRepository<Customer, Long> {
    }

The CustomerService. The createCustomer method will create a customer and emit a CustomerCreated event using Spring’s ApplicationEventPublisher

    @Service
    public class CustomerService {

        private final CustomerJpaRepository customerJpaRepository;

        private final ApplicationEventPublisher applicationEventPublisher;

        public CustomerService(CustomerJpaRepository customerJpaRepository, ApplicationEventPublisher applicationEventPublisher) {
            this.customerJpaRepository = customerJpaRepository;
            this.applicationEventPublisher = applicationEventPublisher;
        }

        @Transactional
        public Customer createCustomer(String name, String email) {
            final Customer customer = customerJpaRepository.save(new Customer(name, email));
            applicationEventPublisher.publishEvent(new CustomerCreated(customer.getId()));
            return customer;
       }

    }

To create subscribers, you can use Spring’s **@EventListener**

    @Component
    public class VerificationTokenGenerator {

        private final CustomerJpaRepository customerJpaRepository;

        public VerificationTokenGenerator(CustomerJpaRepository customerJpaRepository) {
            this.customerJpaRepository = customerJpaRepository;
        }

        @EventListener
        public void generateVerificationToken(CustomerCreated customerCreated) {
            final Long customerId = customerCreated.getId();
            final Customer customer = customerJpaRepository.findById(customerId).get();
            customer.setVerificationToken(
                String.valueOf(
                    Objects.hashCode(customer)
                )
            ); //Dummy

            customerJpaRepository.save(customer);
        }

    }

Is there a separation of concerns here? i.e does the token generation process happen in a separate transaction? — **No**!

Why? — By default, Spring events are synchronous i.e the producer won’t proceed unless all listeners have been executed i.e if the producer is executing inside a transaction, the transaction is propagated to all listeners i.e in this case the generateVerificationToken() is executed within the same transaction as createCustomer()


![Event Listener](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ftr0ymv9zxi5qs2eesi.png)

See above — The createCustomer() has executed, and we are currently in the generateVerificationToken(), still a “select * from customer” query returns no result! Once the generateVerificationToken method is successful, the transaction is committed!

But, how does customerJpaRepository.findById() in the generateVerificationToken() returns back a customer if the transaction is not committed? — Hibernate’s First Level Cache!

But I want the generateVerificationToken() to be executed in a separate transaction. How do we get around this? — use **@TransactionalEventListener**. i.e using @TransactionalEventListener, you can collaborate with surrounding transactions using Phases!

By default the phase is AFTER_COMMIT, i.e execute this listener only after the enclosing transaction has committed


![Phases](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/86uo34u8vgf1dipzp4g0.png)

If we change our code to this:

    @TransactionalEventListener
    public void generateVerificationToken(CustomerCreated customerCreated) {
        final Long customerId = customerCreated.getId();
        final Customer customer =   customerJpaRepository.findById(customerId).get();
        customer.setVerificationToken(
            String.valueOf(
                Objects.hashCode(customer)
            )
        ); //Dummy

        customerJpaRepository.save(customer);
    }


![Transactional Event Listener](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mj09hfjst21hgdjkkaoy.png)

The event listener receives the event only after the transaction has committed. But the verification token is not saved. It’s like the second save didn’t work at all!

Think about it, the listener is executed only after the original transaction commits. A transaction is all or nothing. Once the transaction has committed, you cannot do anything more!


![After Commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gcjwyzzf7cn9vdsla2cm.png)

Use this to do something after the transaction has successfully committed. Things like sending an email or to publish a message to a messaging broker, but if you want to do additional DB work, it won’t work! Instead you need to start another transaction!

        @TransactionalEventListener
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void generateVerificationToken(CustomerCreated customerCreated) {
            final Long customerId = customerCreated.getId();
            final Customer customer = customerJpaRepository.findById(customerId).get();
            customer.setVerificationToken(
                String.valueOf(
                    Objects.hashCode(customer)
                )
            ); //Dummy

            customerJpaRepository.save(customer);
        }

    }

i.e execute this listener after the original transaction (AFTER_COMMIT), and open a new Transaction (REQUIRES NEW)!

**Rule of Thumb**

1. Use @EventListener if you want this work to be a part of the enclosing/original transaction
2. Use @TransactionalEventListener (default phase = AFTER_COMMIT) if you want this work to be done after the enclosing transaction is committed. But remember, you won’t be able to do any DB work since the original transaction has already committed. Use this to do other work — send emails, send events to a messaging queue, etc.
3. Use @TransactionalEventListener (default phase = AFTER_COMMIT) along with @Transactional(propagation=REQUIRES_NEW) to do DB work in a new transaction after the enclosing/original transaction is committed!