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
- Django and PostgreSQL behind two React SPAs; money is integer cents end to end, with one rounding step per line item.
- Every replayable path (ingest, aggregation, invoice issuance, credits, overrides, webhooks) sits behind a Postgres primitive, and 145 test functions run against a live Postgres, including thread-level races.
- Not deployed, no CI workflow, and reconciliation of events that arrive after an invoice is issued is designed but not built.
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."
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.
| Chose | Over | Because | Cost |
|---|---|---|---|
One INSERT ... ON CONFLICT DO NOTHING RETURNING per batch | Check-then-insert, or insert then count | The 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 rates | Decimal everywhere | One 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 lease | Celery and Redis | One 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_log | Application-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 optimisation | An application lock alone | The 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 SPAs | One app with role-based routing | Ops 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.
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.
- 145test functions on live Postgres
- 24test files
- 10 / 5 / 8 / 2threads in the four race tests
What I found
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.
- No CI workflow in the repo. The "CI guardrail" test only guards if something runs it.
- Not deployed. No live URL and no screenshots; the only image in the tree is a favicon.
- Late-event reconciliation after invoice issuance is designed, not implemented (DESIGN.md sections 3 and 7, and the test docstring say so).
- Also not built: row-level security, rate limiting on
/v1/events, a request-levelIdempotency-Keyheader, and outbound webhook retries. - Each SPA is a single
App.tsx(1,664 and 1,457 lines). Front-end tests cover helpers, not components. - Ops console auth is HTTP Basic with credentials held client-side.
/metrics,/healthzand/readyzare unauthenticated by design. - 145 counts
def test_functions in the tree, matching the README and DESIGN.md; it is not apytest --collect-onlyfigure. - The README lists 99,999 among the tested tier boundaries; no test file uses that value.
- The
ingest.pydocstring still describes the earlierbulk_createapproach alongside the current one. - No license file.
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