Aggregates and Events#
Aggregate IDs#
Every aggregate ID contains a type and key:
orderID := timebox.NewAggregateID("order", "ORD-123")Both components are always populated. For a concept with exactly one aggregate, use NewAggregateType; it fills the key with timebox.SingletonKey:
catalogID := timebox.NewAggregateType("catalog")Use this pattern for singleton concepts such as a catalog or cluster. Use NewAggregateID when a type has many instances, such as ("workflow", workflowID).
Event Types and Payloads#
Event type names are durable identifiers. Payloads are JSON encoded by Aggregator.Raise:
const OrderConfirmed timebox.EventType = "order.confirmed"
err := ag.Raise(OrderConfirmed, struct{}{})Use Event.GetValue when reading an event directly:
data, err := event.GetValue[OrderCreatedData]()Timebox caches a compatible decoded value, including the value supplied to a newly raised event.
Appliers#
An applier maps the previous state and one event to the next state. Prefer MakeApplier for typed payloads:
func orderCreated(
order Order, event *timebox.Event, data OrderCreatedData,
) Order {
order.CustomerName = data.CustomerName
order.CreatedAt = event.Timestamp
return order
}
var orderAppliers = timebox.Appliers[Order]{
OrderCreated: timebox.MakeApplier(orderCreated),
}Appliers run both while replaying stored history and immediately after Raise. They should not perform I/O or other side effects.
Post-Commit Actions#
Side effects belong after a successful commit. Register defaults when constructing an executor, or register a command-specific action through the aggregator:
ag.OnSuccess(func(order Order, events []*timebox.Event) {
publish(order, events)
})Success actions run only after persistence succeeds. Timebox recovers a panic in an action and logs it; the commit is already durable and is not rolled back.