5. Repositories and Event Stores

The repository is the mechanism that provides access to aggregates. The repository acts as a gateway to the actual storage mechanism used to persist the data. In CQRS, the repositories only need to be able to find aggregates based on their unique identifier. Any other types of queries should be performed against the query database, not the Repository.

In the Axon Framework, all repositories must implement the Repository interface. This interface prescribes two methods: load(identifier) and save(aggregate).

Depending on your underlying persistence storage and auditing needs, there are a number of base implementations that provide basic functionality needed by most repositories. Axon Framework makes a distinction between repositories that save the current state of the aggregate (see Section 5.1, “Standard repositories”), and those that store the events of an aggregate (see Section 5.2, “Event Sourcing repositories”).

5.1. Standard repositories

Standard repositories store the actual state of an Aggregate. Upon each change, the new state will overwrite the old. This makes it possible for the query components of the application to use the same information the command component also uses. This could, depending on the type of application you are creating, be the simplest solution. If that is the case, Axon provides some building blocks that help you implement such a repository.

Note that the Repository interface does not prescribe a delete(identifier) method. This is because not all types of repositories use that functionality. Of course, nothing witholds you from adding it to your repository implementation.

AbstractRepository

The most basic implementation of the repository is AbstractRepository. It takes care of the event publishing when an aggregate is saved. The actual persistence mechanism must still be implemented. This implementation doesn't provide any locking mechanism and expects the underlying data storage mechanism to provide it.

LockingRepository

If the underlying data store does not provide any locking mechanism to prevent concurrent modifications of aggregates, consider using the abstract LockingRepository implementation. Besides providing event dispatching logic, it will also ensure that aggregates are not concurrently modified.

You can configure the LockingRepository to use an optimistic locking strategy, or a pessimistic one. When the optimistic lock detects concurrent access, the second thread saving an aggregate will receive a ConcurrencyException. The pessimistic lock will prevent concurrent access to the aggregate alltogether.

5.2. Event Sourcing repositories

Aggregate roots that implement the EventSourcedAggregateRoot interface can be stored in an event sourcing repository. Those repositories do not store the aggregate itself, but the series of events generated by the aggregate. Based on these events, the state of an aggregate can be restored at any time.

EventSourcingRepository

The abstract EventSourcingRepository implementation provides the basic functionality needed by any event sourcing repository in the AxonFramework. It depends on an EventStore, which abstracts the actual storage mechanism for the events. See Section 5.3, “Event store implementations”.

The EventSourcingRepository has two abstract methods: getTypeIdentifier() and instantiateAggregate(identifier). The first is a value passed to the event store that provides information about the type of aggregate that the events relate to. A good starting point to use as return value is the simple name of a class (i.e. the fully qualified class name withouth the package name). The second method requires you to create an uninitialized instance of the aggregate using the given identifier. The repository will initialize this instance with the events obtained from the event store.

CachingEventSourcingRepository

Initializing aggregates based on the events can be a time-consuming effort, compared to the direct aggregate loading of the simple repository implementations. The CachingEventSourcingRepository provides a cache from which aggregates can be loaded if available. You can configure any jcache implementation with this repository. Note that this implementation can only use caching in combination with a pessimistic locking strategy.

5.3. Event store implementations

Event Sourcing repositories need an event store to store and load events from aggregates. Typically, event stores are capable of storing events from multiple types of aggregates, but it is not a requirement.

Axon provides two implementations of event stores, both are capable of storing all domain events (those that extend the DomainEvent class). These event stores use an EventSerializer to serialize and deserialize the event. By default, Axon provides an implementation of the Event Serializer that serializes events to XML: the XStreamEventSerializer.

FileSystemEventStore

The FileSystemEventStore stores the events in a file on the file system. It provides good performance and easy configuration. The only downside of this event store is that is does not provide transaction support and doesn't cluster very well. The only configuration needed is the location where the event store may store its files and the serializer to use to actually serialize and deserialize the events. Note that the provided url must end on a slash. This is due to the way Spring's Resource implementations work.

JpaEventStore

The JpaEventStore stores events in a JPA-compatible data source. Unlike the XStream version, the JPAEventStore supports transactions. The JPA event store can also load events based on their timestamps.

To use the JpaEventStore, you must have the javax.persistence annotations on your classpath. Furthermore, you should configure your persistence context (defined in META-INF/persistence.xml file) to contain the classes org.axonframework.eventstore.jpa.DomainEventEntry and org.axonframework.eventstore.jpa.SnapshotEventEntry.

Below is an example configuration of a persistence context configuration:

<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
    <persistence-unit name="eventStore"(1) transaction-type="RESOURCE_LOCAL">
        <class>org...eventstore.jpa.DomainEventEntry</class> (2)
        <class>org...eventstore.jpa.SnapshotEventEntry</class>
    </persistence-unit>
</persistence>
1

In this sample, there is is specific persistence unit for the event store. You may, however, choose to add the third line to any other persistence unit configuration.

2

This line registers the DomainEventEntry (the class used by the JpaEventStore) with the persistence context.

Implementing your own event store

If you have specific requirements for an event store, it is quite easy to implement one using different underlying data sources. Reading and appending events is done using a DomainEventStream, which is quite similar to iterator implementations.

[Tip]Tip

The SimpleDomainEventStream class will make the contents of a sequence (List or array) of DomainEvent instances accessible as event stream.

Influencing the serialization process

Event Stores need a way to serialize the Domain Event to prepare it for storage. By default, Axon uses the XStreamEventSerializer, which uses XStream (see xstream.codehaus.org) to serialize Domain Events into XML and vice versa. XStream is very fast and is more flexible than Java Serialization. For example, if you remove a field from a class, you can still deserialize instances from that class using the old XML. XStream will simply ignore that field. Furthermore, the result of XStream serialization is human readable. Quite useful for logging and debugging purposes.

The XStreamEventSerializer can be configured. You can define aliases it should use for certain packages, classes or even fields. Besides being a nice way to shorten potentially long names, aliases can also be used when class definitions of event change. For more information about aliases, visit the XStream website: xstream.codehaus.org.

You may also implement your own Event Serializer, simply by creating a class that implements EventSerializer, and configuring the Event Store to use that implementation instead of the default.

5.4. Using Snapshot Events

When aggregates live for a long time, and their state constantly change, they will generate a large amount of events. Having to load all these events in to rebuild an aggregate's state may have a big performance impact. The snapshot event is a domain event with a special purpose: it summarises an arbitrary amount of events into a single one. By regularly creating and storing a snapshot event, the event store does not have to return long lists of events. Just the last snapshot events and all events that occurred after the snapshot was made.

For example, items in stock tend to change quite often. Each time an item is sold, an event reduces the stock by one. Every time a shipment of new items comes in, the stock is incremented by some larger number. If you sell a hundred items each day, you will produce at least 100 events per day. After a few days, your system will spend too much time reading in all these events just to find out wheter it should raise an "ItemOutOfStockEvent". A single snapshot event could replace a lot of these events, just by storing the current number of items in stock.

Storing Snapshot Events

Both the JpaEventStore and the FileSystemEventStore are capable of storing snapshot events. They provide a special method that allows a DomainEvent to be stored as a snapshot event. You have to initialize the snapshot event completely, including the aggregate identifier and the sequence number. There is a special constructor on the DomainEvent for this purpose. The sequence number must be equal to the sequence number of the last event that was included in the state that the snapshot represents. In most cases, you can use the getLastCommittedEventSequenceNumber() on the VersionedAggregate (which each event sourced aggregate implements) to obtain the sequence number to use in the snapshot event.

When a snapshot is stored in the Event Store, it will automatically use that snapshot to summarize all prior events and return it in their place. Both event store implementations allow for concurrent creation of snapshots. This means they allow snapshots to be stored while another process is adding Events for the same aggregate. This allows the snapshotting process to run as a separate process alltogether.

[Note]Note

Normally, you can archive all events once they are part of a snapshot event. Snapshotted events will never be read in again by the event store in regular operational scenario's. However, if you want to be able to reconstruct aggregate state prior to the moment the snapshot was created, you must keep the events up to that date.

Triggering snapshot creation

Snapshot creation can be triggered by a number of factors, for example the number of events created since the last snapshot, the time to initialize an aggregate exceeds a certain threshold, time-based, etc. Currently, Axon does not provide a triggering mechanism (yet).

However, Axon does provide an interface that instances that produce snapshots should implement: SnapshotProducer. Typically, this interface is implemented by an aggregate root, since that is typically the only object that has full access to the aggregate's full state information.

Initializing an aggregate based on a Snapshot Event

A snapshot event is just a regular DomainEvent. That means a snapshot event is handled just like any other domain event. When using annotations to demarcate event handers (@EventHandler), you can annotate a method that initializes full aggregate state based on a snapshot event. The code sample below shows how snapshot events are treated like any other domain event within the aggregate.

public class MyAggregate extends AbstractAnnotatedAggregateRoot {

    // ... code omitted for brevity

    @EventHandler
    protected void handleSomeStateChangeEvent(MyDomainEvent event) {
        // ...
    }

    @EventHandler
    protected void applySnapshot(MySnapshotEvent event) {
        // the snapshot event should contain all relevant state
        this.someState = event.someState;
        this.otherState = event.otherState;
    }
}