[POC] Add snapshots
Here is a POC proposal for support of snapshots.
The Idea: I introduced an optional interface that informs that the Entity supports snapshots:
type EntityWithSnapshots[T any] interface {
Entity[T]
// Snapshot returns a Snapshot representing the current state of the Entity.
Snapshot() Snapshot[T]
}
The snapshot is just an interface that devs will need to implement.
// Snapshot is an Event that stores and applies the current state back to the Entity.
type Snapshot[T any] interface {
// ApplyTo applies the snapshot to the entity.
ApplyTo(*T) error
// SnapshotName should identify the snapshot and the version of its schema.
SnapshotName() string
}
As part of the event store implementation, I started with an in-memory event store, and it could be later easily propagated to other databases. The idea is pretty simple:
- When we save the entity, we just look into the current version of the last event. If it is time to make a snapshot and the Entity implements the interface, we just create it and store it with the current version in a separate collection of snapshots.
- When we load the entity, we start by looking for the last snapshot created. If it exists we just load events with versions higher than the snapshot version. Eventually, we apply the snapshot and the later events to the empty Entity.
Testing:
I created a new _example with a Counter entity to prove the POC works.
Here it is visible how simple it is to implement the snapshot support:
type Snapshot struct {
ID string
CurrentValue int
}
func (s Snapshot) SnapshotName() string {
return "CounterSnapshot_v1"
}
func (s Snapshot) ApplyTo(c *Counter) error {
c.id = s.ID
c.currentValue = s.CurrentValue
return nil
}
...
func (c Counter) Snapshot() esja.Snapshot[Counter] {
return Snapshot{
ID: c.id,
CurrentValue: c.currentValue,
}
}
@krzysztofreczek I took a look at the implementation and the idea is brilliant in its simplicity. However, I would prefer to have a separate interface for the snapshot event, something like:
type SnapshotEvent[T any] interface {
SnapshotName() string
ApplyTo(*T) error
}
This would definitely save me confusion and make the API a bit more intuitive in case you read the code for the first time. At the same time, the implementation would be more robust. With the current approach, you allow to do weird things (see the code below) because from the domain perspective there is no difference between events and snapshots.
// you allow to do this - it is ok :)
func (c Counter) Snapshot() esja.Snapshot[Counter] {
return Snapshot{
ID: c.id,
CurrentValue: c.currentValue,
}
}
// and you allow to do this - it is weird! :(
func (c Counter) Snapshot() esja.Snapshot[Counter] {
return Created{
ID: c.id,
}
}
@krzysztofreczek I took a look at the implementation and the idea is brilliant in its simplicity. However, I would prefer to have a separate interface for the snapshot event, something like:
type SnapshotEvent[T any] interface { SnapshotName() string ApplyTo(*T) error }This would definitely save me confusion and make the API a bit more intuitive in case you read the code for the first time. At the same time, the implementation would be more robust. With the current approach, you allow to do weird things (see the code below) because from the domain perspective there is no difference between events and snapshots.
// you allow to do this - it is ok :) func (c Counter) Snapshot() esja.Snapshot[Counter] { return Snapshot{ ID: c.id, CurrentValue: c.currentValue, } } // and you allow to do this - it is weird! :( func (c Counter) Snapshot() esja.Snapshot[Counter] { return Created{ ID: c.id, } }
@kamy22 , @m110 also pointed that out. Thank You. I will try to separate Snapshots from Events somehow. 👍
@kamy22 @m110 , please check the results of Event/Snapshot separation: https://github.com/ThreeDotsLabs/esja/pull/30
@krzysztofreczek LGTM 💪
@vinitius JFYI, there will be snapshots in esja <3