Spring Data JPA – Publishing Domain Events When Changing an Entity


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Since Spring Data JPA 1.11 (the Ingalls release), you can automatically publish domain events when an entity object gets saved. You only need to add a method to your entity class that returns a Collection of the event objects you want to publish and annotate the method with @DomainEvents. Spring Data JPA calls that method and publishes the events when you execute the save or saveAll method of the entity’s repository. Similar to other Spring application events, you can observe them using an @EventListener or @TransactionalEventListener.

The main goal of this implementation is to support domain events defined in Domain-Driven Design. These are usually published by aggregate roots and used to inform other parts of your application that an event happened in your business domain. In contrast to other commonly used events, like entity lifecycle events, a domain event should not contain any technical details.

You can, of course, publish these events programmatically in your business code using Spring’s ApplicationEventPublisher. That’s usually the right approach if the event is triggered by a specific business operation and not the change of an attribute’s value. But if different business operations cause the same change on an entity object and trigger the same event, using a domain event is easier and less error-prone.

Publish Domain Events From Your Entity Class

As mentioned earlier, your entity class has to provide a method annotated with @DomainEvents that returns all events you want to publish. Each event is represented by one object. I recommend using a specific class for each type of event you want to trigger. That makes it easier to implement an event observe that only reacts to a specific type of event.

In the example of this article, I want to publish a domain event when a tournament has ended. I created the TournamentEndedEvent class to represent this event. It contains the id of the tournament and its end date.

public class TournamentEndedEvent {

    private Long tournamentId;

    private LocalDate endDate;

    public TournamentEndedEvent(Long tournamentId, LocalDate endDate) {
        this.tournamentId = tournamentId;
    }

    public Long getTournamentId() {
        return tournamentId;
    }

    public LocalDate getEndDate() {
        return endDate;
    }
}

Implement the Event Publishing Yourself

One option to tell Spring Data JPA which events you want to publish is implementing your own method and annotating it with @DomainEvents.

In the endTournament method of my ChessTournament class, I set the endDate of the tournament to now. Then I instantiate a new TournamentEndedEvent and add it to the List of events I want to publish when saving the tournament.

@Entity
public class ChessTournament {

    @Transient
    private final List<Object> domainEvents = new ArrayList<>();

    private LocalDate endDate;

    // more entity attributes
	
    public void endTournament() {
        endDate = LocalDate.now();
        domainEvents.add(new TournamentEndedEvent(id, endDate));
    }

    @DomainEvents
    public List<Object> domainEvents() {
        return domainEvents;
    }

    @AfterDomainEventPublication
    public void clearDomainEvents() {
        domainEvents.clear();
    }
}

As you can see in the code snippet, I also implemented 2 additional methods.

I annotated the domainEvents method with a @DomainEvents annotation and returned the List of events I want to publish. That’s the method I mentioned earlier. Spring Data JPA calls it when I call the save or saveAll method on my ChessTournamentRepository.

The @AfterDomainEventPublication annotation on the clearDomainEvents method tells Spring Data JPA to call this method after publishing all events returned by the domainEvents method. Depending on your observer implementation, this can be before or after your observers handled the event.

In this example, I use that method to clear the List of events. That ensures that I don’t publish any event twice, even if my business code calls the save method of my ChessTournamentRepository multiple times.

Extend Spring’s AbstractAggregateRoot

As you saw in the previous section, you can easily implement the required methods to manage the List of events you want to publish and provide it to Spring Data JPA. But I recommend using an even simpler option.

Spring Data provides the AbstractAggregateRoot class, which provides all these methods for you. You only need to extend it and call the registerEvent method to add your event object to the List.

@Entity
public class ChessTournament extends AbstractAggregateRoot<ChessTournament> {

    private LocalDate endDate;

    // more entity attributes
	
    public void endTournament() {
        endDate = LocalDate.now();
        registerEvent(new TournamentEndedEvent(id, endDate));
    }
}

Observe Domain Events

Spring provides a powerful event handling mechanism that’s explained in great detail in the Spring documentation. You can observe your domain events in the same way as any other Spring event. In this article, I will give you a quick overview of Spring’s event handling features and point out a few pitfalls when working in a transactional context.

To implement an observer, you need to implement a method that expects 1 parameter of the type of your event class and annotate it with @EventListener or @TransactionalEventListener.

Observing Events Synchronously

Spring executes all observers annotated with @EventListener synchronously and within the transactional context of the event publisher. As long as your observer uses Spring Data JPA, all its read and write operations use the same context as the business code that triggered the event. This enables it to read uncommitted changes of the current transaction and add its own changes to it.

In the following observer implementation, I use that to change the ended flag on all ChessGames of a ChessTournament to true and write a short log message.

@EventListener
public void handleTournamentEndedEvent(TournamentEndedEvent event) {
	log.info("===== Handling TournamentEndedEvent ====");

	Optional<ChessTournament> chessTournament = chessTournamentRepository.findById(event.getTournamentId());
	chessTournament.ifPresent(tournament -> {
		tournament.getGames().forEach(chessGame -> {
			chessGame.setEnded(true);
			log.info("Game with id {} ended: {} ", chessGame.getId(), chessGame.isEnded());
		});
	});
}

Let’s use this event observer and the previously described ChessTournament entity in the following test case. It gets a ChessTournament entity from the database and calls the entity’s endTournament method. It then calls the save method of the tournamentRepository and writes a log message afterward.

log.info("===== Test Domain Events =====");
ChessTournament chessTournament = tournamentRepository.getOne(1L);

// End the tournament
chessTournament.endTournament();

// Save the tournament and trigger the domain event
ChessTournament savedTournament = tournamentRepository.save(chessTournament);
log.info("After tournamentRepository.save(chessTournament);");

You can see in the log output that Spring Data JPA called the event observer when saving the entity. That was a synchronous call that paused the execution of the test case until all observers handled the event. All operations performed by the observer were part of the current transaction. That enabled the observer to initialize the lazily fetched association from the ChessTournament to the ChessGame entity and change each game’s ended attribute.

2021-10-23 14:56:33.158  INFO 10352 --- [           main] c.t.janssen.spring.data.TestKeyConcepts  : ===== Test Domain Events =====
2021-10-23 14:56:33.180 DEBUG 10352 --- [           main] org.hibernate.SQL                        : select chesstourn0_.id as id1_2_0_, chesstourn0_.end_date as end_date2_2_0_, chesstourn0_.name as name3_2_0_, chesstourn0_.start_date as start_da4_2_0_, chesstourn0_.version as version5_2_0_ from chess_tournament chesstourn0_ where chesstourn0_.id=?
2021-10-23 14:56:33.216  INFO 10352 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : ===== Handling TournamentEndedEvent ====
2021-10-23 14:56:33.221 DEBUG 10352 --- [           main] org.hibernate.SQL                        : select games0_.chess_tournament_id as chess_to6_0_0_, games0_.id as id1_0_0_, games0_.id as id1_0_1_, games0_.chess_tournament_id as chess_to6_0_1_, games0_.date as date2_0_1_, games0_.ended as ended3_0_1_, games0_.player_black_id as player_b7_0_1_, games0_.player_white_id as player_w8_0_1_, games0_.round as round4_0_1_, games0_.version as version5_0_1_ from chess_game games0_ where games0_.chess_tournament_id=?
2021-10-23 14:56:33.229  INFO 10352 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 3 ended: true 
2021-10-23 14:56:33.230  INFO 10352 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 2 ended: true 
2021-10-23 14:56:33.230  INFO 10352 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 5 ended: true 
2021-10-23 14:56:33.230  INFO 10352 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 1 ended: true 
2021-10-23 14:56:33.230  INFO 10352 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 6 ended: true 
2021-10-23 14:56:33.230  INFO 10352 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 4 ended: true 
2021-10-23 14:56:33.230  INFO 10352 --- [           main] c.t.janssen.spring.data.TestKeyConcepts  : After tournamentRepository.save(chessTournament);
2021-10-23 14:56:33.283 DEBUG 10352 --- [           main] org.hibernate.SQL                        : update chess_tournament set end_date=?, name=?, start_date=?, version=? where id=? and version=?
2021-10-23 14:56:33.290 DEBUG 10352 --- [           main] org.hibernate.SQL                        : update chess_game set chess_tournament_id=?, date=?, ended=?, player_black_id=?, player_white_id=?, round=?, version=? where id=? and version=?
2021-10-23 14:56:33.294 DEBUG 10352 --- [           main] org.hibernate.SQL                        : update chess_game set chess_tournament_id=?, date=?, ended=?, player_black_id=?, player_white_id=?, round=?, version=? where id=? and version=?
2021-10-23 14:56:33.296 DEBUG 10352 --- [           main] org.hibernate.SQL                        : update chess_game set chess_tournament_id=?, date=?, ended=?, player_black_id=?, player_white_id=?, round=?, version=? where id=? and version=?

Observing Events at the End of the Transaction

If you want to execute your observers at the end of the current transaction, you need to annotate it with @TransactionalEventListener instead of @EventListener. Spring then calls the observer in the defined TransactionPhase. You can choose between BEFORE_COMMIT, AFTER_COMMIT, AFTER_ROLLBACK, and AFTER_COMPLETION. By default, Spring executes transactional observers in the AFTER_COMMIT phase.

Besides the different annotations, you can implement your event observer in the same way as the synchronous observer I showed you in the previous example.

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleTournamentEndedEvent(TournamentEndedEvent event) {
	log.info("===== Handling TournamentEndedEvent ====");

	Optional<ChessTournament> chessTournament = chessTournamentRepository.findById(event.getTournamentId());
	chessTournament.ifPresent(tournament -> {
		tournament.getGames().forEach(chessGame -> {
			chessGame.setEnded(true);
			log.info("Game with id {} ended: {} ", chessGame.getId(), chessGame.isEnded());
		});
	});
}

In this case, I decide to execute my observer before Spring commits the transaction. This ensures that the observer doesn’t block the execution of my test case. When Spring calls the observer, the transactional context is still active, and all performed operations become part of the transaction that my test case started.

When I execute the same test case as in the previous example, you can see in the log output that Spring calls the observer after my test case performed all its operations but before Spring commits the transaction.

2021-10-23 15:15:43.234  INFO 18704 --- [           main] c.t.janssen.spring.data.TestKeyConcepts  : ===== Test Domain Events =====
2021-10-23 15:15:43.254 DEBUG 18704 --- [           main] org.hibernate.SQL                        : select chesstourn0_.id as id1_2_0_, chesstourn0_.end_date as end_date2_2_0_, chesstourn0_.name as name3_2_0_, chesstourn0_.start_date as start_da4_2_0_, chesstourn0_.version as version5_2_0_ from chess_tournament chesstourn0_ where chesstourn0_.id=?
2021-10-23 15:15:43.291  INFO 18704 --- [           main] c.t.janssen.spring.data.TestKeyConcepts  : After tournamentRepository.save(chessTournament);
2021-10-23 15:15:43.332  INFO 18704 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : ===== Handling TournamentEndedEvent ====
2021-10-23 15:15:43.337 DEBUG 18704 --- [           main] org.hibernate.SQL                        : select games0_.chess_tournament_id as chess_to6_0_0_, games0_.id as id1_0_0_, games0_.id as id1_0_1_, games0_.chess_tournament_id as chess_to6_0_1_, games0_.date as date2_0_1_, games0_.ended as ended3_0_1_, games0_.player_black_id as player_b7_0_1_, games0_.player_white_id as player_w8_0_1_, games0_.round as round4_0_1_, games0_.version as version5_0_1_ from chess_game games0_ where games0_.chess_tournament_id=?
2021-10-23 15:15:43.344  INFO 18704 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 3 ended: true 
2021-10-23 15:15:43.345  INFO 18704 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 5 ended: true 
2021-10-23 15:15:43.345  INFO 18704 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 6 ended: true 
2021-10-23 15:15:43.345  INFO 18704 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 4 ended: true 
2021-10-23 15:15:43.345  INFO 18704 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 2 ended: true 
2021-10-23 15:15:43.345  INFO 18704 --- [           main] c.t.j.s.d.h.TournamentEndedEventHandler  : Game with id 1 ended: true 
2021-10-23 15:15:43.356 DEBUG 18704 --- [           main] org.hibernate.SQL                        : update chess_tournament set end_date=?, name=?, start_date=?, version=? where id=? and version=?
2021-10-23 15:15:43.362 DEBUG 18704 --- [           main] org.hibernate.SQL                        : update chess_game set chess_tournament_id=?, date=?, ended=?, player_black_id=?, player_white_id=?, round=?, version=? where id=? and version=?
2021-10-23 15:15:43.365 DEBUG 18704 --- [           main] org.hibernate.SQL                        : update chess_game set chess_tournament_id=?, date=?, ended=?, player_black_id=?, player_white_id=?, round=?, version=? where id=? and version=?

Pitfalls When Working with Domain Events

As simple as working with domain events might seem, several pitfalls can cause Spring not to publish an event, not to call an observer, or not to persist the changes performed by an observer.

No save call = No events

Spring Data JPA only publishes the domain events of an entity if you call the save or saveAll method on its repository.

But if you’re working with a managed entity, which usually is every entity object you fetched from the database during the current transaction, you don’t need to call any repository method to persist your changes. You only need to call a setter method on an entity object and change the attribute’s value. Your persistence provider, e.g., Hibernate, detects the change automatically and persists.

No transaction = No transactional observers

Spring only calls the transaction observers I showed you in the 2nd example if you commit or rollback a transaction. If your business code publishes an event without an active transaction, Spring will not call these observers.

AFTER_COMMIT / AFTER_ROLLBACK / AFTER_COMPLETION = New transaction required

If you implement a transactional observer and attach it to the transaction phase AFTER_COMMIT, AFTER_ROLLBACK or AFTER_COMPLETION, Spring executes the observer without an active transaction. Due to that, you can only read data from the database, but Spring Data JPA doesn’t persist any changes.

You can avoid that problem by annotating your observer method with @Transactional(propagation = Propagation.REQUIRES_NEW). That tells Spring Data JPA to start a new transaction before calling the observer and committing it afterward.

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleTournamentEndedEvent(TournamentEndedEvent event) {
	log.info("===== Handling TournamentEndedEvent ====");

	Optional<ChessTournament> chessTournament = chessTournamentRepository.findById(event.getTournamentId());
	chessTournament.ifPresent(tournament -> {
		tournament.getGames().forEach(chessGame -> {
			chessGame.setEnded(true);
			log.info("Game with id {} ended: {} ", chessGame.getId(), chessGame.isEnded());
		});
	});
}

When doing that, please keep in mind that the observer’s transaction is independent of the one used by the business code that triggered the event.

BEFORE_COMMIT = Change

If you attach your event observer to the BEFORE_COMMIT transaction phase, as I did in one of the previous examples, Spring executes the observer as part of your current transaction. Due to that, you have no guarantee that all changes have been flushed to the database, and you only see the flushed changes if you access the database using the same transaction.

To prevent your observers from working on outdated information, you should use Spring Data JPA’s repositories to access your database. That’s what I did in the examples of this article. It gives you access to all unflushed changes in the current persistence context and ensures that your queries are part of the same transaction.

Conclusion

Domain events, as defined in Domain-Driven Design, describe an event that happened in the business domain of your application.

Using Spring Data JPA, you can publish one or more domain events when calling the save or saveAll method of a repository. Spring then checks if the provided entity has a method annotated with a @DomainEvents annotation, calls it, and publishes the returned event objects.

You can implement an observer for your domain events in the same way as any other event observer in Spring. You only need a method that expects a parameter of the type of your event class and annotate it with @EventListener or @TransactionalEventListener.

2 Comments

  1. Great article. How do publish event when delete an entity?

    1. Thanks, Eyal!
      I saw you asked the same question in the Persistence Hub Forum and I answered it there in more detail.
      To everyone else who might ask themselves the same question: You need to add a lifecycle callback that adds an event object to the list. Spring Data JPA then takes care of the rest.

Comments are closed.