1. Introduction

Memento is a Groovy library which can be used to implement an Event Sourcing solution. Memento tries to provide a high level abstraction for different event sourcing concepts such as:

  • Aggregate: Is the subject of a group of events.

  • Event: Something that expresses a change in a specific aggregate.

  • EventStore: Stores and retrieves all aggregates events, and it’s capable of restore the state of an aggregate

2. Getting started

The following example presents a Document aggregate:

  • The Document can support different types of events such as creating a document (Created) or appending text to the document (Appended).

  • Also an EventStore implementation is created to store the aggregate events. This particular implementation stores events and snapshots as csv files.

save an aggregate
@Grab('com.github.grooviter:memento-csv:0.1.0')
import memento.*
import memento.model.*
import groovy.transform.*
import static java.util.UUID.randomUUID

// EVENTS... (1)
@TupleConstructor
class Created extends Event<Document> {
    String title, author
}

@TupleConstructor
class Appended extends Event<Document> {
    String content
}

// AGGREGATE... (2)
@InheritConstructors
class Document extends Aggregate {
    String title, author, content = \'\'
    // EVENT_CONFIGURATION... (3)
    @Override
    void configure() {
        // applies all matching event properties to Aggregate
        bind(Created)
        // applies event to Aggregate as defined in closure
        bind(Appended) { Document doc, Appended event ->
            doc.content += event.content
        }
    }
}

// EVENTSTORE IMPLEMENTATION... (4)
EventStore eventStore = Memento.builder()
    .csvStorage('/tmp/events.csv', '/tmp/snapshots.csv')  // STORAGE
    .jacksonSerde()                                       // SERDE
    .snapshotThreshold(2) // snapshot every 2 events
    .onEvent(Object::println)                             // EVENTBUS
    .build()

// CREATING AND STORING AN AGGREGATE.... (5)
Document document = new Document(UUID.randomUUID())
    .apply(new Created("Memento", "Christopher Nolan"))
    .apply(new Appended("A man who, as a result of an injury,"))
    .apply(new Appended(", has anterograde amnesia"))
    .apply(new Appended("and has short-term memory loss"))
    .apply(new Appended("approximately every fifteen minutes."))

// SAVING THE AGGREGATE's EVENTS... (6)
eventStore.save(document)
1 The events used should extend the Event interface and have as generic value the class of the Aggregate
2 Creating an aggregate. Check the Aggregate section to know more
3 Configuring how the aggregate is going to apply the events. The void configure() {} method is a hook in the aggregate used to register how events are going to be applied to the aggregate. There are some built-in bind(…​) functions to avoid creating boilerplate code. For example if Created has a title property, the bind(class) method when applying the Created event to the Document aggregate, it will set the aggregate title property with the Created event title property.
4 Configuring Memento engine responsible for storing aggregate events and snapshots. Is always mandatory to configure at least the storage, serialization and event bus you’re going to use. By default we are using a csv file storage to store both events and snapshots. Creating snapshots every two events. And the even bus we’ve configured is just printing out the events inserted in the event store.
5 Creating an aggregate and adding events to it
6 Storing the aggregate state to the configured memento engine.

If you take a look at the events.csv file you will see the following.

events.csv
...-6740974752dc|1|Created|{"title":"Memento","aggregateId":"5f5469ba-a031-4ccb...
...-6740974752dc|2|Appended|{"content":"A man who, as a result of an injury","aggregate...
...

3. Usage

In order to use Memento in your Gradle project:

Gradle
repositories {
    mavenCentral()
}

dependencies {
    // ONLY if you'd like to implement your own event store
    implementation 'com.github.grooviter:memento-base:$VERSION'

    // ONLY if you'd like to store events in CSV files
    implementation 'com.github.grooviter:memento-csv:$VERSION'

    // ONLY if you'd planning to use Micronaut's data JDBC as event storage
    implementation 'com.github.grooviter:memento-micronaut-jdbc:$VERSIONf'
}

Or as we mentioned in the getting started section use it in your Groovy scripts directly:

Groovy Script
@Grab('com.github.grooviter:memento-csv:$VERSION')
// @Grab('com.github.grooviter:memento-base:$VERSION')
// @Grab('com.github.grooviter:memento-micronaut-jdbc:$VERSION')
import memento.*

// your code here

4. Aggregate

An aggregate in Memento is a class extending memento.Aggreggate:

Aggregate
class MyAggregate extends Aggregate {
  String clientId
}

4.1. Creating

Before applying any event to the aggregate, we need to create an instance of it. The only attribute required up front is the aggregate property id. All events applied to that aggregate will be related to it via the aggregate id.

For example, a possible way to do it is to create a static method:

Create Aggregate (static)
import memento.model.*

class Delivery extends Aggregate {
    String clientId

    static Delivery create() {
        return new Delivery(id: UUID.randomUUID())
    }
}
def delivery = Delivery.create()
// ...and then apply events to the instance

or maybe using a constructor:

Create Aggregate (Constructor)
import memento.model.*

class Delivery extends Aggregate {
    String clientId

    Delivery(UUID id) {
       super(id)
    }
}
def delivery = new Delivery(UUID.randomUUID())
// ...and then apply events to the instance

Of course we can short it with Groovy’s @InheritConstructors:

Create Aggregate (@InheritConstructors)
import memento.model.*
import groovy.transform.*

@InheritConstructors()
class Delivery extends Aggregate {
    String clientId
}
def delivery = new Delivery(UUID.randomUUID())
// ...and then apply events to the instance

4.2. Adding events

Once we’ve got an aggregate instance we can add events to it. An aggregate represents a domain context where a set of events will be applied. For example, imagine an events over the aggregate Delivery such as:

  • REQUESTED: a client has bought something and has triggered a new delivery

  • RECEIVED: once the purchase arrives to the client’s address the delivery received event is applied

The aggregate model could be modelled as:

Adding events
package memento.guide.aggregate.nobind

interface DeliveryProcess {
    void requested(String clientId)
    void received()
}

And the events we’d like to use. Please notice how the name of the events properties match the aggregate properties, that will be important later on:

DeliveryRequested
package memento.guide.aggregate.nobind

import memento.model.Event

class DeliveryRequested extends Event<Delivery> {
    String clientId
    Date deliveryRequestedAt = new Date()
}
DeliveryReceived
package memento.guide.aggregate.nobind

import memento.model.Event

class DeliveryReceived extends Event<Delivery> {
    Date deliveryReceivedAt = new Date()
}

Now lets see how do we apply these events to the aggregate:

Aggregate#apply
package memento.guide.aggregate.nobind

import groovy.transform.InheritConstructors
import memento.model.Aggregate

@InheritConstructors
class Delivery extends Aggregate implements DeliveryProcess {
    String clientId
    Date deliveryRequestedAt, deliveryReceivedAt

    @Override
    void requested(String clientId) {
        this.apply(new DeliveryRequested(clientId: clientId))
    }

    @Override
    void received() {
        this.apply(new DeliveryReceived())
    }

    private void apply(DeliveryRequested requested) {
        super.apply(requested) (1)
        this.clientId = requested.clientId (2)
        this.deliveryRequestedAt = requested.deliveryRequestedAt
    }

    private void apply(DeliveryReceived received) {
        super.apply(received)
        this.deliveryReceivedAt = received.deliveryReceivedAt
    }
}
1 Triggers event versioning
2 Changes aggregate state

The original super.apply(event), only triggers event versioning. Event versioning means:

  • The applied event is given a version id so that we know which order the events happened.

  • The applied event is given a unique id to the event when applied to the aggregate.

Apart from these characteristics we’d like to update the state of the aggregate, that’s why we create as many overloaded apply methods as the number of events we’d like to apply to the aggregate. However is a little bit verbose and there are a couple of methods specifically created to avoid creating this extra apply methods, the bind(…​) methods explained in the next section.

Once you’ve got your aggregate you can use it and add your events. This is a Spock test example:

Adding events (no-bind)
    void 'creating events'() {
        setup:
        Delivery delivery = new Delivery(UUID.randomUUID())

        when:
        delivery.requested("1000")

        then:
        delivery.eventList.size() == 1
        delivery.eventList[0].version == 1

        when:
        delivery.received()

        then:
        delivery.eventList.size() == 2
        delivery.eventList[1].version == 2
    }

4.3. Bind methods

These overloaded apply methods are pretty common that’s why there’s an alternative to avoid all that boilerplate code. If we reviewed the previous example, but now using the bind method:

Aggregate with bind methods
package memento.guide.aggregate.full

import groovy.transform.InheritConstructors
import memento.model.Aggregate

@InheritConstructors
class Delivery extends Aggregate implements DeliveryProcess {
    String clientId
    Date deliveryRequestedAt, deliveryReceivedAt

    @Override
    void requested(String clientId) {
        this.apply(new DeliveryRequested(clientId: clientId))
    }

    @Override
    void received() {
        this.apply(new DeliveryReceived())
    }

    @Override
    void configure() { (1)
        bind(DeliveryRequested, DeliveryReceived) (2)
    }
}
1 The Aggregate class has a hook method Aggregate#configure() that should be used to register the functions telling the aggregate how to process incoming events
2 In order to avoid boilerplate code, the Aggregate#bind(…​) methods. Aggregate#bind(Class<Event) copies the properties matching the aggregate

Then event handing should work the same:

Adding events (bind)
    void 'creating events'() {
        setup:
        Delivery delivery = new Delivery(UUID.randomUUID())

        when:
        delivery.requested("1000")

        then:
        delivery.eventList.size() == 1
        delivery.eventList[0].version == 1

        when:
        delivery.received()

        then:
        delivery.eventList.size() == 2
        delivery.eventList[1].version == 2
    }

In this example both Delivery and Requested classes have a field named clientId. So by using:

bind(Event)
bind(Requested)

Every time the Requested event is applied to the Delivery instance it will copy the value from Requested#clientId to Delivery#clientId.

There’s a variant of this method that allows to handle an array of event types to be applied:

bind(Event…​)
bind(Requested, Received...)

There’s another method Aggregate#bind(Event, Closure) when the logic is more complex.

bind(Event, Closure)
bind(Requested) { Delivery delivery, Requested event ->
    delivery.clientId = event.clientId
}

The Closure params are the Aggregate type the event is going to be applied to, and the event type is going to be applied. Imagine we’ve got events with properties not matching the aggregate properties:

DeliveryRequested (custom)
package memento.guide.aggregate.bindcustom

import memento.model.Event

class DeliveryRequested extends Event<Delivery> {
    String userId
    Date requestedAt = new Date()
}
DeliveryReceived (custom)
package memento.guide.aggregate.bindcustom

import memento.model.Event

class DeliveryReceived extends Event<Delivery> {
    Date receivedAt = new Date()
}

Then you can match the state of the aggregate using the bind(Class, Closure) methods inside the void configure() method block:

Delivery (custom events)
package memento.guide.aggregate.bindcustom

import groovy.transform.InheritConstructors
import memento.model.Aggregate

@InheritConstructors
class Delivery extends Aggregate implements DeliveryProcess {
    String clientId
    Date deliveryRequestedAt, deliveryReceivedAt

    @Override
    void requested(String clientId) {
        this.apply(new DeliveryRequested(userId: clientId))
    }

    @Override
    void received() {
        this.apply(new DeliveryReceived())
    }

    @Override
    void configure() {
        bind(DeliveryRequested) { Delivery delivery, DeliveryRequested event ->
            delivery.clientId = event.userId
            delivery.deliveryRequestedAt = event.requestedAt
        }

        bind(DeliveryReceived) { Delivery delivery, DeliveryReceived event ->
            delivery.deliveryReceivedAt = event.receivedAt
        }
    }
}

There’s still something to comment on the bind(Class, Closure) method usage…​

Why Event<Aggregate> should match Aggregate ?

When using the bind(EventType, Closure) method, it’s important to use the Aggregate in the generics argument of the Event:

Use aggregate in event generics
// class EventType extends Event<AggregateType> {}
class Requested extends Event<Delivery> {}
class Received extends Event<Delivery> {}
class Loaded extends Event<Delivery> {}

// class AggregateType extends Aggregate {}
class Delivery extends Aggregate {}

If we forgot to add the generic argument, for example:

Forgot aggregate in event generics
// class EventType extends Event<AggregateType> {}
class Requested extends Event {}
class Received extends Event {}
class Loaded extends Event {}

// class AggregateType extends Aggregate {}
class Delivery extends Aggregate {}

When using the bind(Class, Closure) method the event type hint wouldn’t be recognized, so instead of seeing this in your IDE:

bind
bind(EventType) { AggregateType agg, EventType event ->
 // here the aggregate specific type is recognized
}

Your method would look more like the following:

bind
bind(EventType) { Aggregate agg, EventType event ->
 // here the aggregate specific type is not recognized
}

4.4. Design

So far you may have seen that an aggregate class could be design in different ways. We’d like to comment a little bit on every different design strategy.

4.4.1. Method chaining

Method chaining strategy could be suited for:

  • Situations you’d want every time an event is applied the updated aggregate is returned.

  • Proofs of concepts where you’d like to see how all possible events are going to be rendered or store in the underlying event store

  • Still your methods can apply validations before applying the events

Here we have an example with methods which always return the aggregate. See how by having these methods we can apply validation. In this case we’re using Groovy Contracts to make sure certain methods are not used without meeting certain conditions.

Chained Aggregate
package memento.guide.aggregate.design.chain

import groovy.contracts.Ensures
import groovy.transform.InheritConstructors
import memento.model.Aggregate

@InheritConstructors
class PatientCase extends Aggregate {
    String patientId
    String diagnosedByDoctorId
    String testApplied
    String prescribedDrug
    Date caseOpenedAt, caseClosedAt

    @Ensures({ patientId })
    static PatientCase opened(String patientId) {
        return new PatientCase(UUID.randomUUID()).apply(new CaseOpened(patientId))
    }

    PatientCase testApplied(String test) {
        return this.apply(new TestDone(test))
    }

    @Ensures({ testApplied })
    PatientCase diagnosisConfirmed(String doctorId) {
        return this.apply(new DiagnosisConfirmed(doctorId))
    }

    @Ensures({ diagnosedByDoctorId })
    PatientCase drugPrescribed(String drug) {
        return this.apply(new DrugApplied(drug))
    }

    @Ensures({ diagnosedByDoctorId && caseOpenedAt < caseClosedAt })
    PatientCase caseClosed() {
        return this.apply(new CaseClosed())
    }

    @Override
    void configure() {
        bind(CaseClosed, CaseOpened, DiagnosisConfirmed, DrugApplied, TestDone)
    }
}

This design allows using the aggregate this way:

Chained Aggregate usage
PatientCase patientCase = PatientCase.opened("U92323")
    .testApplied("X-Ray")
    .diagnosisConfirmed("doctor-10001")
    .drugPrescribed("drug-1110")
    .caseClosed()

eventStore.save(patientCase)

4.4.2. Void

Although method chaining is really appealing, but most of the time in real application we may face that our aggregate is created and updated in different moments in time, so we may find no use on chaining the method calls, there are also some motivations for using the void strategy:

  • Your aggregate wants to stick to an interface and that interface doesn’t know anything about the aggregate

  • You don’t want to return the aggregate every time an event is applied

  • Still your aggregate methods can apply validations before applying the events

Following the same example, lets say that the medical procedure is already defined in an interface:

Medical Procedure
package memento.guide.aggregate.design.voidtype

interface MedicalProcedure {
    void opened(String patientId)
    void testApplied(String test)
    void diagnosisConfirmed(String doctorId)
    void drugPrescribed(String drug)
    void caseClosed()
}

Then your aggregate may look like this:

Aggregate (void)
package memento.guide.aggregate.design.voidtype

import groovy.contracts.Ensures
import groovy.transform.InheritConstructors
import memento.model.Aggregate

@InheritConstructors
class PatientCase extends Aggregate implements MedicalProcedure {
    String patientId
    String diagnosedByDoctorId
    String testApplied
    String prescribedDrug
    Date caseOpenedAt, caseClosedAt

    @Ensures({ patientId })
    void opened(String patientId) {
        this.apply(new CaseOpened(patientId))
    }

    void testApplied(String test) {
        this.apply(new TestDone(test))
    }

    @Ensures({ testApplied })
    void diagnosisConfirmed(String doctorId) {
        this.apply(new DiagnosisConfirmed(doctorId))
    }

    @Ensures({ diagnosedByDoctorId })
    void drugPrescribed(String drug) {
        this.apply(new DrugApplied(drug))
    }

    @Ensures({ diagnosedByDoctorId && caseOpenedAt < caseClosedAt })
    void caseClosed() {
        this.apply(new CaseClosed())
    }

    @Override
    void configure() {
        bind(CaseClosed, CaseOpened, DiagnosisConfirmed, DrugApplied, TestDone)
    }
}

And then imagine you are going to update the aggregate in different REST calls. For instance the case will be opened in a post call and the subsequent put calls may reload it from the event store and update it:

Void type usage
// POST /api/case
PatientCase patientCase = new PatientCase(aggregateUUID)
patientCase.opened("patient-1000")
eventStore.save(patientCase)

// PUT /api/case/{UUID}/test
patientCase = eventStore.load(aggregateUUID, PatientCase).get()
patientCase.testApplied("X-Ray")
eventStore.save(patientCase)

// PUT /api/case/{UUID}/doctor
patientCase = eventStore.load(aggregateUUID, PatientCase).get()
patientCase.diagnosisConfirmed("doctor-1000")
eventStore.save(patientCase)

// PUT /api/case/{UUID}/drug
patientCase = eventStore.load(aggregateUUID, PatientCase).get()
patientCase.drugPrescribed("drug-1000")
eventStore.save(patientCase)

// PUT /api/case/close
patientCase = eventStore.load(aggregateUUID, PatientCase).get()
patientCase.caseClosed()
eventStore.save(patientCase)

4.4.3. Raw apply

Nothing prevents you from using apply methods directly without creating any higher structure. Some scenarios for this could be:

  • You don’t want to write any more code than you’d think is necessary to apply events

  • Validations are done outside the aggregate

Of course up front the aggregate will be lighter:

Raw aggregate
package memento.guide.aggregate.design.raw

import groovy.transform.InheritConstructors
import memento.model.Aggregate

@InheritConstructors
class PatientCase extends Aggregate {
    String patientId
    String diagnosedByDoctorId
    String testApplied
    String prescribedDrug
    Date caseOpenedAt, caseClosedAt

    @Override
    void configure() {
        bind(CaseClosed, CaseOpened, DiagnosisConfirmed, DrugApplied, TestDone)
    }
}

And then only using the default Aggregate#apply(event) method:

Raw usage
PatientCase patientCase = new PatientCase(UUID.randomUUID())
    .apply(new CaseOpened())
    .apply(new TestDone("X-Ray"))
    .apply(new DiagnosisConfirmed("doctor-1000"))
    .apply(new DrugApplied("drug-1000"))
    .apply(new CaseClosed())

eventStore.save(patientCase)

5. Event

An event in Memento is a class extending memento.Event and it will be always be applied to a specific type of Aggregate:

Declaring Event
import memento.Event

class MyEvent extends Event<AggregateType> {
  // event properties here
}

The event then can be applied to the aggregate it belongs to. The events will be integrated in the aggregate by using the Aggregate#apply(event) method. Although this method can be used directly, normally we can create domain related methods:

Declaring Event
import memento.Event
import groovy.transform.InheritConstructors

class NameAdded extends Event<User> {
    String name
}

class AddressAdded extends Event<User> {
    String address
}

@InheritConstructors
class User extends Aggregate {
    String name, address

    static User create(String name) {
        return new User(UUID.randomUUID).apply(new NameAdded(name)
    }

    User mainAddress(String address) {
        return this.apply(new AddressAdded(address))
    }

    @Override
    void configure() {
        bind(NameAdded, AddressAdded)
    }
}

And then we can use a fluid method chaining if we want to:

fluid method chaining
User aggregate = User
    .create("John")
    .mainAddress("42 First Avenue")

Once we have our aggregate populated we may need to persist all the events in some event store. This is explained in the next section.

6. EventStore

In order to persist all the aggregate events we use an EventStore. A Memento EventStore instance is built on top of these three components:

  • STORAGE: were events and snapshots are going to be stored (database, csv…​)

  • EVENT-BUS:: system to notify when events/snapshots are stored

  • SERDE: (serialization/deserialization) how to serialize/deserialize events to/from the storage

To create an EventStore in Memento you can use Memento builder:

EventStore
import memento.*

EventStore eventStore = Memento.builder()
    .eventStorage(...) (1)
    .eventBus(...) (2)
    .serde(...) (3)
    .snapshotThreshold(2) (4)
    .build()
1 Declares where the events are going to be stored, it could a database, a csv file…​etc
2 Every time an event is applied to the aggregate the event could be send to an event bus if it’s set
3 Events are serialized and deserialized from the underlying event storage. Classes responsible for the serialization are handled by a implementation of the serde.
4 We can customize how many events should be persisted before a new version of a snapshot is generated.

For example, you can start storing your events with a CSV based event store CsvEventStorage and serializing events with Jackson JSON library:

CSV EventStore
import memento.*

EventStore eventStore = Memento.builder()
    .eventStorage(new CsvEventStorage(eventsFile, snapshotsFile))
    .onEvent(Object::println)
    .serde(new JacksonEventSerde())
    .build()

Because every module in Memento provides new extension methods to the Memento builder instance, you can re-write the previous example as the following:

CSV EventStore (extension methods)
import memento.*

EventStore eventStore = Memento.builder()
    .csvStorage(eventsFile, snapshotsFile)
    .onEvent(Object::println)
    .jacksonSerde()
    .build()

6.1. Storage

Basically where the events are going to be stored. That could be anything, a database, a csv file, queue systems. The only thing that matters is that the event information could be stored in such a way it could be later be used again to replay the state of the system.

The event structure is:

  • ID: id of the event

  • AGGREGATE ID: the id of the aggregate id the event belongs to

  • VERSION: version of the aggregate

  • JSON: the event payload

  • DATE: when the event happened

Memento has a default CSF file storage. You can persist events by using CSV storage:

CSV Store
import memento.*

EventStore eventStore = Memento.builder()
    .eventStorage(new CsvEventStorage(eventsFile, snapshotsFile))
    .onEvent(Object::println)
    .serde(new JacksonEventSerde())
    .build()

The eventsFile and snapshotsFile could be either a String with the path of the files, or instances of type File. You can check more in the implementations CSV section.

Apart from using CSV file there is another implementation using a database via the memento-micronaut-jdbc module. This module persist events in a database using Micronaut Data (JDBC). More information on this event storage implementation in section implementations Micronaut Data (jdbc)

6.2. Event Bus

Regarding that the event store is more used for reading than writing, there could be many systems that are interested in knowing when a new event has been stored in the system. In order to be able to publish notifications the event store uses an event bus. An event bus is a system that delivers messages from message producers to message receivers.

Any event bus added to Memento’s event store builder should implement memento.EventBusPort

package memento

import memento.model.Event

interface EventBusPort {
    void publish(Event event, EventSerdePort serdePort)
    void publishAsync(Event event, EventSerdePort serdePort)
}

Sometimes you may just one to check that events trigger event bus by using the Memento builder onEvent(Consumer<Event>):

Basic CSV EventStore
@Grab('com.github.grooviter:memento-csv:$VERSION')
import memento.*

EventStore eventStore = Memento.builder()
    .csvStorage('/tmp/events.csv', '/tmp/snapshots.csv')
    .snapshotThreshold(2)
    .onEvent { event -> println("event: $event") } (1)
    .build()
1 Every time a new event is persisted in the event store the onEvent method will be triggered

6.3. Serde

Because the payload of the event is JSON, we need to convert the event information to JSON and then re-create the object from that JSON. Serde is an acronym for SERialization/DEserialization.

6.3.1. Jackson

Memento provides a default SERDE mechanism using the Jackson JSON library. By default when persisting an event the qualified name of the class is persisted. For example:

Default
EventStore eventStore = Memento.builder()
    .csvStorage(eventsFile, snapshotsFile)
    .onEvent(Object::println)
    .jacksonSerde()
    .build()

When saving an aggregate instance, the persisted events will result in something like this:

Default output (CSV)
59e93fbb-...|9cbf9152-...|1|memento.guide.eventstore.events.TicketOpened|{"id":"..."}|"2024-11-05T18:48"

But you can create aliases in order to make the resulting JSON nicer:

Mappings
Mappings customMappings = Mappings.builder()
    .addMapping("TICKET_OPENED", TicketOpened)
    .addMapping("TICKET_INFO_FULFILLED", TicketInfoProvided)
    .addMapping("TICKET_INFO_CONFIRMED", TicketInfoConfirmed)
    .addMapping("TICKET_PAYMENT_INFO", TicketPaymentInfoProvided)
    .addMapping("TICKET_PAYMENT_SENT", TicketPaymentSent)
    .addMapping("TICKET_PAYMENT_CONFIRMED", TicketPaymentConfirmed)
    .addMapping("TICKET_PURCHASED", TicketPurchased)
    .build()

And then apply them to the serialization process.

EventStore
EventStore eventStore = Memento.builder()
    .csvStorage(eventsFile, snapshotsFile)
    .onEvent(Object::println)
    .jacksonSerde(customMappings)
    .snapshotThreshold(2)
    .build()

The persisted events will result in something like this:

Mappings output (CSV)
59e93fbb-...|9cbf9152-...|1|TICKET_OPENED|{"id":"..."}|"2024-11-05T18:48"

6.3.2. Custom Serde

You can provide your own serde mechanism by implementing the memento.EventSerdePort and pass it to memento event store builder serde(EventSerdePort):

Custom serde implementation
class YourImplementationSerde implements EventSerdePort {
    ...
}

EventStore eventStore = Memento.builder()
    .csvStorage('/tmp/events.csv', '/tmp/snapshots.csv')
    .serde(new YourImplementationSerde())
    .build()

7. Use cases

Event Sourcing is an alternative way to persist data. In contrast with state-oriented persistence that only keeps the latest version of the entity state, Event Sourcing stores each state mutation as a separate record called an event.
— Event Sourcing definition at eventstore.com

Every change is registered in an event log so that we can keep an audit trail. For instance imagine we’d like to keep track of our bank account deposits and withdrawals:

Ledger

We’re using our account id (AID), a version number to denote the order of events happening, the type of event, the value deposited or withdraw and finally when the event happened. Storing data this way enables among other things:

  • HISTORICAL REVIEW: we can re-create the state of our account at any given point in time

  • HETEROGENEOUS USES OF THE DATA: we can create different views of the data depending on our requirements

7.1. Historical Review

Marting Fowler highlights historical review features of event sourcing:

  • COMPLETE REBUILD: rebuild the application state from the event log

  • TEMPORAL QUERY: determine the application state in a given moment in time

  • EVENT REPLAY: the possibility to compute consequences of past events and maybe replay those events differently

Following our bank account example, I may want to know why my balance is at a given state, and the bank must be able to show every event until the current state so that there’s no doubt about the current balance. If the bank stored only the last state that information would be lost forever. However because we’ve stored every event we can answer that question:

Temporal Query

7.2. Heterogeneous Uses

Another use case is when the data the system is producing is being consumed in different ways by different systems.

For example, in an e-commerce application, different departments could be interested in different views of the data, accounting might be interested in sales whereas marketing could be interested in user fidelity. In such systems there are always more reads than writes.

An architectural pattern applied to this use case is CQRS (Command/Query Responsibility Segregation). This pattern uses the idea of using a different model for create information than the model used for reading information.

An event log could be used in this context to be the single source of truth of the system. From there any reading system could read from the event store and then create their own views to serve to their clients.

Heterogeneous reads

8. Implementations

8.1. CSV

8.1.1. Usage

Gradle
repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.github.grooviter:memento-csv:$VERSION'
}

8.1.2. Example

You can create a basic CSV backed EventStore by using the memento-csv module:

Basic CSV EventStore
@Grab('com.github.grooviter:memento-csv:$VERSION')
import memento.*

EventStore eventStore = Memento.builder()
    .csvStorage('/tmp/events.csv', '/tmp/snapshots.csv') (1)
    .snapshotThreshold(2) (2)
    .build()

This creates a new EventStore persisting both events and snapshots in csv files:

1 declares files where events and snapshots will be persisted (events.csv and snapshots.csv)
2 stores a new snapshot every 2 events

Event is stored as a new line in the csv file, and the event payload will be serialized as JSON

8.2. Micronaut Data (jdbc)

8.2.1. Usage

In order to use memento-micronaut-jdbc in your Gradle project:

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.github.grooviter:memento-micronaut-jdbc:$VERSION'
}

9. Samples

There are several examples in the Github Repository in the samples folder:

10. License

Apache 2.0

Memento is an open source project and its licensed under the Apache 2 License.