1. Overview
In this lesson, we’ll publish a domain event through a Spring Data repository and handle it in the application. We’ll also examine when publication occurs, what it guarantees, and how it relates to Domain-Driven Design.
We’ll start with the domain-events-start project.
For reference, the completed code is in the domain-events-end project.
2. Publishing and Handling Domain Events
We’ll use a task update to see how a repository publishes an event and how a listener handles it.
2.1. What Are Events
Events let loosely coupled components exchange information.
The Resources section has more detail on Spring events. Here, we’ll review the roles we need for this example.
An event mechanism has three roles:
- Event object
- Event publisher
- One or more event listeners
Spring provides a publisher and listener mechanism for custom application events, so we do not have to call each listener directly.
Spring also publishes application lifecycle events that listeners can handle.
2.2. The @DomainEvents Annotation
Spring Data publishes events returned by an aggregate’s @DomainEvents method when supported repository methods receive that aggregate. Saving or deleting through a supported repository method can publish events. Changing a managed entity alone cannot.
Suppose a Task update should trigger a separate action for its Campaign. We’ll publish an event so the task update does not call that action directly.
To do this, first, we’ll define our TaskUpdated event class in the com.baeldung.lsd.domain.task.model package:
public class TaskUpdated {
private Long taskId;
public TaskUpdated(Task task) {
this.taskId = task.getId();
}
public Long getTaskId() {
return taskId;
}
}
The listener reads the task ID through getTaskId().
To publish TaskUpdated when the repository saves a task, we’ll add an event method to the Task entity:
@Entity
public class Task {
// ...
@DomainEvents
public List<Object> domainEvents() {
return List.of(new TaskUpdated(this));
}
}
The @DomainEvents method returns one or more events for publication. These include save(), saveAll(), delete(entity), and deleteAll(Iterable). The no-argument deleteAll() does not publish events in this project.
@AfterDomainEventPublication runs a callback after event dispatch.
The callback can clean up event state. It does not confirm a commit or listener success.
Let’s implement a callback method here, just to log a message:
@Entity
public class Task {
// ...
private static final Logger LOG = LoggerFactory.getLogger(Task.class);
// ...
@AfterDomainEventPublication
void callback() {
LOG.info("Task Domain Events Published!");
}
}
In the application runner, we’ll save a new Task:
@SpringBootApplication
public class DomainEventsApp implements ApplicationRunner {
// ...
@Autowired
private TaskRepository taskRepository;
@Override
public void run(ApplicationArguments args) throws Exception {
Campaign campaign = campaignRepository.findById(1L).get();
taskRepository.save(new Task(
"Sample Name",
"Sample Description",
LocalDate.of(2025, 01, 01),
campaign));
}
}
Run DomainEventsApp from the IDE to check this step. The callback logs:
Task Domain Events Published!
The log confirms dispatch to Spring’s event publisher. It does not confirm a commit or listener success.
2.3. Monitor Domain Events
A Spring listener can handle the event after the repository publishes it.
We’ll add CampaignEventListeners in com.baeldung.lsd.infrastructure.campaign.listener. Its method uses @TransactionalEventListener to react after a successful transaction:
@Component
public class CampaignEventListeners {
private static final Logger LOG = LoggerFactory.getLogger(CampaignEventListeners.class);
@TransactionalEventListener(classes = TaskUpdated.class)
public void taskUpdatedListener(TaskUpdated event) {
LOG.info("Task(id={}) Updated - Invoke Campaign Application Service Method to "
+ "Process the Event", event.getTaskId());
}
}
Run DomainEventsApp again to check the listener. The logs appear in this order:
Task Domain Events Published!
Task(id=5) Updated - Invoke Campaign Application Service Method to Process the Event
The callback logs first. The transactional listener then logs after the repository transaction commits.
The @TransactionalEventListener method runs after the transaction commits by default. A plain @EventListener would handle the event when it is published, even if an enclosing transaction later rolls back. Without an active transaction, the transactional listener is skipped by default.
We could enable asynchronous handling with @Async, but this example keeps the listener synchronous.
2.4. @DomainEvents Caveats
The repository publication mechanism has several limits.
The @DomainEvents annotation works through Spring Data repositories. Outside repository calls, we would need to publish events programmatically.
Changing a managed Task and letting Hibernate flush it does not trigger repository domain-event publication. We need a supported repository call that receives the aggregate, such as save() or delete(entity). The no-argument deleteAll() is not such a call.
These in-process events do not provide durable delivery or automatic retries. A failure before commit may roll back the transaction, while a failure in an after-commit listener occurs after the write has committed. Handling failure therefore depends on when it occurs and on any retry mechanism we add.
Spring Data also provides AbstractAggregateRoot to collect domain events on an aggregate. The Resources section links to further reading.
2.5. Benefits of Domain Events with Domain-Driven Design
Domain-Driven Design gives us terms such as aggregates, aggregate roots, bounded contexts, and domain events. The Resources section points to further reading on these concepts.
Eric Evans describes a domain event as a “representation of something that happened in the domain.” It records a change that matters to the model.
Domain events can support eventual consistency between aggregates. Each aggregate maintains its own consistency, while a listener can react to changes in another aggregate after publication.
Domain events can help keep aggregates loosely coupled. A handler can react to a change without putting that behavior in the aggregate itself. Integrating separate systems also requires a way to deliver events beyond this application.
A durable event store can support event sourcing, auditing, or reconstruction of state. The in-process publication shown here does not store a history of events.
2.6. DDD Domain Events and the @DomainEvents Annotation
We could publish events directly through Spring’s application event publisher. The @DomainEvents method instead lets the aggregate expose events without calling the publisher itself during supported repository operations.
Our example is not a full Domain-Driven Design model. In a larger design, Task and Campaign could be roots of separate aggregates, and a task event could trigger work for the campaign aggregate.
Local event handling is useful within this application. To reach other systems, a listener could forward events to a message queue, but that would require additional infrastructure and delivery handling.
3. Resources
- Spring Events
- DDD Aggregates and @DomainEvents
- Eric Evans – Domain-Driven Design: Tackling Complexity in the Heart of Software
- Vaughn Vernon – Implementing Domain-Driven Design
4. Conclusion
Repository-triggered domain events let an aggregate announce changes without coupling it directly to the code that handles them. The event publication shown here is local to the application. Delivering events to other systems requires additional infrastructure.