Projections and subscriptions
This page explains the two kinds of handler and the guarantee each one gives.
| Projection | Subscription | |
|---|---|---|
| Writes to | The same database | Anything: email, HTTP, other systems |
| Checkpoint | In the same transaction as its writes | After the handler succeeds |
| Guarantee | Each event’s effect happens exactly once | Each event arrives at least once |
| Idempotency key | Not needed | EventId, or StreamId with Version |
| Run modes | Inline or async | Async only |
A projection can promise exactly once because its writes and its checkpoint commit together. A subscription cannot: an email can be sent and the process can die before the checkpoint moves. Pass the event ID on as an idempotency key.
// A subscription does anything outside the database. Delivery is at least once,// so pass the event ID on as an idempotency key.public sealed class SendReceipt : Subscription{ // Subscriptions are created once; a scoped service comes from ctx.Services instead. public SendReceipt(IEmailSender email) => On<CheckedOut>((_, ctx) => email.SendReceipt(ctx.Envelope.StreamId, ctx.Envelope.EventId, ctx.CancellationToken));}When a handler fails
Section titled “When a handler fails”- The runner retries the event with growing delays, up to
HandlerRetriestimes. - Then it stops that consumer, sets it to
stalled, and records the stream, version, event type and exception. - Metrics, logs, the health check and
deedbox statusreport it. - You fix the code and restart, or skip the one event with an audited job.
No timeout ever moves a consumer past an event it did not handle.
Causation and tracing
Section titled “Causation and tracing”A subscription’s ctx.Services is a scope made for the event. A store resolved there appends with the event’s tenant and correlation ID, and records the event as the cause. Each handler call runs in a trace span whose parent is the append that wrote the event.