1. Overview
In this article, we’ll be looking at the Axon framework and how it helps us implement an architecture based on CQRS (Command Query Responsibility Segregation) and potentially Event Sourcing.
Note that a lot of these concepts come right out of DDD, which is beyond the scope of this current article.
2. Maven Dependencies
Before we start creating out sample application, we need to add the axon-core and axon-test dependencies into our pom.xml:
<dependency> <groupId>org.axonframework</groupId> <artifactId>axon-core</artifactId> <version>${axon.version}</version> </dependency> <dependency> <groupId>org.axonframework</groupId> <artifactId>axon-test</artifactId> <version>${axon.version}</version> <scope>test<scope> </dependency> <properties> <axon.version>3.0.2</axon.version> </properties>
3. Message Service – Commands
With the initial goal of doing CQRS in out system, we’ll define two types of actions that the user can perform:
- create a new text message
- mark text message as read
Naturally these will be two commands that model our domain – CreateMessageCommand and MarkReadMessageCommand:
public class CreateMessageCommand { @TargetAggregateIdentifier private String id; private String text; public CreateMessageCommand(String id, String text) { this.id = id; this.text = text; } // ... } public class MarkReadMessageCommand { @TargetAggregateIdentifier private String id; public MarkReadMessageCommand(String id) { this.id = id; } // ... }
The TargetAggregateIdentifier annotation tells Axon that the annotated field is an id of the given aggregate. We’ll briefly touch on aggregates soon.
4. Events
Our aggregate will be reacting to the above-created commands by producing MessageCreatedEvent and MessageReadEvent events:
public class MessageCreatedEvent { private String id; private String text; // standard constructors, getters, setters } public class MessageReadEvent { private String id; // standard constructors, getters, setters }
5. Aggregates – Producing Events on Commands
Now that we’ve modeled our commands, we need to create handlers that will produce events for commands.
Let’s create an aggregate class:
public class MessagesAggregate { @AggregateIdentifier private String id; @CommandHandler public MessagesAggregate(CreateMessageCommand command) { apply(new MessageCreatedEvent(command.getId(), command.getText())); } @EventHandler public void on(MessageCreatedEvent event) { this.id = event.getId(); } @CommandHandler public void markRead(MarkReadMessageCommand command) { apply(new MessageReadEvent(id)); } // standard constructors }
Each aggregate needs to have an id field, and we specify this by using an AggregateIdentifier annotation.
Our aggregate is created when CreateMessageCommand arrives – receiving that command will produce a MessageCreatedEvent.
At this point, the aggregate is in the messageCreated state. When the MarkReadMessageCommand arrives, the aggregate is producing a MessageReadEvent.
6. Testing our Setup
Firstly, we need to set up our test by creating a FixtureConfiguration for the MessagesAggregate:
private FixtureConfiguration<MessagesAggregate> fixture; @Before public void setUp() throws Exception { fixture = new AggregateTestFixture<MessagesAggregate>(MessagesAggregate.class); }
The first test case should cover the simplest situation – when the CreateMessageCommand arrives in our aggregate, it should produce the MessageCreatedEvent:
String eventText = "Hello, how is your day?"; String id = UUID.randomUUID().toString(); fixture.given() .when(new CreateMessageCommand(id, eventText)) .expectEvents(new MessageCreatedEvent(id, eventText));
Next, we will test a situation when aggregate already produced MessageCreatedEvent and the MarkReadMessageCommand arrives. It should produce a MessageReadEvent:
String id = UUID.randomUUID().toString(); fixture.given(new MessageCreatedEvent(id, "Hello")) .when(new MarkReadMessageCommand(id)) .expectEvents(new MessageReadEvent(id));
7. Putting Everything Together
We created commands, events, and aggregates. To start our application we need to glue everything together.
First, we need to create a command bus to which commands will be sent:
CommandBus commandBus = new SimpleCommandBus(); CommandGateway commandGateway = new DefaultCommandGateway(commandBus);
Next, we need to setup a message bus to which produced events will be sent:
EventStore eventStore = new EmbeddedEventStore(new InMemoryEventStorageEngine()); EventSourcingRepository<MessagesAggregate> repository = new EventSourcingRepository<>(MessagesAggregate.class, eventStore);
Events should be persistent, so we need to define a repository for storing them.
In this simple example, we’re storing events in memory. In a production system, it should of course be a database or some other type of persistence store.
If we’re doing Event Sourcing, the EventStore is the central component of the architecture. All events produced by the aggregate need to be persisted into to store to keep a master record of all change in the system.
Events are immutable, so once they are saved in the event store, they can not be modified or deleted. Using events we can recreate a state of a system to any point in the time, taking all events that were produced to this specific point in time.
Before starting the application we need to set up the aggregate that will be handling commands and producing events:
AggregateAnnotationCommandHandler<MessagesAggregate> handler = new AggregateAnnotationCommandHandler<MessagesAggregate>( MessagesAggregate.class, repository); handler.subscribe(commandBus);
The last thing we should declare is a handler that will subscribe to the events produced by the aggregate.
We’ll create a message event handler that will handle both messages – the MessageCreatedEvent and the MessageReadEvent:
public class MessagesEventHandler { @EventHandler public void handle(MessageCreatedEvent event) { System.out.println("Message received: " + event.getText() + " (" + event.getId() + ")"); } @EventHandler public void handle(MessageReadEvent event) { System.out.println("Message read: " + event.getId()); } }
We need to define that this listener should handle events by invoking a subscribe() method:
AnnotationEventListenerAdapter annotationEventListenerAdapter = new AnnotationEventListenerAdapter(new MessagesEventHandler()); eventStore.subscribe(eventMessages -> eventMessages.forEach(e -> { try { annotationEventListenerAdapter.handle(e); } catch (Exception e1) { throw new RuntimeException(e1); } }));
We completed the setup, so now we can send some commands to the commandGateway:
String itemId = UUID.randomUUID().toString(); commandGateway.send(new CreateMessageCommand(itemId, "Hello, how is your day?")); commandGateway.send(new MarkReadMessageCommand(itemId));
After running our application, the MessagesEventHandler should handle events produced by the MessagesAggregate class, and we should see similar output:
Message received: Hello, how is your day? (d2ba9cbe-1a44-428e-a710-13b1bdc67c4b) Message read: d2ba9cbe-1a44-428e-a710-13b1bdc67c4b
8. Conclusion
In this article we introduced the Axon framework as a powerful base to building a CQRS and Event Sourcing system architecture.
We implemented a simple message application using the framework – to show how that should be structured in practice.
The implementation of all these examples and code snippets can be found over on GitHub; this is a Maven project, so it should be easy to import and run as it is.