fivenines
10/38

Guided Problem

YourRide Build 08: Settle a Trip Once

Time
20m
Level
foundation
Artifacts
not specified
Progress0%
Lesson 1.8 99.9%

Payments, ledger, and payouts

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. Financial state therefore pays the full price for exactly-once effects.

A first pass might retry a timed-out capture as a new request and repair duplicate charges later from provider reports. That works only while the failure case is absent. One trip operation has one stable financial identity and produces at most one provider effect and one balanced, append-only ledger effect.

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.

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.