Guide contents

Lesson 10 of 53

Payment Gateway and Ledger: Interview Calibration, Opinions, and Runbooks

This lesson completes Chapter 5 with sections 7 to 10 and the chapter checklist. It follows the ledger and deep dives lesson.

7. Staff Interview Calibration: Five Probing Questions

Probe 1: "A customer clicks 'Pay Now', the connection hangs for 30 seconds, and they refresh the browser. How do you prevent a double charge?"

Senior Trap: "The user sends a new payment request, but we check if an order was already paid."

Staff Answer: "The checkout page binds the order to a client-generated UUID idempotency key on initial load. When the user refreshes, the frontend queries /orders/{id}/payment_status. Because the previous request timed out, the order is in PENDING_RESOLUTION state. The UI immediately displays 'Payment processing — please wait' and disables the payment button. Meanwhile, our background resolver polls the provider API. Even if an attacker attempts to force a POST with a new idempotency key, our backend validates that the order is already locked in PENDING_RESOLUTION and rejects the attempt with HTTP 409 Conflict."

Probe 2: "Your PostgreSQL ledger primary node crashes during an in-flight transaction. The standby is promoted with 2 seconds of replication lag. What happens to the money?"

Senior Trap: "We failover to the replica and replay the failed transactions from logs."

Staff Answer: "Because we mandate synchronous replication (synchronous_commit = on with synchronous_standby_names), the primary will not acknowledge a commit until at least one replica has written the WAL to disk, eliminating replication data loss. However, if an external provider call succeeded while the database crashed before recording the journal entry, the charge becomes an orphaned transaction. Within 15 minutes, our reconciliation engine ingests the provider's transaction feed, detects charges that have no corresponding internal journal entry, and automatically triggers an idempotent refund while alerting the engineering team."

Probe 3: "Reconciliation reveals 100 transactions where our ledger shows a completed charge, but the payment gateway has no record. How do you triage this?"

Senior Trap: "It's a bug in our code, so we should update our ledger to mark it failed."

Staff Answer: "We never mutate the ledger. We execute a three-phase triage: First, check the transaction age. Bank settlement feeds frequently exhibit publication delays of up to 4 hours; if the transactions are under 4 hours old, we mark them as PENDING_RECON and allow them to settle. Second, if they are older than 4 hours, we query the provider's direct transaction status API rather than the settlement feed. Third, if the provider's status API confirms no charge exists, this represents a phantom ledger write. We append an explicit Reversal Journal Entry debiting our escrow account and crediting the user wallet, and immediately raise a P0 alert to investigate why a capture was recorded without a valid provider transaction ID."

Probe 4: "User A (on Shard 1) transfers $50 to Merchant B (on Shard 2). Shard 2 is offline due to a network partition. How do you guarantee consistency?"

Senior Trap: "We use a distributed two-phase commit (XA) across both database shards."

Staff Answer: "Distributed 2PC introduces blocking coordinators that cause thread exhaustion during partitions. Instead, we execute a Two-Phase Transfer Protocol using a transactional outbox pattern. On Shard 1, we atomically debit User A -$50 and write an outbox record with status = PENDING. Shard 1 commits locally. Shard 2 is temporarily offline, so the background transfer worker backs off. Once Shard 2 recovers, the worker executes an idempotent credit to Merchant B with the unique transfer UUID and marks the Shard 1 outbox record as COMPLETED. The money is never lost; it is tracked explicitly in the outbox pending state."

Probe 5: "A merchant in Germany accepts a purchase in Japanese Yen. The exchange rate drops 3% between authorization and capture 5 days later. Who absorbs the loss?"

Senior Trap: "We convert the currency at capture time so we always charge the latest rate."

Staff Answer: "Converting at capture time violates consumer protection regulations in the EU, which require the customer to be billed the exact amount displayed at checkout. In our architecture, exchange rates are locked at authorization time. The customer pays a fixed JPY amount, and the merchant receives a fixed EUR amount. The platform's internal FX Clearing Account absorbs the currency variance between authorization and settlement. Our automated treasury service monitors the net balance of the FX Clearing Account daily and executes currency hedge contracts whenever net exposure exceeds our configured risk threshold."

8. Staff Insiders: Controversial Opinions

8.1 "The Ledger Is More Critical Than the User Database"

Engineers instinctively spend all their time securing the user database. But the ledger is infinitely more critical. If your user database suffers catastrophic corruption, you can recover user profiles from OAuth providers (Google, Apple), email confirmation histories, and customer service verifications. If your financial ledger is lost or corrupt, you cannot recover financial records from anywhere. You literally do not know who owes what to whom, what funds are in escrow, or what your tax liability is.

Every architectural safeguard—synchronous replication, point-in-time recovery, WAL archiving to independent cloud regions, and quarterly disaster recovery drills—must prioritize the ledger above all other databases in the enterprise.

8.2 "Reconciliation Is the Real Product, Not the Payment Flow"

Candidates routinely spend 40 minutes designing the checkout payment flow and mention reconciliation in the last 2 minutes as "a daily batch job that compares records."

This is backwards. Calling Stripe's API is a solved commodity problem; Stripe's SDK handles 80% of it. Reconciliation is where Staff engineers earn their compensation. Reconciliation is where you discover every real-world bug: the charge that succeeded at the bank but failed in your network, the refund processed twice, the currency conversion error that silently drained margin.

At $500M/month, a 0.01% reconciliation discrepancy is $50,000 per month of lost money. The Staff engineer's job isn't building the payment flow; it's building the system that detects, isolates, and resolves the 0.01%.

8.3 "You Don't Need Event Sourcing for Payments"

Candidates who have recently read about Event Sourcing immediately propose building an Event Store (Kafka, EventStoreDB) to manage payment state.

This is architectural over-engineering. Double-entry bookkeeping has provided everything event sourcing promises—complete immutability, mathematical auditability, point-in-time state reconstruction, and historical replayability—since 1494.

Introducing event sourcing on top of double-entry adds an unnecessary layer of projection complexity, read-model drift, and event schema versioning. Use relational double-entry. It is battle-tested, enforces ACID constraints natively, and is understood by every financial auditor on earth.

8.4 "Most Payment Bugs Are Accounting Model Bugs, Not Engineering Bugs"

When an outage occurs in financial infrastructure, software engineers immediately search for null pointer exceptions, network timeouts, and thread race conditions.

Yet in production reality, the vast majority of catastrophic payment discrepancies trace back to accounting model flaws:

  • A refund transaction that credited the customer but failed to debit the merchant's escrow account.
  • A cancellation workflow that failed to reverse the platform commission fee.
  • A foreign exchange conversion that rounded fractions of a cent down instead of routing them to a rounding adjustment clearing account.

These bugs pass 100% of software unit tests because the code executed cleanly. The accounting logic was wrong. Staff engineers treat financial controllers and certified accountants as primary architecture review partners before writing a single line of production code.

8.5 "PCI Scope Is the Most Consequential Architecture Decision in the Company"

Junior engineers often suggest: "Let's build our own checkout form, accept credit card numbers on our server, and forward them to the bank API."

This single naive suggestion commits the company to millions of dollars in annual security overhead. It triggers mandatory PCI-DSS Level 1 compliance audits, requires hiring dedicated security compliance personnel, and exposes the company to existential liability in the event of an infrastructure breach.

Defaulting to hosted tokenized fields (Stripe Elements, Adyen Drop-in) reduces your compliance scope to a simple self-assessment questionnaire (SAQ A-EP) with zero performance degradation. It is an architectural decision that saves the company hundreds of thousands of dollars before the system even launches.

9. The Architectural Fault Lines & Default Staff Positions

Decision Fault Line Who Pays The Real Operational Cost
Immutable Ledger vs Mutable Table Storage Volume vs Auditability Engineering & Infra Storage volume grows 4x faster. Balance lookups require cached snapshot tables and continuous validation jobs.
Distributed Lock Idempotency Throughput vs Double-Charge Safety Users & Downstream Fast double-taps are rejected with HTTP 409 Conflict. Users must wait on a spinner rather than immediately re-submitting.
Synchronous DB Replication (RF=3) Write Latency vs Zero Data Loss Users (Latency) Adds 15–30ms of cross-AZ network round-trip time to every checkout commit. Completely eliminates data loss during AZ failovers.
Two-Phase Transfer Protocol Local Simplicity vs Eventual Settle Support & Finance Transfers across sharded accounts have a 2–5 minute eventual consistency window where funds are in-flight in outbox tables.
Continuous 15-Minute Reconciliation Real-Time Safety vs API Rate Limits Infra & Partners Generates continuous API query load against bank partners; requires dedicated streaming diff infrastructure.

DEFAULT STAFF POSITIONS

# Position Default When to deviate
1 Data Model Double-entry append-only ledger with immutable journal entries. Never. Never use a mutable status column for balances.
2 Storage Engine PostgreSQL with synchronous replication (RF=3) across AZs. Spanner/CockroachDB only for global multi-region writes.
3 Idempotency Client UUID + Redis lock + DB unique constraint + Provider header. Never relax idempotency on financial debit paths.
4 Timeout Handling PENDING_RESOLUTION state with 3-channel resolution. Never treat timeouts as terminal failures.
5 Reconciliation Continuous 15-minute diff against provider settlement feeds. Daily batch acceptable only during initial MVP scale.

10. Failure Modes, 3 AM Incident Runbooks & Active Crisis Drills

Incident 1: PostgreSQL Ledger Primary Node Crash (Sync Replication Failover)

The Failure: The primary database node suffers a hardware kernel panic. 1,000 checkouts are in-flight.

The Mechanism: Because PostgreSQL is configured with synchronous_commit = on, no acknowledged transaction is lost. A standby node in AZ-2 is promoted to primary within 15 seconds.

The Ambiguous Window: Checkouts where the external Stripe API call succeeded, but the database crashed before committing the journal entry, appear as orphaned transactions.

Runbook Action:

  1. Automated Sentinel/Patroni promotes standby node to primary.
  2. Gateway health checks re-route traffic to the new primary within 15 seconds.
  3. The continuous reconciliation engine runs an emergency catch-up scan, querying Stripe for all successful charges in the past 15 minutes, automatically inserting missing journal entries into the new primary ledger.

Incident 2: Third-Party Payment Provider Global Outage

The Failure: Stripe or Adyen experiences a major global API outage, returning 500 errors or timing out.

Runbook Action:

  1. Ingress provider circuit breaker trips to OPEN after 5 consecutive timeouts (>3 seconds).
  2. Provider Router immediately diverts traffic to the secondary fallback provider (e.g., from Stripe to Adyen).
  3. Any in-flight transactions caught in the transition are shifted to PENDING_RESOLUTION.
  4. The frontend UI remains functional; customer checkout success rates recover to >98% within 30 seconds.

Incident 3: Trial Balance Invariant Breach (trial_balance_delta != 0)

The Failure: An unexpected edge-case bug in a new microservice deployment bypasses application checks and attempts to insert an unbalanced journal entry.

The Severity: P0 EMERGENCY.

Runbook Action:

  1. The PostgreSQL deferred constraint trigger immediately aborts the transaction, preventing database corruption.
  2. Prometheus detects ledger_trial_balance_violations_total > 0 and triggers an immediate P0 PagerDuty page to the Platform Staff on-call.
  3. Automated deployment canary halts and rolls back the newly deployed microservice version.
  4. The on-call engineer reviews the aborted journal payload in the application dead-letter queue (DLQ) to diagnose the arithmetic discrepancy.

Crisis Drill 1: The Outbox Dead-Letter Replay & Phantom Charge Sweep

The Scenario: A network firewall update accidentally drops traffic between the Payment Orchestrator and the Kafka cluster for 20 minutes. 40,000 cross-shard transfers accumulate in the PostgreSQL outbox table with status = 'PENDING'.

The Staff Mitigation Protocol:

  1. Verify firewall connectivity is restored: nc -zv kafka-broker-01 9092.
  2. Throttle outbox worker batch size to 500 records per second to prevent consumer lag storms on destination database shards.
  3. The outbox worker re-publishes pending transfers with their original idempotency keys. Destination shards process them idempotently without duplicate credits.

Crisis Drill 2: Double Settlement Acquirer Glitch Remediation

The Scenario: An upstream card acquirer experiences an internal queue glitch and submits the same batch settlement file twice, causing 15,000 transactions to clear twice on the bank settlement feed.

The Staff Mitigation Protocol:

  1. The 15-minute reconciliation engine flags 15,000 Duplicate Settlement discrepancies within 15 minutes of feed ingestion.
  2. Payouts to affected merchant escrow accounts are automatically frozen for 2 hours.
  3. The automated reconciliation refund worker invokes the Acquirer Void API, reversing the duplicate clearing lines before funds leave corporate settlement accounts.

Chapter Summary Checklist

STAFF CHECKLIST: Mission-Critical Payment Systems

When evaluating or defending a payment architecture in an L6 loop:

  • Ledger Decoupled: Is the internal double-entry ledger established as the permanent source of truth, treating PSPs as pluggable adapters?
  • Append-Only Immutability: Are all financial state transitions recorded as new journal entries, with zero UPDATE operations on balance rows?
  • Mathematical Balance Invariant: Does a deferred constraint trigger enforce Sum(Debits) - Sum(Credits) = 0 before transaction commit?
  • Dual-Representation Balances: Are balances cached on accounts using row-level locks and validated periodically against append-only journal entries?
  • Timeout Resolution Protocol: Is an explicit PENDING_RESOLUTION state defined with three parallel resolution channels (Webhook, Polling, Reconciliation)?
  • Distributed Idempotency: Are duplicate requests guarded by a 4-layer pipeline (Client UUID, Redis lock, DB unique constraint, PSP header)?
  • Continuous Reconciliation: Does a near-real-time reconciliation engine run every 15 minutes with automated orphan charge resolution?
  • Cross-Shard Outbox: Are transfers across sharded accounts handled via an asynchronous transactional outbox rather than blocking 2PC?
  • PCI Scope Minimized: Are hosted tokenized fields used to keep raw cardholder data completely out of origin application scope?
  • Multi-Currency Hedging: Are exchange rates locked at authorization with dedicated FX Clearing and Rounding Adjustment accounts?