Payments, ledger, and payouts
- Design the money flow: authorize at request, capture at completion, payout weekly.
- Make every money movement idempotent and double-entry ledgered.
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 key — trip 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
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"]
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.