Snapshots, Indexing, and Archiving#

Snapshots#

Snapshots accelerate state loading; events remain authoritative. Executors save snapshots opportunistically when there is no snapshot or when trailing event data exceeds SnapshotRatio relative to the stored snapshot.

Set TrimEvents when retained events before a snapshot are unnecessary:

store, err := backend.NewStore(timebox.Config{
    TrimEvents:    true,
    SnapshotRatio: 1.0,
})

Trimming happens with snapshot persistence. Do not enable it when the complete live event history must remain queryable.

Status and Tag Indexing#

An indexer derives metadata from the same event batch being appended:

func orderIndexer(events []*timebox.Event) []*timebox.Index {
    indexes := make([]*timebox.Index, 0, len(events))
    for _, event := range events {
        switch event.Type {
        case OrderCreated:
            indexes = append(indexes, &timebox.Index{
                Status: new("active"),
            })
        case OrderDelivered:
            indexes = append(indexes, &timebox.Index{
                Status: new("delivered"),
            })
        }
    }
    return indexes
}

store, err := backend.NewStore(timebox.Config{Indexer: orderIndexer})

The last non-nil status wins. Tag values add (true) or remove (false) membership. Query the derived indexes through:

status, err := store.GetAggregateStatus(orderID)
active, err := store.ListAggregatesByStatus("active")
priority, err := store.ListAggregatesByTag("priority")
orders, err := store.ListAggregates("order")

A workflow service can use an indexer to turn started and deactivated events into queryable status and tag records.

Archiving#

Memory, Redis, and Raft can move a snapshot and event history into archive storage, then remove the live aggregate and its indexes:

if err := store.Archive(orderID); err != nil {
    return err
}

Consume one archive record with a context deadline:

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

err := store.ConsumeArchive(ctx,
    func(ctx context.Context, record *timebox.ArchiveRecord) error {
        return writeArchive(ctx, record)
    },
)

Consumption is at least once, so handlers must be idempotent. Archiving is one-way; Timebox does not restore archived aggregates.