Count I-confluence in your own schema
Simulate a modelNeeds Python
Problem
Some constraints can be checked by each replica alone and still hold after replicas merge; others cannot. The test is invariant confluence (I-confluence): if two states that each satisfy the invariant, reached independently from a common ancestor, always merge into a state that satisfies it, the invariant needs no coordination. Per-row checks pass. Uniqueness of a value users choose, upper bounds on a quantity transactions push upward, and foreign keys under deletion fail: two replicas can each accept the same username, or each grant the last unit. For bounds there is one escape, escrow: pre-split the slack between replicas, at a coordination rate of consumption ÷ slack per replica.
List every constraint in a real schema: UNIQUE, CHECK, NOT NULL, foreign keys, "balance >= 0", "stock >= 0", "at most N". Mark each as I-confluent or not using the table in The unification and its price (uniqueness, upper bounds, and foreign keys under deletion are not). For each that is not, write down what mechanism enforces it today, and how often that mechanism coordinates. If it is a bound, compute the escrow rate: consumption per second divided by slack per replica.
The script classifies a small example shop schema (24 constraints, with assumed consumption rates for the bounds); replace its list with your own schema's constraints.
The question. What share of the constraints really needs coordination, and how often would escrow have to refill for each bound?
Predict
Expected result: a short list (often under 20% of constraints) that really needs coordination, and a long list that does not.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
count-i-confluence-in-your-own.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Count I-confluence in your own schema. Lists every constraint of a schema, marks each as invariant-confluent (safe to check on each replica alone) or not, names what would enforce the others, and for each bound computes the escrow rate. The schema below is a small shop; replace SCHEMA with your own constraints. """ import argparse, json p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--replicas", type=int, default=3, help="Replicas that accept writes (for the escrow split).") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() # (table, constraint, kind, detail). kinds: not_null, check_row, unique_generated, unique_chosen, # fk_insert_only, fk_with_deletes, lower_bound (with decrements), upper_bound (with increments) SCHEMA = [ ("customers", "id PRIMARY KEY (uuid)", "unique_generated", {}), ("customers", "email UNIQUE", "unique_chosen", {}), ("customers", "email NOT NULL", "not_null", {}), ("customers", "country CHECK (length = 2)", "check_row", {}), ("products", "id PRIMARY KEY (uuid)", "unique_generated", {}), ("products", "sku UNIQUE", "unique_chosen", {}), ("products", "price CHECK (price > 0)", "check_row", {}), ("products", "name NOT NULL", "not_null", {}), ("stock", "on_hand >= 0", "lower_bound", {"slack": 900, "use_per_s": 2.5}), ("orders", "id PRIMARY KEY (uuid)", "unique_generated", {}), ("orders", "customer_id REFERENCES customers (customers are never deleted)", "fk_insert_only", {}), ("orders", "status CHECK (in placed, paid, shipped)", "check_row", {}), ("orders", "total CHECK (total >= 0)", "check_row", {}), ("order_lines", "order_id REFERENCES orders ON DELETE CASCADE", "fk_with_deletes", {}), ("order_lines", "product_id REFERENCES products (products are never deleted)", "fk_insert_only", {}), ("order_lines", "qty CHECK (qty > 0)", "check_row", {}), ("wallets", "balance >= 0", "lower_bound", {"slack": 50_000, "use_per_s": 12}), ("coupons", "redemptions <= max_uses", "upper_bound", {"slack": 500, "use_per_s": 0.4}), ("coupons", "code UNIQUE", "unique_chosen", {}), ("sessions", "token NOT NULL", "not_null", {}), ("sessions", "user_id REFERENCES customers", "fk_insert_only", {}), ("reviews", "rating CHECK (1..5)", "check_row", {}), ("reviews", "UNIQUE (customer_id, product_id)", "unique_chosen", {}), ("audit", "at NOT NULL", "not_null", {}), ] RULE = { "not_null": (True, "row-local: any merge of valid rows is valid"), "check_row": (True, "row-local: any merge of valid rows is valid"), "unique_generated": (True, "uniqueness by construction (uuid or a per-replica id range)"), "fk_insert_only": (True, "inserts only: a referenced row never disappears"), "unique_chosen": (False, "two replicas can accept the same chosen value: needs a unique index on one node, or a reservation"), "fk_with_deletes": (False, "a delete on one replica and an insert on another can orphan a row: needs coordination on delete"), "lower_bound": (False, "two replicas can each spend the same slack: needs a lock, or escrow (pre-split slack)"), "upper_bound": (False, "two replicas can each grant the last unit: needs a lock, or escrow"), } rows = [] for table, c, kind, d in SCHEMA: ok, why = RULE[kind] r = {"table": table, "constraint": c, "i_confluent": "yes" if ok else "no", "why": why} if "slack" in d: # escrow: each replica gets slack / replicas, refilled when spent per = d["slack"] / args.replicas r["escrow_refills_per_hour"] = d["use_per_s"] / per * 3600 rows.append(r) print(f"{table:<12} {c:<62} {'I-confluent' if ok else 'NEEDS COORDINATION'}" + (f" escrow: {r['escrow_refills_per_hour']:.1f} refills/hour" if "escrow_refills_per_hour" in r else "")) need = sum(r["i_confluent"] == "no" for r in rows) print(f"\n{need} of {len(rows)} constraints ({need / len(rows):.0%}) need coordination") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"constraints": rows, "summary": [{"need": need, "total": len(rows), "need_pct": need / len(rows) * 100}]}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run count-i-confluence-in-your-own.py uv run count-i-confluence-in-your-own.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --replicas3 | Replicas that accept writes (for the escrow split). |
Our run
The example schema, constraint by constraint
| Table | Constraint | I-confluent | Escrow refills / hour |
|---|---|---|---|
| customers | id PRIMARY KEY (uuid) | yes | — |
| customers | email UNIQUE | no | — |
| customers | email NOT NULL | yes | — |
| customers | country CHECK (length = 2) | yes | — |
| products | id PRIMARY KEY (uuid) | yes | — |
| products | sku UNIQUE | no | — |
| products | price CHECK (price > 0) | yes | — |
| products | name NOT NULL | yes | — |
| stock | on_hand >= 0 | no | 30 |
| orders | id PRIMARY KEY (uuid) | yes | — |
| orders | customer_id REFERENCES customers (customers are never deleted) | yes | — |
| orders | status CHECK (in placed, paid, shipped) | yes | — |
| orders | total CHECK (total >= 0) | yes | — |
| order_lines | order_id REFERENCES orders ON DELETE CASCADE | no | — |
| order_lines | product_id REFERENCES products (products are never deleted) | yes | — |
| order_lines | qty CHECK (qty > 0) | yes | — |
| wallets | balance >= 0 | no | 2.59 |
| coupons | redemptions <= max_uses | no | 8.64 |
| coupons | code UNIQUE | no | — |
| sessions | token NOT NULL | yes | — |
| sessions | user_id REFERENCES customers | yes | — |
| reviews | rating CHECK (1..5) | yes | — |
| reviews | UNIQUE (customer_id, product_id) | no | — |
| audit | at NOT NULL | yes | — |
customers id PRIMARY KEY (uuid) I-confluent
customers email UNIQUE NEEDS COORDINATION
customers email NOT NULL I-confluent
customers country CHECK (length = 2) I-confluent
products id PRIMARY KEY (uuid) I-confluent
products sku UNIQUE NEEDS COORDINATION
products price CHECK (price > 0) I-confluent
products name NOT NULL I-confluent
stock on_hand >= 0 NEEDS COORDINATION escrow: 30.0 refills/hour
orders id PRIMARY KEY (uuid) I-confluent
orders customer_id REFERENCES customers (customers are never deleted) I-confluent
orders status CHECK (in placed, paid, shipped) I-confluent
orders total CHECK (total >= 0) I-confluent
order_lines order_id REFERENCES orders ON DELETE CASCADE NEEDS COORDINATION
order_lines product_id REFERENCES products (products are never deleted) I-confluent
order_lines qty CHECK (qty > 0) I-confluent
wallets balance >= 0 NEEDS COORDINATION escrow: 2.6 refills/hour
coupons redemptions <= max_uses NEEDS COORDINATION escrow: 8.6 refills/hour
coupons code UNIQUE NEEDS COORDINATION
sessions token NOT NULL I-confluent
sessions user_id REFERENCES customers I-confluent
reviews rating CHECK (1..5) I-confluent
reviews UNIQUE (customer_id, product_id) NEEDS COORDINATION
audit at NOT NULL I-confluent
8 of 24 constraints (33%) need coordinationWhat we found
In the example schema, 8 of 24 constraints (33.3%) need coordination: the user-chosen unique values (email, SKU, coupon code, one review per customer and product), the cascade-deleted foreign key, and the three bounds. That is more than the chapter's "often under 20%", because this schema was written to include each kind of constraint at least once; a real schema is dominated by NOT NULL and per-row CHECK constraints, which pass. The result is only as good as the list, so the number that matters is the one for your own schema.
Read it
The short list is where the coordination budget goes: one linearizable compare-and-swap for usernames at sign-up, escrow for stock and coupons, and a policy for deletes that other rows reference. Everything else can be checked locally, in any region, during a partition.
I-confluence fails exactly when the invariant is an upper bound on a quantity that transactions push upward, or a uniqueness condition. Escrow turns such a bound into local checks at the price of stranded slack: units reserved at a replica where nobody is buying.