This the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Snapshotting

In EventSourcing a Snapshot is used to memorize an object’s state at a certain point in the EventStream, so that when later-on this object has to be retrieved again, rather than creating a fresh one and use it to process all relevant events, we can start with the snapshot (that already has the state of the object from before) and just process all the facts that happened since.

It is easy to see that storing and retrieving snapshots involves some kind of marshalling and unmarshalling, as well as some sort of Key/Value store to keep the snapshots.

Snapshot Serialization

Serialization is done using a SnapshotSerializer.


public interface SnapshotSerializer {
    byte[] serialize(SnapshotProjection a);

    <A extends SnapshotProjection> A deserialize(Class<A> type, byte[] bytes);

    /**
     * @return displayable name of the serializer. Make sure it is unique, as it is used as part of
     *     the snapshot key
     */
    SnapshotSerializerId id();
}

As you can see, there is no assumption whether it produces JSON or anything, it just has to be symmetric. Factus ships with a default SnapshotSerializer, that - you can guess by now - uses Jackson. Neither the most performant, nor the most compact choice. Feel free to create one on your own or use provided optional modules like: factcast-factus-serializer-binary or factcast-factus-serializer-fury

Choosing serializers

If your SnapshotProjection does not declare anything different, it will be serialized using the default SnapshotSerializer known to your SnapshotSerializerSupplier (when using Spring boot, normally automatically bound as a Spring bean).

In case you want to use a different implementation for a particular ‘SnapshotProjection’, you can annotate it with ‘@SerializeUsing’

@SerializeUsing(MySpecialSnapshotSerializer.class)
static class MySnapshotProjection implements SnapshotProjection {
    //...
}

Note that those implementations need to have a default constructor and are expected to be stateless. However, if you use Spring boot those implementations can be Spring beans as well which are then retrieved from the Application Context via the type provided in the annotation.

Snapshot caching

The Key/Value store that keeps and maintains the snapshots is called a SnapshotCache.

Revisions

When a projection class is changed (e.g. a field is renamed or its type is changed), depending on the Serializer, there will be a problem with deserialization. In order to rebuild a snapshot in this case a “revision” is to be provided for the Projection. Only snapshots that have the same “revision” than the class in its current state will be used.

Revisions are declared to projections by adding a @ProjectionMetaData(revision = 1L) to the type.

1 - Snapshot Caching

The component responsible for storing and managing snapshots is called the SnapshotCache.

Factus does not provide a default SnapshotCache, requiring users to make an explicit configuration choice. If a SnapshotCache is not configured, any attempt to use snapshots will result in an UnsupportedOperationException.

By default, the SnapshotCache retains only the latest version of a particular snapshot.

There are several predefined SnapshotCache implementations available, with plans to introduce additional options in the future.

In-Memory SnapshotCache

For scenarios where persistence and sharing of snapshots are not necessary, and sufficient RAM is available, the in-memory solution can be used:


<dependency>
    <groupId>org.factcast</groupId>
    <artifactId>factcast-snapshotcache-local-memory</artifactId>
</dependency>

Refer to the In-Memory Properties for configuration details.

In-Memory and Disk SnapshotCache

To persist snapshots on disk, consider using the following configuration:


<dependency>
    <groupId>org.factcast</groupId>
    <artifactId>factcast-snapshotcache-local-disk</artifactId>
</dependency>

Note that this setup is designed for single-instance applications and handles file access synchronization within the active instance. It is not recommended for distributed application architectures.

Refer to the In-Memory and Disk Properties for more information.

Redis SnapshotCache

For applications utilizing Redis, the Redis-based SnapshotCache offers an optimal solution to implement a snapshotcache that can be shared between / used by many instances:


<dependency>
    <groupId>org.factcast</groupId>
    <artifactId>factcast-snapshotcache-redisson</artifactId>
</dependency>

This option supports multiple instances of the same application, making it suitable for distributed environments. By default, this cache automatically deletes stale snapshots after 90 days.

For further details, see the Redis Properties.

JDBC SnapshotCache

For applications utilizing a JDBC storage solution, the JDBC-based SnapshotCache offers an optimal solution:

<dependency>
    <groupId>org.factcast</groupId>
    <artifactId>factcast-snapshotcache-jdbc</artifactId>
</dependency>

This option also enables the use of multiple instances of the same application, facilitating distributed environments by leveraging the ACID properties of the databases. Additionally, the cache is configured by default to automatically purge stale snapshots after 90 days.

⚠️ Warning: This SnapshotCache assumes that you already created the needed table with the specified schema and will fail on startup when this condition is not met.

You can run one of the following SQL scripts to create the necessary table:

PostgreSQL

CREATE TABLE IF NOT EXISTS factcast_snapshot(
    projection_class VARCHAR(512),
    aggregate_id VARCHAR(36) NULL,
    last_fact_id VARCHAR(36),
    bytes BYTEA,
    snapshot_serializer_id VARCHAR(128),
    last_accessed VARCHAR,
    PRIMARY KEY (projection_class, aggregate_id));
CREATE INDEX IF NOT EXISTS factcast_snapshot_last_accessed_index ON factcast_snapshot(last_accessed);

MySQL & MariaDB

CREATE TABLE IF NOT EXISTS factcast_snapshot (
    projection_class VARCHAR(512) NOT NULL,
    aggregate_id VARCHAR(36) NULL,
    last_fact_id VARCHAR(36) NOT NULL,
    bytes BLOB,
    snapshot_serializer_id VARCHAR(128) NOT NULL,
    last_accessed VARCHAR(255),
    PRIMARY KEY (projection_class, aggregate_id)
);
CREATE INDEX factcast_snapshot_last_accessed_index ON factcast_snapshot (last_accessed);

Oracle

CREATE TABLE factcast_snapshot (
    projection_class VARCHAR2(512) NOT NULL,
    aggregate_id VARCHAR2(36) NULL,
    last_fact_id VARCHAR2(36) NOT NULL,
    bytes BLOB,
    snapshot_serializer_id VARCHAR2(128) NOT NULL,
    last_accessed VARCHAR2(255),
    PRIMARY KEY (projection_class, aggregate_id)
);
CREATE INDEX factcast_snapshot_last_accessed_index ON factcast_snapshot (last_accessed);

For further details, see the JDBC Properties.