Section 3: The Contract Before the Boxes - API Design & High-Level Architecture
Executive Summary: Section 2 fixed the numbers: ~1,200 checkouts/s at peak, ~1,500 authorization calls/s to the PSP, 3,500 journal transactions/s at the ledger. This section writes the contract first, as resources (PaymentIntent, Capture, Refund, Payout, Balance, Statement) carrying integer minor-unit amounts, with an Idempotency-Key on every mutating call. Three interviewer pushbacks test it: one /pay call, client-supplied splits, and returning 200 when the PSP times out. Then we draw the services behind it and split them along one line. Anything that makes a claim about money is synchronous and strongly consistent. Anything that only shows money is asynchronous and fed from the outbox.
The interviewer caps the marker. "Numbers are fixed. Before you draw a box, what does a client actually call?" Starting with the API is the right instinct. Payments is a domain where the contract outlives every implementation behind it, and a wrong verb in v1 becomes a migration you're still running three years later.
Resources, Not Verbs
The API models the nouns of money movement, each with its own lifecycle and its own creator:
| Resource | Created by | When | What it represents |
|---|---|---|---|
PaymentIntent |
Checkout (buyer's session) | At checkout | One cart, one card, one authorization; carries server-computed per-seller splits |
Capture |
Fulfilment service | When a seller sub-order ships | Money actually taken, for one seller's portion |
Refund |
Support / seller tools | After a capture | Money flowing back, against a specific capture |
Payout |
Payout scheduler | Daily batch | A transfer of a seller's available funds to their bank |
Balance |
Derived | Read-only | A seller's pending, available and reserved amounts |
Statement |
Derived | Read-only | Line items explaining every balance change |
Three conventions run through every endpoint. Amounts are int64 minor units plus an ISO currency (D6): Maya's cart is 12000 with usd, never 120.00 as a float. Every mutating call carries an Idempotency-Key, stored with a request fingerprint in the same database transaction as the effect (D10, the full mechanics are in Section 8). And no amount the server can compute is accepted from the client.
Here is Maya's checkout for ord_7Q2, Lantern & Loom's $80 lamp and Copperleaf Tea's $40 tea set:
POST /v1/payment_intents
Idempotency-Key: 9f1c…
{ "order_id": "ord_7Q2", "payment_method": "pm_tok_4b…" }
201 Created
{
"id": "pi_7Q2",
"status": "requires_confirmation",
"amount": 12000, "currency": "usd",
"splits": [
{ "seller_id": "lantern_loom", "amount": 8000, "commission": 960 },
{ "seller_id": "copperleaf_tea", "amount": 4000, "commission": 480 }
]
}
The request names an order and a card token. That's all it carries. The 12000, the two splits and the 12% commissions ($9.60 and $4.80) come from the Order service's priced cart. The pm_tok_ value comes from the PSP's hosted fields, so a card number never reaches us (D15, Section 10). Maya then confirms with POST /v1/payment_intents/pi_7Q2/confirm, which triggers the authorization.
The rest of the surface is small:
| Endpoint | Caller | Notes |
|---|---|---|
POST /v1/payment_intents/{id}/confirm |
Buyer client | Authorizes the full cart; returns authorized, requires_action (3-D Secure), processing or failed |
POST /v1/payment_intents/{id}/captures |
Fulfilment (internal) | {seller_id, amount}; one per seller sub-order at shipment |
POST /v1/refunds |
Support, seller tools | {capture_id, amount}; validated against what was captured |
GET /v1/balance, GET /v1/statements, GET /v1/payouts |
Seller dashboard | Scoped to the caller's seller id; served from projections |
Quick check before moving on:
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson