Transactional Outbox
Eventual consistency rests on one promise: if a component’s state changed, the events of that change will be published. The way that promise is kept decides what a subscriber can count on.
§The gap
Committing state and publishing an event are two actions against two different systems, a database and a message broker. Done one after the other, they leave a gap:
- If the state is written and the process stops before publishing, the order is approved and no
one is told.
billingnever charges. Nothing in the system knows that anything is missing. - If the event is published first and the write then fails,
billingcharges for an approval that never happened.
Either failure breaks the application silently. Neither can be prevented by careful code in an operation, because the operation is not what performs these actions.
§One commit
The runtime closes the gap by making the announcement part of the state change. This is the transactional outbox. When an
operation commits, a record of the change — its origin, resulting state, input, and trailers — is written in the same atomic write as the entity itself. Both are stored, or
neither is.
Publication then happens from that record. Normally it follows the commit immediately and the record is marked as published. If the process stops in between, the record remains, and another replica of the component publishes it later.
Three properties follow.
Nothing is announced that did not happen. A record exists only if its state change committed.
Nothing that happened goes unannounced. A record that was not published stays until it is. There is no limit on attempts and no moment at which the runtime gives up on it.
The operation does not wait for the broker. A broker that is slow or unavailable delays the events, not the operations whose changes produced them. A component keeps serving while its announcements queue up behind it.
§What a subscriber can rely on
At least once. A process can stop after publishing an event and before marking it. The event is then published again. Receivers see duplicates for this reason as well as for those described in Delivery.
No ordering. Events are published as soon as possible and recovered when necessary; the two paths are independent. Two changes to the same order can be announced in either order, and a recovered event can arrive long after later ones.
A version on every event. Because order of arrival carries no meaning, each event carries
the VERSION of the entity state it describes. A subscriber that needs ordering compares
versions: a lower version than the one it last applied is stale, and an equal one is a
duplicate.
Conditions and payloads evaluated on the record. An event’s condition and payload are functions of the stored change, so a recovered event is evaluated on exactly the facts a prompt one would have been.
§What it requires
The promise depends on the storage being able to write the entity and the record atomically. Where it cannot, the runtime still publishes events, immediately after the commit, and says at startup that it is doing so without the guarantee. In that mode the gap described above is open again: a process that stops at the wrong moment loses the announcement.
Recovery by another replica also requires the replicas of a component to agree on which of them recovers what, which is a shared decision the platform arbitrates for them.
A component whose events nothing consumes keeps no records at all. The mechanism costs nothing where it has nothing to deliver.
§More than events
The record of a change is the general mechanism by which a state change reaches anything outside the component. Events are its first destination, not its only one. A deployment that keeps the same application in several regions carries changes between them the same way, and streams served to clients are fed from it as well. Each destination is delivered independently: one that is unavailable delays only itself.