A quick introduction to CQRS and event sourcing and implementation in Axon
A typical CRUD architecture interacts with the system in the following ways:
This architecture is simple, versatile and widely accepted. However, in this architecture, the behavior of the domain model cannot be expressed well because it is a data-centric dialogue by sending and receiving DTO.
Applying CQRS and event sourcing can take the following form:
By applying CQRS and clearly separating the update model and the reference model, you can convey the user's intent as a command rather than a data-centric interaction. For example, what could only be described as "update user information" in the CRUD architecture can be expressed with a clearer intention, such as issuing a "change user's address" command. In addition, it will be possible to express whether the address change is a correction of a typo or a move. The domain model can express how it behaves by processing commands and generating events. Also, by applying event sourcing, it becomes the current state by saving "what happened (event) itself" instead of saving "state of the result of something happening" like CRUD. In addition to being able to retain the background and reasons for this, the components can be loosely connected via events, making it highly expandable.
Axon
Axon is a framework based on CQRS and event sourcing. The version introduced this time is 2.4.5. The architecture of Axon is shown in the figure below.
The state change for the application starts with Command. Processed by the CommandHandler to change the state of the domain object (Aggregate). And the domain event is generated by the state change of the domain object. Events that occur on domain objects are persisted in the event store through the repository. Events are also dispatched through EventBus, where event handlers update the data source used for queries and send messages to external systems.
Let's see how to implement it in Axon using a simple application that only registers and marks tasks as an example.
Command is an object that expresses the intention for the application and has the data necessary for processing based on that intention. Event is an object that represents what happened in your application. The command and event created by Todo are as follows.
public class CreateToDoItemCommand {
@TargetAggregateIdentifier
private final String todoId;
private final String description;
public CreateToDoItemCommand(String todoId, String description) {
this.todoId = todoId;
this.description = description;
}
public String getTodoId() {
return todoId;
}
public String getDescription() {
return description;
}
}
public class ToDoItemCreatedEvent {
private final String todoId;
private final String description;
public ToDoItemCreatedEvent(String todoId, String description) {
this.todoId = todoId;
this.description = description;
}
public String getTodoId() {
return todoId;
}
public String getDescription() {
return description;
}
}
@TargetAggregateIdentifier
indicates the field (or method) used to identify the target Aggregate instance.
Similarly, create a Command and Event to mark the completion.
public class MarkCompletedCommand {
@TargetAggregateIdentifier
private final String todoId;
public MarkCompletedCommand(String todoId) {
this.todoId = todoId;
}
public String getTodoId() {
return todoId;
}
}
public class ToDoItemCompletedEvent {
private final String todoId;
public ToDoItemCompletedEvent(String todoId) {
this.todoId = todoId;
}
public String getTodoId() {
return todoId;
}
}
The domain model in Axon behaves as an Aggregate that receives a Command, changes the state, and issues an Event to it. The implementation of ToDoItem that represents ToDo is as follows.
public class ToDoItem extends AbstractAnnotatedAggregateRoot {
@AggregateIdentifier
private String id;
private String description;
private boolean completed;
public ToDoItem() {
}
@CommandHandler
public ToDoItem(CreateToDoItemCommand command) {
apply(new ToDoItemCreatedEvent(command.getTodoId(), command.getDescription()));
}
@CommandHandler
public void markCompleted(MarkCompletedCommand command) {
apply(new ToDoItemCompletedEvent(id));
}
@EventSourcingHandler
public void on(ToDoItemCreatedEvent event) {
this.id = event.getTodoId();
this.desc = event.getDescription();
}
@EventSourcingHandler
public void on(ToDoItemCompletedEvent event) {
this.completed = true;
}
}
ʻAbstractAnnotatedAggregateRoot` provides functions such as event persistence, dispatch to EventBus, and initialization of domain object (Aggregate) based on the event stream obtained from the event store.
First, I want to create a new instance of ToDoItem
with CreateToDoItemCommand
, so @ CommandHandler
Create a constructor with. By calling apply () in the constructor, ToDoItemCreatedEvent
is issued and persisted in the event store. Also, the generated event is dispatched through EventBus to the event listener interested in ToDoItemCreatedEvent
.
Similarly, if you add a completion mark, you want to generate ToDoItemCompletedEvent
, so create a markCompleted
method.
When MarkCompletedCommand
is issued, markCompleted () is called for the ToDoItem instance where the event stream loaded from the event store is applied and the value is set.
Also, create an event handler with @EventSourcingHandler
to initialize the state of the instance when creating a ToDoItem instance.
You can easily perform actions on Events by creating an event listener. For example, write the current state of ToDoItem to the reference DB as shown below.
public class ToDoEventListener {
@EventHandler
public void handle(ToDoItemCreatedEvent event) {
//Update DB for reference
}
@EventHandler
public void handle(ToDoItemCompletedEvent event) {
//Update DB for reference
}
}
You can also add a function to notify the completion as follows.
public class ToDoEventNotifyListener {
@EventHandler
public void handle(ToDoItemCompletedEvent event) {
//Notify the completion of ToDo
}
}