Frame 03Open sourceBackend correctness2026

Every must-never-happen is a database constraint

A usage-based billing system: batched event ingest, hourly aggregation, monthly tiered invoices, a signed payment webhook, and an ops console for credits and overrides. I wrote it as an open-source reference implementation, sole author, with one rule: if a mistake would move money twice, the thing that stops it has to be a UNIQUE index, a row lock, or a trigger in Postgres. Never a comment, never a well-meaning if statement.

Stack
Python 3.12, Django 5, DRF, PostgreSQL 16, React 18, TypeScript 5, Docker Compose
Scope
Sole author, open-source reference implementation
The 30-second version

What could go wrong, and how we would know

Billing has a short list of failures that are worse than downtime. A customer's SDK retries a batch and the same usage is billed twice. Two schedulers fire at month end and a customer gets two invoices. An ops engineer double-clicks "issue credit". Someone who knows the webhook URL replays a recorded delivery and flips an invoice to paid. The hourly cache drifts from the raw events and nobody notices until the customer does. A new endpoint forgets to filter by tenant.

None of these fail loudly on their own. A double-billed row looks exactly like a legitimate one. So the design question was not "how do I avoid these" but "what raises if they happen anyway". DESIGN.md states the rule in three words: "Constraints over comments."

Why it holds up

Each path has a database primitive behind it, listed in DESIGN.md section 2. Replayed request_id: UNIQUE(usage_event.request_id). Double-issue: UNIQUE(invoice.customer_id, period_start). Credit double-click: UNIQUE(credit.customer_id, idempotency_key), with the key generated once when the modal opens. Override replay: UNIQUE(supersedes_id, override_idempotency_key). Webhook replay: UNIQUE(webhook_event.source, delivery_id) inside the same transaction as the invoice mutation. Audit-log edits: a BEFORE UPDATE/DELETE trigger that raises, even for raw SQL. Aggregator drift: a job that samples 100 random windows, re-derives each from raw events, and bumps a Prometheus counter on any mismatch. Forgotten tenant scoping: a pytest that walks the URL resolver and fails if any /v1 viewset does not inherit TenantScopedViewSet. The application's only job is to catch IntegrityError and return {created: false}. If someone forgets the catch, the result is a 500, not a double charge.

What I built

Ingest. POST /v1/events takes up to 1,000 events per call. The whole batch goes into one SQL statement: jsonb_to_recordset unpacks the payload, INSERT ... ON CONFLICT (request_id) DO NOTHING RETURNING request_id writes what is new, and the accepted count is the number of returned rows. The docstring puts it plainly: "The database, not the application, is the dedupe authority."

Aggregation. usage_event is the source of truth and is never edited. usage_window is a cache of units per customer per hour; the aggregator is its only writer, locks the concrete row with SELECT FOR UPDATE, and recomputes from raw events, so running it twice gives the same answer. Late events carry received_at, and any window whose last_aggregated_at is older than a contained event's received_at is picked up on the next pass. Before month-end issuance the scheduler drains aggregation; if it cannot drain in 10,000 passes it logs at CRITICAL and skips issuance rather than underbill.

Money. Amounts are integer cents. Per-unit rates are micro-dollars, because the volume rate is $0.0005 per unit and that is not a whole number of cents; a rate is a multiplier, not money. Rounding happens exactly once, half-up, where units times rate becomes a line item. The invoice total is then plain integer addition. The worked example in money.py and DESIGN.md: 250,000 units on the standard plan is 10,000 free, then 90,000 at 1,000 µUSD (9,000 cents), then 150,000 at 500 µUSD (7,500 cents), for 16,500 cents, or $165.00 exactly. A test asserts that number, and the invoicing service raises RuntimeError if the sum of its line items ever disagrees with the standalone helper.

Webhooks. The payment endpoint verifies HMAC-SHA256 over timestamp.body with hmac.compare_digest, rejects timestamps more than 300 seconds off, and does both before the JSON parser runs. A valid delivery is inserted into webhook_event and the invoice flipped to paid in one transaction; a replayed delivery_id hits the UNIQUE index, the transaction rolls back, and the response is 200 with {replayed: true}. A forged signature or a stale timestamp gets a 400 and the invoice stays issued.

Ops. Credits and overrides go through the ops console with HTTP Basic against Django staff users, a separate auth stack from API keys. Overrides never mutate a line item; they insert a new one with supersedes pointing at the old, and If-Match rejects stale views with 412. Every money-moving action writes an audit_log row in the same transaction, and a plpgsql trigger installed by post_migrate makes that table append-only, Django admin included.

ChoseOverBecauseCost
One INSERT ... ON CONFLICT DO NOTHING RETURNING per batchCheck-then-insert, or insert then countThe unique index serialises duplicates in the database, and the accepted count is exactly the rows this statement wrote. No window for a concurrent insert to be miscounted.Raw SQL through jsonb_to_recordset instead of the ORM; Postgres only.
Integer cents for amounts, micro-USD for ratesDecimal everywhereOne half-up rounding at the line-item boundary; totals are integer addition; every amount on the wire is a plain integer, so the front end and back end cannot disagree by half a cent.Two units to keep straight. The $0.0005 rate lives as 500 µUSD and has to be labelled as a rate, not money.
Locked job table with a leaseCelery and RedisOne fewer service; the lease is a row you can read with one query; aggregator correctness comes from row locks plus recompute, not from a broker. "At this scale, the simplicity is worth more than the headroom."Scaling out workers means reading lease fields by hand. Switch at Celery scale.
Postgres BEFORE UPDATE/DELETE trigger on audit_logApplication-level "do not edit"Raw SQL, Django admin and future bugs are all blocked; the database raises instead of rewriting history.Installed by a post_migrate signal; plpgsql, so SQLite is unsupported even for tests.
UNIQUE(customer, period_start) as the invoice guard, SELECT FOR UPDATE as an optimisationAn application lock aloneThe constraint is the guarantee; the row lock only stops the losing runner from wasting a full aggregation read first.Issuance is serial per customer; parallelising it is future work.
Two SPAsOne app with role-based routingOps auth never ships in the customer bundle, so a customer-side auth bug cannot expose ops routes.Duplicated Vite config and layout chrome; each app is one large App.tsx.

Try it

Three panels that run entirely in the browser: the ingest replay from ingest.py, the tier ladder from money.py, and the webhook check from webhooks/services.py.

Ingest: send a batch of request_ids, then replay it and watch the accepted and duplicate counts split the way the ON CONFLICT ... RETURNING statement splits them; a Set stands in for the UNIQUE index, so the concurrency half is illustrative. Tiers: the free, standard and volume rates from money.py with half-up rounding, and the 250,000-unit example landing on 16,500 cents. Webhook: HMAC-SHA256 over timestamp.body via Web Crypto, mirroring webhooks/services.py, with forge, stale (one hour old) and replay toggles that produce the same 400 and {replayed: true} outcomes the test suite asserts. The signing secret is a demo constant.

Verification

The backend has 145 test functions across 24 files, and they run against a real PostgreSQL 16 rather than SQLite, because the trigger is plpgsql and the ON CONFLICT path is Postgres-specific. The concurrency tests use OS threads with fresh connections: 10 threads replaying the same request_id, 5 racing to issue the same invoice, 8 double-clicking the same credit, 2 replaying the same override key. Each asserts exactly one row.

Tier math is tested at the boundaries 9,999 / 10,000 / 10,001 / 100,000 / 100,001, plus half-up rounding, negatives, and sub-cent rates that round to zero. Audit-log immutability is tested against the trigger. The late-event test ingests 50,000 units, issues the invoice, ingests 25,000 more into the same period, and checks that the window now reads 75,000 while the issued invoice's total and line-item ids are unchanged. Front-end tests are vitest on helpers (money formatting, override filtering, pagination URLs), and both builds run tsc --noEmit before Vite.

What I found

Finding

The first ingest used bulk_create(ignore_conflicts=True) and then a follow-up query to work out how many rows had landed. That count has a hole. Between the insert and the query, a concurrent request can land a row with one of the same ids, and this call would report it as accepted. The fix was to make the insert and the count the same statement, ON CONFLICT DO NOTHING RETURNING request_id, so the accepted count is exactly the rows this statement wrote. I do not have a record of a test catching the race first; the reasoning lives in the ingest.py docstring, not in a failing test, and that is the weaker of the two.

Outcome, and what it does not prove

What it proves: under the races the tests can construct, every money-moving invariant holds, and it holds because of the schema rather than because of code discipline.

What it does not prove: throughput. DESIGN.md has a scaling section (partition usage_event by month at 10x, put a queue in front of ingest at 100x) but there is no load test behind it, so those are plans, not measurements. It also does not prove the system runs anywhere but docker compose on a laptop; there is no deployment and no CI, so the 145 tests pass when I run them, not on every push.

What I would do differently

Build the post-issuance reconciliation before anything else. The schema already supports a prior_period_adjustment line and the pre-issuance half is tested, so the missing piece is a policy decision plus one job. Add the GitHub Actions workflow, because a guardrail test that only runs when I remember to run it guards nothing. Split each App.tsx; roughly 1,500 lines in one file was fine for shipping and is wrong for maintaining. Replace HTTP Basic with session auth on the ops console. And write the load test, so the scaling section is backed by a number.

Honest notes

Sources README DESIGN.md ingest.py money.py webhooks/services.py tests_invoicing.py ops/apps.py Scope: I (sole author) Verified 3 Sep 2026

Elsewhere