Lesson 3: Domain Events
In this lesson out of Learn Spring Data, we'll explore a useful functionality that Spring Data provides for Domain Events, the @DomainEvents annotation.
Even though "Domain Event" is actually a Domain-Driven Design concept, the feature offered by Spring isn't really tied to a solution following this modelling and software development technique.
As such, in this lesson, we’ll start by simply exploring how the annotation works and its technical caveats. We'll finish by analyzing how the annotation fits in the Domain-Driven Design universe.
2. Lesson Notes
The relevant module you need to import when you're starting with this lesson is: domain-events-start
If you want to have a look at the fully implemented lesson, as a reference, feel free to import: domain-events-end
2.1. What Are Events
In Software Applications, Events allow information to be exchanged between loosely coupled components.
In the Resources section, you’ll be able to find more detailed information and examples on using Events in Spring. Here we’ll just recapitulate the basics.
In a nutshell, events include:
- An Event Object
- An Event Publisher
- One or more Event Listeners
Spring offers a handy event publishing mechanism that allows custom events to be easily created, listened to, and processed as we wish, taking away a lot of the boilerplate code.
The framework also offers predefined events tied to the application lifecycle that the developers can listen to and act upon.
2.2. The @DomainEvents Annotation
Simply put, with the @DomainEvents annotation, the framework offers support to easily publish events when Entities are modified in some way. This could be when an Entity is created when it’s been updated, or if it was deleted from the datasource.
Let's take a look at an example. Imagine that whenever we modify a Task, we want to trigger some functionality on the Projects that's not conceptually part of the update process, sort of like a side-effect of the Task update.
To do this, first, we'll define our TaskUpdated event class in the com.baeldung.lsd.domain.task.model package:
For simplicity, our Event only carries the ID of the Task that was updated, and it exposes a public getter to retrieve it.
Note: you can appreciate we're using terminology and naming conventions similar to the ones adopted in the main DDD reference books, but naturally, you can follow the approach that better fits your application.
Now we need our TaskUpdated event to be published when a Task is created or updated. For this, we'll have to work on our Task Entity class:
Spring Data expects the method with the @DomainEvents annotation to return a single event, or list of events, that will automatically publish whenever a save/saveAll/delete/deleteAll operation is invoked on the corresponding repository.
Spring Data also offers the @AfterDomainEventPublication annotation to define a callback method to be executed after all Domain Events are published.
This can be used to perform any potential clean up work after events are published. Naturally, it's optional and only works when the @DomainEvents annotation is present on the Entity.
Let’s implement a callback method here, just to log a message:
Now let's see how all this comes together in the run method of our main class by saving a Task:
Running the application, we see in the logs:
We see that Spring has invoked the callback, presumably after publishing our Event successfully.
In the next section, we'll learn how we can listen to this event.
2.3. Monitor Domain Events
Once they're published, Domain Events can be handled as any other application event; they can be intercepted either programmatically or declaratively.
In this case, let's remember that we want to simulate triggering some side-effect logic for our Projects, so we'll define a ProjectEventListeners @Component class in the com.baeldung.lsd.infrastructure.project.listener package, and use the @EventListener annotation on a method:
Let's run the app now and observe the log output:
As we can see, our Domain Event has reached the listener successfully.
At this point, there are a few things to take into consideration.
First of all, note that we decorated the method with a @TransactionalEventListener annotation; if we used the simple @EventListener instead, the method would get executed even if the save/update operation was rolled back as part of a failing facade transaction.
We could also consider annotating the method with @Async after enabling this feature in our application, as there should be no need to process these events in a synchronous manner, but we'll keep it simple on this occasion.
2.4. @DomainEvent Caveats
Using the @DomainEvent annotation has some caveats that we should be aware of.
First and foremost, the @DomainEvent annotation only works with Spring Data Repositories; we would have to resort to programmatic means if we want to achieve this outside the scope of Spring Data Repositories.
In addition, we have to always remember that Domain Events will get published only when save/saveAll/delete/deleteAll methods are explicitly invoked. So, for instance, if we simply let Hibernate persist our managed Task Entity modifications when a transaction is flushed, we would be bypassing the event publish functionality.
Furthermore, if there's an exception either during event publication or consumption, the event will remain unpublished or unconsumed, and therefore be lost.
Finally, it's worth mentioning that Spring also provides another method to achieve the same result with the AbstractAggregateRoot template. We'll leave a link in the Resources section where you can see how this alternative approach can be used.
2.5. Benefits of Domain Events with Domain-Driven Design
To fully comprehend the following sections, it's advisable to have at least a basic understanding of the Domain-Driven Design technique, along with its key concepts, such as Aggregates, Aggregate Roots, Bounded Contexts, and of course, Domain Events. We have listed some useful links under the Resources section as a reference.
Evans indicates that a Domain Event is simply a "representation of something that happened in the domain" that has relevance for the domain, so it has to be included in the modelling process as an actual component.
Furthermore, Domain Events can facilitate eventual consistency. Aggregates keep internal consistency at all times, so with this mechanism, we can efficiently propagate the changes that have an effect on other Aggregates.
Domain Events actually allow us to correctly implement Domain-Driven Design, and they directly benefit our software applications by allowing us to define loosely coupled components that can be managed independently of each other. By listening to and processing Domain Events, disparate systems can be easily integrated.
Domain Events also play well with Event-Driven Architectures and Event Sourcing, allowing us to generate a relevant history of events that can be used for auditing and debugging purposes, and to generate or to reestablish a correct state for the Entities managed by our system.
These benefits are the reason that Spring Data has built-in support for Domain Events.
2.6. DDD Domain Events and the @DomainEvents Annotation
We could publish and listen to Domain Events just like we do with any other Application Event in Spring, but with the @DomainEvents annotation, we avoid muddling our business logic with infrastructure-specific event handling code, at least for some operations that commonly trigger Domain Events.
It's worth mentioning that the example we developed in this lesson would have made more sense in an application that was fully aligned with all the DDD specifications; we can imagine our Task and Project Entities actually being Root Entities of two different Aggregates in order to appreciate the use of publishing a Domain Event whenever there's an update on a Task Entity, so the Project Aggregate can act accordingly.
Even though local propagation of Domain Events is in fact a valid scenario, Domain Events often have to be published externally to other systems. To do this, for instance, we could have just as easily implemented a listener that forwards the events to a Message Queue to be processed on remote Bounded Contexts; of course, we would also need the proper infrastructure in place.