Skip to content
v0.3

Events & the outbox

Every command declares the events it emits. When it runs, those events are written in the same database transaction as the change itself.

That single sentence is the guarantee. If the order was created, the order.created event exists. If the transaction rolled back, neither does. There is no window where the change happened and the notification did not, and no window where you were told about something that then un-happened.

This is the transactional outbox pattern, and it is the difference between an event stream you can build on and one you have to reconcile against.

An append-only table. Events go in; nothing edits them and nothing deletes them. It is the ledger of what happened, in the order it happened.

Delivery is tracked separately, per consumer — the question the system asks is “what does this consumer still owe?”, not “where is the global cursor?”. Two things follow that matter to you:

  • A slow or broken consumer does not hold anyone else back. Its own backlog grows; the others drain.
  • A new consumer can be added without rewinding anything. It gets its own accounting from the start.

Delivery is at-least-once. Your consumer will see the same event twice — after a retry, after a redeploy, after a network timeout that succeeded on the far side.

So the rule is not negotiable: consumers must be idempotent. Processing an event twice must have the same effect as processing it once. In practice that means keying your side-effects on something in the event rather than on the fact that you received it.

Order is per cycle, not global. Events carry a sequence that orders them within a delivery cycle. Do not build logic that assumes a total global ordering across everything that ever happened.

They are opposites, and picking the wrong one is a real bug:

  • An event is a fact that is already true. You react to it. If your reaction fails, the fact stands.
  • A decision is a question asked before the fact exists. Your answer shapes it.

If you find yourself reacting to an event by trying to change what it describes, you wanted a decision.

Every command’s reference page lists its events — and, for commands that emit none, says why. A silent command carries its reason rather than a bare “none”, because “deliberately quiet” and “nobody got to it yet” are very different facts to build on.