4. Domain Modeling

In a CQRS-based application, a Domain Model (as defined by Eric Evans and Martin Fowler) can be a very powerful mechanism to harness the complexity involved in the validation and execution of state changes. Although a typical Domain Model has a great number of building blocks, two of them play a major role when applied to CQRS: the Event and the Aggregate.

The following sections will explain the role of these building blocks and how to implement them using the Axon Framework.

4.1. Events

The Axon Framework makes a distinction between three types of events, each with a clear use and type of origin. Regardless of their type, all events must implement the Event interface or one of the more specific sub-types, Domain Events, Application Events and System Events, each described in the sections below.

All events may carry data and meta-data. Typically, the data is added to each event as fields in the event implementation. Meta-data, on the other hand is stored separately. The Auditing interceptor uses this mechanism to attach meta-data to events for auditing purposes. All Axon's implementations of Events allow the subclasses to attach arbitrary information as meta-data.

[Note]Note

In general, you should not base business decisions on information in the meta-data of events. If that is the case, you might have information attached that should really be part of the event's regular data instead. Meta-data is typically used for auditing and tracing.

4.1.1. Domain Events

The most important type of event in any CQRS application is the domain event. It represents an event that occurs inside your domain logic, such as a state change or special notification of a certain state. The latter not being per definition a state change.

In the Axon Framework, all domain events should extend the abstract DomainEvent class. This abstract class keeps track of the aggregate they are generated by, and the sequence number of the event inside the aggregate. This information is important for the Event Sourcing mechanism, as well as for event handlers (see Section 6.2, “Event Listeners”) that need to know the origin of an event.

Although not enforced, it is good practice to make domain events immutable, preferably by making all fields final and by initializing the event within the constructor.

[Note]Note

Although Domain Events technically indicate a state change, you should try to capture the intention of the state in the event, too. A good practice is to use an abstract implementation of a domain event to capture the fact that certain state has changed, and use a concrete sub-implementation of that abstract class that indicates the intention of the change. For example, you could have an abstract AddressChangedEvent, and two implementations ContactMovedEvent and AddressCorrectedEvent that capture the intent of the state change. Some listeners don't care about the intent (e.g. database updating event listeners). These will listen to the abstract type. Other listeners do care about the intent and these will listen to the concrete subtypes (e.g. to send an address change confirmation email to the customer).

Adding intent to events

Figure 4.1. Adding intent to events


There is a special type of Event, which has a special meaning: the AggregateDeletedEvent. This is a marker interface that indicates a migration to a "deleted" state of the aggregate. Repositories must treat aggregates that have applied such an event as deleted. Hence, loading an aggregate that has an AggregateDeletedEvent results in an exception.

Snapshot events are instances of DomainEvent with a special intent. They are typically not dispatched via the event bus, but are used to summarize an arbitrary number of events from the past into a single entry. This can drastically improve performance when initializing an aggregate's state from a series of events. See Section 5.4, “Snapshotting” for more information about snapshot events and their use.

4.1.2. Application Events

Application events are events that cannot be categorized as domain events, but do have a significant importance for the application. When using application events, check if the event is actually a domain event that you overlooked. Examples of application events are the expiry of a user session, or the notification of an email being successfully sent. The usefulness of these events depend on the type of application you are creating.

In the Axon Framework, you can extend the abstract ApplicationEvent class for application events. This class will generate a unique identifier and a time stamp for the current event. Optionally, you can attach an object that acts as the source of the event. This source is loosely referenced, which means that if the garbage collector cleans up the source, or when the event is serialized and de-serialized, the original source class is not available anymore. Instead, you will have access to the type of source and the value of it's toString() method.

4.1.3. System Events

The third type of event identified by Axon Framework is the System Event. These events typically provide notifications of the status of the system. These events could, for example, indicate that a subsystem is non-responsive or has raised an exception.

All system events extend the abstract SystemEvent class. Upon construction of this event, you may pass an exception, defining the cause of the event, and a source object which is considered the source of the event. As with application events, the source is loosely referenced from the event.

4.2. Aggregate

An Aggregate is an entity or group of entities that is always kept in a consistent state. The aggregate root is the object on top of the aggregate tree that is responsible for maintaining this consistent state.

[Note]Note

The term "Aggregate" refers to the aggregate as defined by Evans in Domain Driven Design:

A cluster of associated objects that are treated as a unit for the purpose of data changes. External references are restricted to one member of the Aggregate, designated as the root. A set of consistency rules applies within the Aggregate's boundaries.

A more extensive definition can be found on: http://domaindrivendesign.org/freelinking/Aggregate.

For example, a "Contact" aggregate will contain two entities: contact and address. To keep the entire aggregate in a consistent state, adding an address to a contact should be done via the contact entity. In this case, the Contact entity is the appointed aggregate root.

In Axon, aggregates are identified by an AggregateIdentifier. There are two basic implementations of these identifiers: UUIDAggregateIdentifier, which used Java's UUID to generate random identifiers, and the StringAggregateIdentifier, which allows you to choose a String which should be used as identifier. You can choose any identifier type you like, and even create your own, as long as they have a valid String representation.

[Note]Note

It is considered a good practice to use randomly generated identifiers, as opposed to sequenced ones. Using a sequence drastically reduces scalability of your application, since machines need to keep each other up-to-date of the last used sequence numbers. The chance of collisions with a UUID is very slim (a chance of 10−15, if you generate 8.2 × 1011 UUIDs).

Furthermore, be careful when using functional identifiers for aggregates. They have a tendency to change, making it very hard to adapt your application accordingly.

4.2.1. Basic aggregate implementations

AggregateRoot

In Axon, all aggregate roots must implement the AggregateRoot interface. This interface describes the basic operations needed by the Repository to store and publish the generated domain events. However, Axon Framework provides a number of abstract implementations that help you writing your own aggregates.

[Note]Note

Note that only the aggregate root needs to implement the AggregateRoot interface or implement one of the abstract classes mentioned below. The other entities that are part of the aggregate do not have to implement any interfaces.

AbstractAggregateRoot

The AbstractAggregateRoot is a basic implementation that provides a registerEvent(DomainEvent) method that you can call in your business logic method to have an event added to the list of uncommitted events. The AbstractAggregateRoot will keep track of all uncommitted registered events and make sure they are forwarded to the event bus when the aggregate is saved to a repository.

AbstractJpaAggregateRoot

The AbstractJpaAggregateRoot is a JPA-compatible implementation of the AggregateRoot interface. It has the annotation necessary to persist the aggregate's state and reconstruct it from database tables. It uses the @Version annotation on one of it's field to perform optimistic locking on the database level. Similar to the AbstractAggregateRoot, the AbstractJpaAggregateRoot keeps track of uncommitted events, which have been registered using registerEvent(DomainEvent).

4.2.2. Event sourced aggregates

Axon framework provides a few repository implementations that can use event sourcing as storage method for aggregates. These repositories require that aggregates implement the EventSourcedAggregateRoot interface. As with most interfaces in Axon, we also provide one or more abstract implementations to help you on your way.

EventSourcedAggregateRoot

The interface EventSourcedAggregateRoot defines an extra method, initializeState(), on top of the AggregateRoot interface. This method initializes an aggregate's state based on an event stream.

AbstractEventSourcedAggregateRoot

The AbstractEventSourcedAggregateRoot implements all methods on the EventSourcedAggregateRoot interface. It defines an abstract handle() method, which you need to implement with the actual logic to apply state changes based on domain events. When you extend the AbstractEventSourcedAggregateRoot, you can register new events using apply(). This method will register the event to be committed when the aggregate is saved, and will call the handle() method with the event as parameter. You need to implement this handle() method to apply the state changes represented by that event. Below is a sample implementation of an aggregate.

public class MyAggregateRoot extends AbstractEventSourcedAggregateRoot {

    private String someProperty;

    public MyAggregateRoot() {
        apply(new MyAggregateCreatedEvent());
    }

    public MyAggregateRoot(UUID identifier) {
        super(identifier);
    }

    public void handle(DomainEvent event) {
        if (event instanceof MyAggregateCreatedEvent) {
            // do something with someProperty
        }
        // and more if-else-if logic here
    }
}                

AbstractAnnotatedAggregateRoot

As you see in the example above, the implementation of the handle() method can become quite verbose and hard to read. The AbstractAnnotatedAggregateRoot can help. The AbstractAnnotatedAggregateRoot is a specialization of the AbstractAggregateRoot that provides @EventHandler annotation support to your aggregate. Instead of a single handle() method, you can split the logic in separate methods, with names that you may define yourself. Just annotate the event handler methods with @EventHandler, and the AbstractAnnotatedAggregateRoot will invoke the right method for you.

[Note]Note

Note that @EventHandler annotated methods on an AbstractAnnotatedAggregateRoot are only called when events are applied directly to the aggregate locally. This should not be confused with annotating event handler methods on EventListener classes, in which case event handler methods handle events dispatched by the EventBus. See Section 6.2, “Event Listeners”.

public class MyAggregateRoot extends AbstractAnnotatedAggregateRoot {
    private String someProperty;

    public MyAggregateRoot() {
        apply(new MyAggregateCreatedEvent());
    }

    public MyAggregateRoot(UUID identifier) {
        super(identifier);
    }

    @EventHandler
    private void handleMyAggregateCreatedEvent(MyAggregateCreatedEvent event) {
        // do something with someProperty
    }
}                

In all circumstances, exactly one event handler method is invoked. The AbstractAnnotatedAggregateRoot will search the most specific method to invoke, in the following order:

  1. On the actual instance level of the class hierarchy (as returned by this.getClass()), all annotated methods are evaluated

  2. If one or more methods are found of which the parameter is of the event type or a super type, the method with the most specific type is chosen and invoked

  3. If no methods are found on this level of the class hierarchy, the super type is evaluated the same way

  4. When the level of the AbstractAnnotatedAggregateRoot is reached, and no suitable event handler is found, the event is ignored.

Event handler methods may be private, as long as the security settings of the JVM allow the Axon Framework to change the accessibility of the method. This allows you to clearly separate the public API of your aggregate, which exposes the methods that generate events, from the internal logic, which processes the events.

4.2.3. Complex Aggregate structures

Complex business logic often requires more than what an aggregate with only an aggregate root can provide. In that case, it important that the complexity is spread over a number of entities within the aggregate. When using event sourcing, not only the aggregate root needs to use event to trigger state transitions, but so does each of the entities within that aggregate.

Axon provides support for event sourcing in complex aggregate structures. All entities other than the aggregate root need to extend from AbstractEventSourcedEntity. The EventSourcedAggregateRoot implementations provided by Axon Framework are aware of these entities and will call their event handlers when needed.

When an entity (including the aggregate root) applies an Event, it is registered with the Aggregate Root. The aggregate root applies the event locally first. Next, it will evaluate all its fields for any implementations of AbstractEventSourcedEntity and handle the event on them. Each entity does the same thing to its fields.

To register an Event, the Entity must know about the Aggregate Root. Axon will automatically register the Aggregate Root with an Entity before applying any Events to it. This means that Entities (unlike usual in the Aggregate Root) should never apply an Event in their constructor. Non-Aggregate Root Entities should be created in an @EventHandler annotated method in their parent Entity. Axon will ensure that the Aggregate Root is properly registered in time.

Axon will automatically detect most of the child entities in the fields of an Entity (albeit aggregate root or not). The following Entities are found:

  • directly referenced in a field;

  • inside fields containing an Iterable (which includes all collections, such as Set, List, etc);

  • inside both they keys and the values of fields containing a java.util.Map

If you reference an Entity from any other location than the above mentioned, you can override the getChildEntities() method. This method should return a Collection of entities that should be notified of the Event. Note that each entity is invoked once for each time it is located in the returned Collection.