# Payment Checkout & Ledger Service

**Solution Architecture v1.0 · open source, self-hosted on Kubernetes across two in-country data centres · Payments Platform Architecture · 2026-10 · 24 views · 18 architecture decision records**

Everybody has pressed Pay now. What happens behind that button is the part almost nobody sees, and the products that have solved it — Stripe, Adyen, Razorpay, a UPI PSP app — are judged without mercy on one thing: a service that charges a buyer twice, or that cannot say where a merchant's money is, is not a slow product, it is a broken one. This package is the payment service behind such a button: an assumed 50,000 merchants, 12 million payment attempts a day at peak season, 420 attempts a second at steady state with a 4,000-a-second sale peak sustained for ten minutes, roughly eight ledger entries per payment and therefore about 35 billion entries a year, and 30 provider settlement files a day of up to 10 million lines each — built on self-managed Kubernetes across two in-country data centres, with a PCI-DSS-scoped cardholder data environment, HSM key custody, Kafka as the durable event log and PostgreSQL as an append-only double-entry ledger replicated synchronously for RPO 0.

The hard part is not the arithmetic. It is that the money moves on networks the service neither owns nor can interrogate reliably. A card authorization can time out eight seconds after the issuer has already approved it, leaving the service genuinely unable to say whether the buyer has been charged. An account-to-account callback can arrive twice, out of order, or three hours late. A settlement file can turn up a day later and disagree with everything the service believed happened. Meanwhile the buyer is looking at a spinner and their instinct is to press the button again, the merchant is deciding whether to ship, and a regulator may ask in 2036 what the platform believed about this payment and when it believed it.

The design rests on one rule: **the authorization plane is allowed to fail; the book of record is not allowed to be wrong — and a durable ordered event log is the only thing between them.**

The decisions that carry the design:

- **Authorization plane ≠ ledger plane.** The buyer-facing path is stateless, deadline-bound, holds no truth and may decline, time out and fail closed. The book of record is append-only, strongly consistent, synchronously replicated and permitted to lag. A ledger outage becomes a balance that is behind rather than a checkout that is down — and the price is a window the rest of the architecture exists to close (ADR-01).
- **The ledger is append-only double entry, balanced at write time.** Signed integer minor units, a transaction rejected unless its entries sum to zero, `UPDATE` and `DELETE` revoked for every role. Every balance, statement and report is a projection, so a number that cannot be derived from the ledger is a number the platform does not publish (ADR-02).
- **Idempotency is declared separately at every boundary.** The merchant's key at the edge, the provider's event identifier at the callback, that event identifier as a unique natural key on the ledger transaction, a unique instruction key on the payout. Duplicate provider calls and duplicate ledger entries are different failures and need different keys (ADR-03).
- **`unknown` is an outcome, and there is never a retry after one.** A provider that does not answer within eight seconds leaves the intent in `processing`, which only an inbound signal or a reconciliation job may leave. A retry is the one action that can produce a double charge, and the budget is one per million attempts (ADR-04).
- **The provider's answer is a hint; the settlement file is the evidence.** Raw callbacks are stored before they are parsed, deduplicated on the provider's own event identifier, and refused when the transition they imply is illegal. What the daily file says is what the platform ultimately books (ADR-05).
- **Unmatched value is named, not netted.** Every settlement line is matched, broken or disputed; a break is posted to a named account, aged, owned and alarmed past 48 hours. An unexplained rupee on the books is a finding; an unexplained rupee off the books is a loss nobody will ever find (ADR-07).
- **Payment state is sharded by merchant; the ledger gives up a global sequence.** No synchronous cross-shard transaction exists in the payment path, and one large merchant's sale cannot consume another's capacity. The ledger is partitioned by period and account class, which is also why marketplace splits are deferred rather than quietly unbuilt (ADR-08).
- **Webhooks are at-least-once with a per-object sequence number and a queue per merchant.** The platform promises what it can keep, gives merchants the primitive they need to be correct anyway, and refuses the ordered delivery that would turn one dead endpoint into everyone's backlog (ADR-09).
- **Posting rules ship shadowed, gated on a byte-identical replay.** Code rolls back in minutes; posted ledger entries never roll back, so the gate before the release is worth more than the rollback after it. One poster version runs at a time (ADR-10).
- **The cardholder data environment stays small.** Card details go from the buyer's browser into a separately clustered CDE and never enter the application zone. A full compromise of the application estate yields tokens, not card numbers (ADR-11).
- **The platform holds merchant balances.** Refunds succeed regardless of a merchant's liquidity, disputes are debitable and hold periods are a platform control — and the platform is therefore custodian of other people's money, with the licensing and the account model that follows (ADR-13).
- **Dual control is a service in the path, not a policy in a document.** No sequence exists in which one authenticated human changes the books, and the approval is signed in the HSM so it is evidence rather than a row somebody could write (ADR-14).
- **A leaked merchant credential reaches only its own merchant's account.** Publishable and secret credentials are separate, the publishable one can do nothing but confirm an existing intent, and the settlement destination is a merchant attribute no API call can set (ADR-15).
- **Every derived store is rebuilt on a schedule to prove it is derived.** Projections drift into becoming truth unless someone deletes and rebuilds them, and the scheduled rebuild-and-compare is also the only honest test of the recovery path (ADR-16).
- **A sanctions match holds rather than rejects, and payouts move proven cash.** For every gate in front of an irreversible action, the failure direction is chosen deliberately so that it protects the party who cannot protect themselves (ADR-17).
- **Self-managed open source in two in-country data centres.** Data residency, PCI scope containment and dependency approval drove it, not a preference for owned hardware — and 40 km between sites is what makes synchronous replication and RPO 0 affordable. The accepted limit is that a metro-wide event takes both sites (ADR-18).

The architecture one-pager (including why the design should still hold up in ten years, and the six risks that would change it) and the full decision record appear on the landing page of the diagram set, directly below the index of views. The same content is published as [docs/architecture-one-pager.md](docs/architecture-one-pager.md) (~16 min) and [docs/decision-record.md](docs/decision-record.md) (~79 min).

---

## What is here

| Path | Contents |
|---|---|
| `diagrams/index.html` | The landing page: 24 views in seven acts with every format linked, then the **architecture one-pager** and the **decision record** |
| `diagrams/*.html` | One self-contained page per view: the inlined diagram plus the reasoning cards, with copy / PNG / PDF export |
| `diagrams/svg/*.svg` | The same views as SVG with the diagram XML embedded; they re-open fully editable in diagrams.net |
| `diagrams/drawio/*.drawio` | draw.io source |
| `docs/architecture-one-pager.md` | The one-pager as markdown |
| `docs/decision-record.md` | The eighteen decision records, the stack table and the glossary as markdown |
| `ask.md` | The requirement this package answers |
| `specs/part-*.json`, `specs/manifest-*.json`, `specs/adr-*.json` | The authoring sources. Edit these, never the assembled `views.json`, `manifest.json` or `adr.json` |
| `scripts/build.sh` | Rebuilds every deliverable from the specs. Node 20+, no network, no draw.io Desktop |

## Rebuilding

```bash
cd architecture/deliverables/use-cases/payment-checkout-and-ledger-service
bash scripts/build.sh
```

The build generates draw.io from the specs, fails on overlapping geometry or an arrow crossing a box, renders both SVG forms, writes the HTML pages and index, injects the one-pager and decision record, and proves every relative link resolves. The current set passes the geometry gate with **0 errors and 0 warnings** and the routing gate with **0 errors and 1 warning** — 24 files, 201 edges routed, 466 nodes all iconed. The one warning is on the integration view, where two adjacent labels in the hub fan sit close to a neighbouring line; it is inherent to that layout and was left rather than cutting a real interface out of the view.

## The seven acts

| # | Act | Views |
|---|---|---|
| 1 | Context and scope | 01–02 |
| 2 | People and journeys | 03–06 |
| 3 | Structure | 07–09 |
| 4 | Data | 10–12 |
| 5 | Runtime | 13–17 |
| 6 | Operations | 18–21 |
| 7 | Assurance | 22–24 |

## Evidence

This is a design, not a report on a running system. Every rate, latency, interval, limit and retention figure in `ask.md`, on the cards and in the records is a **stated assumption**, chosen to be defensible and arguable rather than measured. Nothing here is drawn from the internals of any named product; the products in the opening paragraph are there because they make the problem recognisable, not because this is how they are built. The one thing not invented is the shape of the ledger: double-entry bookkeeping is older than computing, and choosing it is choosing not to invent a money model.
