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.
@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.
...-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:
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:
@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
:
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
:
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:
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
:
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:
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:
package memento.guide.aggregate.nobind
import memento.model.Event
class DeliveryRequested extends Event<Delivery> {
String clientId
Date deliveryRequestedAt = new Date()
}
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:
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:
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:
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:
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(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(Requested, Received...)
There’s another method Aggregate#bind(Event, Closure)
when the logic is more complex.
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:
package memento.guide.aggregate.bindcustom
import memento.model.Event
class DeliveryRequested extends Event<Delivery> {
String userId
Date requestedAt = new Date()
}
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:
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…
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.
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:
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:
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:
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:
// 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:
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:
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:
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:
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:
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:
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:
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:
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:
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>)
:
@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:
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:
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 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 = Memento.builder()
.csvStorage(eventsFile, snapshotsFile)
.onEvent(Object::println)
.jacksonSerde(customMappings)
.snapshotThreshold(2)
.build()
The persisted events will result in something like this:
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)
:
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.
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:

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:

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.

8. Implementations
8.1. CSV
8.1.1. Usage
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:
@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
9. Samples
There are several examples in the Github Repository in the samples folder: