DEV Community

Ishan Soni
Ishan Soni

Posted on

Using Spring Application Events within Transactional Contexts

This post is inspired by this talk 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. "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
}
Enter fullscreen mode Exit fullscreen mode

@Repository
public interface CustomerJpaRepository extends JpaRepository<Customer, Long> {
}
Enter fullscreen mode Exit fullscreen mode

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;
   }

}
Enter fullscreen mode Exit fullscreen mode

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);
    }

}
Enter fullscreen mode Exit fullscreen mode

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

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

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);
}
Enter fullscreen mode Exit fullscreen mode

Transactional Event Listener

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

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);
    }

}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (3)

Collapse
 
gasper_lf profile image
Lewis Florez R.

Awesome!!!

Collapse
 
ishansoni22 profile image
Ishan Soni

Thank you!

Collapse
 
andrecbrito profile image
Andrecbrito

Thanks this helped me a lot