I Foundations

Eventual Consistency

A business change often touches more than one component. Approving an order involves the order itself, a charge in billing, and a reservation in inventory. Each of those components owns its own state, and none can reach into another’s.

Toa does not make such a change atomic. There is no transaction that spans components. Instead, each component commits its own part, and the parts are brought into agreement by events. The system is consistent eventually: after every component has reacted to what the others announced.

§One atomic change at a time

The unit of atomicity is one operation on one component’s state. A transition either commits its change or does not; nothing outside the component observes a half-written order.

// orders/operations/approve.ts
export async function transition (input: Input, entry: State) {
  entry.status = 'approved'
}

When this commit succeeds, the order is approved. Nothing has been charged or reserved yet, and the operation does not try to make that happen. Its responsibility ends with its own state.

§Agreement through events

The rest of the change follows from the event the commit produces:

# billing/manifest.toa.yaml
receivers:
  orders.approved: charge
# inventory/manifest.toa.yaml
receivers:
  orders.approved: reserve

billing and inventory each react with an operation of their own, committed on their own. Between the first commit and the last, the system is in an intermediate state: an order that is approved but not yet charged. That state is real, and it is visible to anything that reads it.

§What the runtime guarantees

The application does not implement the propagation. The runtime takes responsibility for it:

  • A committed change is announced. A state change and the events it produces are not allowed to part ways: if the order was approved, orders.approved will be published.
  • An announcement is delivered. An event waits for a receiver that is down, redeploying, or not yet started, and is handed over when it returns.
  • A failed reaction is tried again. A receiver’s operation that fails is retried, and one that keeps failing is kept for an operator rather than dropped.

Together these mean that every component that subscribed to a change will process it. They do not mean it will process it exactly once, or in any particular order relative to other events. Chapter III describes these guarantees and their limits.

§What the application models

Eventual consistency moves a responsibility from infrastructure into the domain model, and the application has to accept it deliberately.

Intermediate states are part of the domain. “Approved, awaiting charge” is not an anomaly to hide; it is a state an order can be in, and the model is clearer when it says so.

A refusal travels forward, not backward. Nothing rolls back the approval if the charge is declined. billing announces the outcome, and orders reacts to it with another change of its own:

# orders/manifest.toa.yaml
receivers:
  billing.declined: cancel

Compensation is ordinary business logic, expressed with the same operations and events as everything else.

A reaction may be repeated. Because delivery is retried, an operation invoked by a receiver has to leave the same state when it handles the same event twice.

§Why this model

A transaction across components would require all of them to be available at once and to share a commit protocol. That would tie their availability together and make their boundaries depend on how they are deployed.

With eventual consistency, a component depends only on its own state to complete an operation. It can be moved between compositions, scaled, or stopped without changing what the others can do. This is the property that lets distribution remain optional: the same model holds whether the components share a process or run on different machines.

Search
Search the documentation