Snapshot Projections

Now that we know how snapshotting works and what a projection is, it is quite easy to put things together:

A SnapshotProjection is a Projection (read EventHandler) that can be stored into/created from a Snapshot. Lets go back to the example we had before:

/**
 *  maintains a map of UserId->UserName
**/
public class UserNames implements SnapshotProjection {

    private final Map<UUID, String> existingNames = new HashMap<>();

    @Handler
    void apply(UserCreated created) {
        existingNames.put(created.aggregateId(), created.userName());
    }

    @Handler
    void apply(UserDeleted deleted) {
        existingNames.remove(deleted.aggregateId());
    }
// ...

This projection is interested in UserCreated and UserDeleted EventObjects and can be serialized by the SnapshotSerializer.

Attention - when the projection class is changed (e.g. a field is renamed or its type is changed), there will be a problem with deserialization. For now, this can be solved by letting the projection implement Serializable and declare a static long serialVersionUID that is changed when fields change. There will be a better solution for this problem soon.

If you have worked with FactCast before, you'll know what needs to be done (if you haven't, just skip this section and be happy not to be bothered by this anymore):

  1. create an instance of the projection class, or get a Snapshot from somewhere
  2. create a list of FactSpecs (FactSpecifications) including the Specifications from UserCreated and UserDeleted
  3. create a FactObserver that implements an void onNext(Fact fact) method, that
    1. looks at the fact's namespace/type/version
    2. deserializes the payload of the fact into the right EventObject's instance
    3. calls a method to actually process that EventObject
    4. keeps track of facts being successfully processed
  4. subscribe to a fact stream according to the FactSpecs from above (either from Scratch or from the last factId processed by the instance from the snapshot)
  5. await the completion of the subscription to be sure to receive all EventObjects currently in the EventLog
  6. maybe create a snapshot manually and store it somewhere, so that you do not have to start from scratch next time

… and this is just the “happy-path”.

With Factus however, all you need to do is to use the following method:

    /**
     * If there is a matching snapshot already, it is deserialized and the
     * matching events, which are not yet applied, will be as well. Afterwards, a new
     * snapshot is created and stored.
     * <p>
     * If there is no existing snapshot yet, or they are not matching (see
     * serialVersionUID), an initial one will be created.
     *
     * @return an instance of the projectionClass in at least initial state, and
     *         (if there are any) with all currently published facts applied.
     */
    @NonNull
    <P extends SnapshotProjection> P fetch(@NonNull Class<P> projectionClass);

like

UserNames currentUserNames = factus.fetch(UserNames.class);

Easy, uh? As the instance is created from either a Snapshot or the class, the instance is private to the caller here. This is the reason why there is no ConcurrentHashMap or any other kind of synchronization necessary within UserNames.