Normalization Confluence · Engineer's guide

Do your replicas agree after the repair code runs?

A guide for backend engineers. No background in proofs or rewriting systems is needed. Plain words come first; the research terms are collected in a glossary at the end.

Dayna Blackwell, Blackwell Systems. Each claim about what is proved links to its machine-checked proof.

1The problem you already have

Many backend systems apply the same events in more than one place, in different orders:

Most of these systems also have business rules. Often a rule is not kept by refusing a write. The write is accepted, and code that runs afterwards puts things right:

This guide calls that code the repair. The question is this: if every replica sees the same events, in different orders, and runs the same repair code, do they end up agreeing?

Not always. The repair can be correct on every replica, and the replicas can still disagree. The next section shows the smallest example.

2Try it: the shelf

A shelf holds at most 5 items and has 4 in stock. Two events arrive: "add 3" and "remove 1". The repair caps the stock at 5. Pick an order for each replica.

When the cap applies

Replica A

StepRawAfter repair
Stock:

Replica B

StepRawAfter repair
Stock:

Repair on every write. The two orders end at 4 and 5. In the order "add 3" then "remove 1", the cap throws away 2 items, and the removal then takes a real item off a full shelf. In the other order the removal comes before the overflow, so the cap later throws away only 1 item. Same events, same code, different stock.

Store raw, cap when reading. The store keeps the raw count, and the cap is applied each time the value is read. Adding and subtracting give the same total in any order, so every replica stores 6 and reads 5, whatever the order. This changes the business meaning. The store now accepts the overflow: it records 6 items for a shelf of 5, and removing one item from a full shelf can still read 5 afterwards, because the removal came out of the overflow. That is right for a backlog of items waiting for shelf space, and wrong for a shelf that refuses extra items.

3Three questions to ask of your repair code

These are the plain versions of the conditions the theory proves. Write apply(e, s) for event e applied to state s, and repair(s) for the repair run until it is done.

(a) Does repairing early or late change what the next event produces?

Research term: state descent (CC2 in the paper; note that CC2 is question (a))

Take a state a replica can be in, including one where the repair has not run yet. Apply the next event to the state as it is, and to its repaired form. Repair both results. They should be equal: repair(apply(e, repair(s))) == repair(apply(e, s)).

The shelf fails at a raw stock of 7. Repair, then remove 1: 4. Remove 1, then repair: 5. The cap threw away items that the removal needed to see.

This only matters if an event can arrive before the repair has finished; section 4 shows when that happens.

(b) Does reordering events change the repaired result?

Research term: history descent, on repaired steps (CC1 in the paper; note that CC1 is question (b))

Apply events with the repair after each one. Every order your delivery allows should give the same result. When any order is possible, it is enough to check every pair of events, in both orders, at every state a replica can reach.

The shelf fails: 4 in one order, 5 in the other.

(c) Does the repair always finish?

Research term: terminating repair (WFC in the paper and in gsm's report)

A repair that triggers another repair, which triggers the first one again, never settles. The usual way to show that it finishes is to find a measure, such as the number of broken rules or the number of items over capacity, that every repair step makes smaller. That is not only a convenient method: a repair finishes from every state exactly when some such measure exists.

Proof: terminating_iff_comp_wf

What is proved. Take one service whose repair always finishes, where events can arrive in any order and an event can arrive before the repair has finished. Its replicas agree in every order exactly when (a) and (b) both hold at the states replicas can reach. "Exactly" means both directions: if (a) and (b) hold, every order agrees; if either fails, some run of events and repair steps ends in two different states. Neither question implies the other. Some systems pass (a) and fail (b), and some pass (b) and fail (a).

Proof: cc_exact · nd_derived_from · descent_not_history · history_not_descent

Tests you can add today

Here are the three questions as property-based tests, written for Python's Hypothesis. Supply your own START, EVENTS, apply, is_valid, repair_step (one step of the repair), repair (the repair run to the end) and measure. For the shelf they are:

START, CAP = 4, 5
EVENTS = ["add 3", "remove 1"]
def apply(e, s):     return s + 3 if e == "add 3" else max(s - 1, 0)
def is_valid(s):     return s <= CAP
def repair_step(s):  return min(s, CAP)          # one step: cap the stock
def repair(s):
    while not is_valid(s): s = repair_step(s)
    return s
def measure(s):      return max(s - CAP, 0)      # items over the cap

The tests:

from hypothesis import given, strategies as st

events = st.sampled_from(EVENTS)

@st.composite
def reachable(draw):
    """A state a replica can be in: events applied, repair partly run."""
    s = START
    for e in draw(st.lists(events, max_size=6)):
        s = apply(e, s)
        for _ in range(draw(st.integers(0, 3))):
            if not is_valid(s):
                s = repair_step(s)
    return s

@st.composite
def reachable_repaired(draw):
    """A state after every event has been repaired."""
    s = START
    for e in draw(st.lists(events, max_size=6)):
        s = repair(apply(e, s))
    return s

# (a) Repairing early or late: same result after the repair.
@given(reachable(), events)
def test_repair_early_or_late(s, e):
    assert repair(apply(e, repair(s))) == repair(apply(e, s))

# (b) Two repaired events give the same result in either order.
@given(reachable_repaired(), events, events)
def test_order(s, e1, e2):
    assert repair(apply(e2, repair(apply(e1, s)))) == \
           repair(apply(e1, repair(apply(e2, s))))

# (c) Every repair step makes the measure smaller.
@given(reachable())
def test_repair_finishes(s):
    if not is_valid(s):
        assert measure(repair_step(s)) < measure(s)

Run against the shelf, the first two tests fail with 4 against 5 and the third passes. For (a), Hypothesis reports a raw stock of 7 and "remove 1"; for (b), a stock of 4 with "add 3" and "remove 1". The states are built by replaying events from START on purpose: the conditions need to hold only at states a replica can reach, and testing arbitrary states can report failures that never happen. If your queue can deliver a message twice, a duplicate is one more order to check; the atlas gives the exact condition for each delivery guarantee.

A property-based test samples states and events. It can find a counterexample. It cannot show that none exists, because it does not try every state. That is what gsm and the proofs are for: gsm tries every state of a finite model at build time, and the proofs show that these are the right conditions to check.

4When the repair runs matters

Question (a) is about timing, so whether you need it depends on when your repair runs.

Proof: atomic_exact · atomic_derived_from

The difference is real. A system can be correct with an in-transaction repair and break with a background reconciler.

An account, modeled by its status alone. It is in good standing, suspended, or over the limit. Over the limit breaks a rule; the other two do not.

  • "charge": a charge that takes the account over its limit, whatever its status.
  • "review": a risk review. If the account is over the limit, the review suspends it. Otherwise it marks the account in good standing.
  • The repair: an account over the limit is covered from a credit line and returns to good standing.

Repair in the same transaction. A charge and its repair end in good standing. A review of an account in good standing leaves it there. Starting from good standing, every order of any mix of these events ends in good standing.

Repair by a background reconciler. The account is in good standing; "charge" arrives, then "review". If the reconciler runs between them, the review sees good standing and the account stays in good standing. If the review arrives before the reconciler, it sees the account over the limit and suspends it. Suspended breaks no rule, so the reconciler has nothing to do. Same events, same order, two different end states, decided by when the reconciler ran.

Question (a) finds it. From over the limit, repairing and then reviewing gives good standing. Reviewing and then repairing gives suspended.

Proof: atomic_not_descent

In practice: if your repair runs in the same transaction as the event, test (b). If anything runs it later, test (a) as well, or move the repair into the transaction.

5When a check fails: your options

These are standard design moves, not results of the theory, except where a proof link says otherwise.

Proof (fewest links): maxcut_plan_reduction · lmin_reduction

6More than one service

Services often copy data to each other: a catalog feeds search, orders feed billing, billing feeds a ledger. Each service enforces its own rules with its own repair. The questions change with the shape of the network.

No loops

When data flows one way, along a chain or a tree of services, the check splits into two local checks per link. An event gives the same result whether or not the link's repair has run (question (a), at the link). Two independent events on one service (events that nothing orders, so they can arrive in either order) give the same result in either order after that repair (question (b)). Settling needs no check of its own: when every service keeps getting its turn, a network without loops always settles at one state.

Proof: fed_exact · rb_robert_fair

Loops

When rules feed each other in a loop, repairs can push against each other forever. A schedule that gives every rule its turn can cycle without end, even when exactly one consistent state exists. The proved example is as small as a loop gets: one record and two rules that act on it. One rule swaps two bad values, A and B. The other repairs A to the good value. If the repair rule only ever gets its turn while the record holds B, the good value is never reached. As an analogy only, this is much like two controllers that each undo the other's change; the proof itself is about one record with two rules. Knowing the one consistent state is not enough; whether runs reach it is a separate question.

Proof: flip2_unique_solution_livelock

Questions with no efficient general check

Suppose the copies between services can lose information (a copy keeps a summary, not the whole record) and no single source feeds every service. Then deciding whether any state satisfies every link at once is NP-complete. Unless P = NP, no efficient check answers it in general. An engineer asks this question when linking services whose rules might contradict each other: is there any consistent state at all across these linked services?

Proof: net_section_iff_sat

7 open problems (9 open tiles on the atlas), most of them about loops, are still open; each is listed with its progress in the regime audit. The Convergence Atlas maps every case on one page. Press "Start here" for a six-stop tour.

7How it relates to what you know

CRDTs

A CRDT is the case where the repair does nothing: the events commute by design. Repair code lets you use operations that do not commute. Some operations converge only with a repair, and no system without one converges using them.

What a repair does not add is outcomes a CRDT cannot have. Every system that converges with repairs behaves like a CRDT on its repaired steps, where an event and its repair count as one operation. The gain is in which operations you may write, and in when the repair may run (section 4), not in which end states are possible.

Proof: crdt_boundary · causal_convergence_exact · merge_action_exact

Database constraints that reject writes

A CHECK constraint or a unique index refuses a write that would break a rule. The research question for that style is invariant confluence (Bailis and others, VLDB 2015): which transactions can run on replicas without coordination and never break a rule. This guide is about the other style, where the write is accepted and a repair restores the rule. With a repair, some transactions that are not invariant confluent converge anyway, at a cost: the repair may undo a committed write, which the rejecting style never does.

Proof: repair_enlarges

Reconcile loops

This is an analogy, not a claim about any product. A controller that compares actual state with desired state and acts until they match, the pattern behind Kubernetes controllers, is a repair that runs separately from the events. In the terms of this guide, section 4 applies to it: question (a) is needed. When several controllers act on each other's output, the loop questions of section 6 apply: do they settle, and on one state?

8Glossary

The research term on the left; what it means in a backend on the right.

Research termIn engineering terms
ReplicaA copy of the state: a database node, a region, a device, a consumer's local view.
EventA write, message or command applied to a replica.
InvariantA business rule the repair restores: stock at most 5, balance at least 0.
Repair, compensationCode that restores a rule after a write. A cap, a clamp, a saga's compensating step, a cleanup, a reconciler pass.
Governed stepAn event followed by its repair, run to the end.
Atomic repairThe repair runs in the same transaction as the event.
RegistryOne service or store with its state, its events, its rules and its repairs.
FederationServices that copy each other's data, each with its own rules.
Delivery modelWhat your transport guarantees about order: any order, causal order, at-least-once (duplicates possible), or declared pairs that may be swapped.
ConvergeEvery replica that has seen the same events ends in the same state, whatever the order.
State descentQuestion (a): repairing early or late does not change what the next event produces.
History descentQuestion (b): every order the delivery model allows gives the same repaired result.
Terminating repairQuestion (c): the repair always finishes. The earlier papers call it WFC.
CCCompensation commutativity. In the papers, CC1 and CC2 together: CC2 is question (a) and CC1 is question (b). The CC line in a gsm report checks CC1 only, question (b), because gsm runs each event and its repair as one step (section 4).
CC1Question (b): two repaired events give the same result in either order. History descent, on repaired steps. Note that the numbering runs the other way: CC1 is (b) and CC2 is (a).
CC2Question (a): repairing early or late does not change what the next event produces. State descent.
WFCWell-founded compensation. Question (c): the repair always finishes.
Fair runA schedule in which every rule keeps getting its turn.
Canonical recurrenceFor systems whose repairs may never stop, such as loops: every fair run eventually stays on one repaired result.
RegimeOne combination of system shape (one service, a chain, a loop) and delivery guarantee. The atlas has one tile per regime.
ExactNecessary and sufficient: if the condition holds, replicas agree; if it fails, some run disagrees.
NP-complete, NP-hardNo efficient general algorithm exists unless P = NP.

9Where to go next