fivenines
10/38
Lesson 1.8 99.9%

Payments, ledger, and payouts

🎯 Objectives
  • Design the money flow: authorize at request, capture at completion, payout weekly.
  • Make every money movement idempotent and double-entry ledgered.
🔗 Connect

From 1.4 you saw at-most-once delivery chosen because pings are cheap. Money is the opposite pole: a duplicated capture is a charged-twice rider and a support fire. This lesson is the promised contrast — here we pay full price for exactly-once effects.

The flow follows the trip: authorize (place a hold) when the ride is requested, so we learn about bad cards before a driver drives; capture the final fare at completion; refund/adjust via support flows. Tips capture separately. Drivers accrue earnings per trip and receive weekly payouts (batch, off-peak) minus the platform take. Promo credits and wallet balances are spent before the card is touched.

Two non-negotiables. First, every mutation to the payment service carries an idempotency keytrip id + operation — stored with the outcome, so retries (and there will be retries: PSP timeouts are routine) return the recorded outcome instead of re-executing. Second, every movement writes a double-entry ledger: equal and opposite entries across accounts (rider receivable, platform revenue, driver payable, PSP clearing). The ledger is append-only and is the substrate for earnings statements, refunds, financial reporting (1.13), and fraud analytics (2.11). The PSP knows about cards; our ledger knows about truth.

sequenceDiagram
    autonumber
    participant T as Trip service
    participant PAY as Payments service
    participant L as Ledger
    participant PSP as Payment provider
    Note over T,PSP: at ride request
    T->>PAY: authorize {trip id, est. fare} — idempotency key trip:auth
    PAY->>PSP: create hold
    PSP-->>PAY: hold ok
    PAY->>L: entry — rider receivable / pending
    Note over T,PSP: at trip completion
    T->>PAY: capture {trip id, final fare} — idempotency key trip:capture
    PAY->>PAY: seen this key? no → proceed
    PAY->>PSP: capture against hold
    PSP--xPAY: timeout!
    PAY->>PSP: retry same PSP idempotency key
    PSP-->>PAY: captured (was already applied)
    PAY->>L: entries — rider charged, driver payable 80%, platform revenue 20%
    PAY-->>T: settled — receipt data
    Note over PAY,PSP: weekly batch
    PAY->>PSP: payout driver balances
    PAY->>L: entries — driver payable cleared
Worked example: capture with a PSP timeout. The idempotency key — ours and the PSP's — turns a scary retry into a safe one. Step 9's timeout is the normal case to design for.
flowchart LR
  TRIP["Trip completed — fare 20.00"] --> PAY["Payments service"]
  PAY --> LED
  subgraph LED ["Double-entry ledger — append only"]
    E1["Rider account −20.00"]
    E2["Driver payable +16.00"]
    E3["Platform revenue +4.00"]
  end
  LED --> STMT["Driver earnings statements"]
  LED --> FIN["Financial reporting 1.13"]
  LED --> FRAUD["Fraud analytics 2.11"]
  LED --> REC["Daily reconciliation vs PSP settlement file"]
Every cent explainable: entries always sum to zero, and a nightly job reconciles our ledger against the PSP's settlement files.
⚠️ Pitfall

Never make trip completion wait on capture. If the PSP is down, the trip still completes, the rider still leaves the car, and capture retries in the background — riders on hooks for PSP outages is self-inflicted downtime. This is your first taste of graceful degradation, formalized in 2.6.

Next step

See what actually stuck.

Take the practice scenarios now.