Chapter 54: Design Stripe/Payment System
1. Requirements
Functional Requirements
| Requirement | Description | Priority |
|---|---|---|
| Process Payments | Accept credit/debit cards, authorize, capture, settle transactions | P0 |
| Idempotency | Prevent duplicate charges from retries using idempotency keys | P0 |
| Multi-Currency | Support 135+ currencies with real-time FX conversion | P0 |
| Refunds & Disputes | Full/partial refunds, chargeback handling, dispute management | P0 |
| Webhooks | Event notifications to merchants (payment succeeded/failed/refunded) | P0 |
| Payment Methods | Cards, ACH, wire transfer, digital wallets (Apple Pay, Google Pay) | P1 |
| Subscriptions | Recurring billing with retry logic for failed payments | P1 |
| Analytics Dashboard | Transaction history, revenue reports, settlement tracking | P2 |
Non-Functional Requirements
| Requirement | Target | Interview Callout |
|---|---|---|
| Availability | 99.999% (5 min/year downtime) | Multi-region active-active, no single point of failure |
| Consistency | Strong consistency for ledger, eventual for analytics | Money is never lost or double-charged |
| Latency | p99 < 500ms for API, < 3s for payment auth | User experience critical at checkout |
| PCI Compliance | PCI DSS Level 1 certification | Tokenization to reduce scope, no raw card storage |
| Durability | Zero transaction data loss | Append-only ledger, replicated across regions |
| Security | Encryption at rest/transit, audit logs, fraud detection | TLS 1.3, AES-256, HMAC signature verification |
Out of Scope
| Fraud detection ML models (assume provided by third-party service) |
| Merchant onboarding/KYC (Know Your Customer) workflows |
| Tax calculation and compliance (integrate with TaxJar/Avalara) |
2. Capacity Estimation
Assumptions (Stripe Scale)
| Metric | Value | Calculation |
|---|---|---|
| Daily Active Merchants | 2 million | Stripe processes for millions of businesses |
| Transactions per Day | 500 million | 2M merchants Γ 250 txns/day average |
| Transactions per Second (TPS) | ~6,000 avg, ~18,000 peak | 500M / 86,400 sec Γ 3x peak multiplier |
| Read:Write Ratio | 10:1 | Dashboard queries, webhook retries, reconciliation checks |
| Avg Transaction Size | 2 KB | Payment intent metadata, card token, merchant ID, amounts |
| Daily Storage | 1 TB/day | 500M Γ 2 KB = 1 TB (append-only ledger) |
| 5-Year Storage | 1.8 PB | 1 TB Γ 365 Γ 5 = 1.8 PB (with compression) |
| Webhook Events | 1.5 billion/day | 500M txns Γ 3 events avg (created, succeeded, settled) |
| Reconciliation Jobs | Hourly | Match 20M txns/hour against bank statements |
Network Bandwidth
| Direction | Calculation | Result |
|---|---|---|
| Ingress (Writes) | 6,000 TPS Γ 2 KB | 12 MB/s (~100 Mbps) |
| Egress (Reads) | 60,000 QPS Γ 2 KB | 120 MB/s (~1 Gbps) |
| Webhooks | 17,000 events/sec Γ 1 KB | 17 MB/s (~140 Mbps) |
Interview Tip: Always clarify scale early. A payment system for a small startup (10 TPS) has dramatically different requirements than Stripe (18,000 peak TPS). Ask: "What transaction volume are we targeting?"
3. High-Level Design
graph TB
Client[Client/Merchant App]
subgraph API Gateway Layer
LB[Load Balancer
TLS Termination] AG[API Gateway
Rate Limiting, Auth] end subgraph Core Services PS[Payment Service
State Machine] IS[Idempotency Service
Key Deduplication] LS[Ledger Service
Double-Entry Bookkeeping] WS[Webhook Service
Event Delivery] RS[Reconciliation Service
Bank Matching] TS[Tokenization Service
PCI Vault] end subgraph Data Stores PSQL[(PostgreSQL
Payment Intents)] Ledger[(Ledger DB
Immutable Log)] Redis[(Redis
Idempotency Cache)] Queue[Kafka/SQS
Event Queue] end subgraph External Systems PSP1[PSP: Visa/Mastercard] PSP2[PSP: Bank Network] PSP3[Backup PSP] Bank[Bank Statements API] end Client -->|HTTPS| LB LB --> AG AG --> PS AG --> IS PS --> PSQL PS --> LS PS --> TS PS -->|Route to PSP| PSP1 PS -->|Fallback| PSP2 PS -->|Fallback| PSP3 IS --> Redis LS --> Ledger PS -->|Publish Events| Queue Queue --> WS Queue --> RS WS -->|HTTP POST| Client RS --> Bank RS --> Ledger TS -->|Store Tokens| PSQL
TLS Termination] AG[API Gateway
Rate Limiting, Auth] end subgraph Core Services PS[Payment Service
State Machine] IS[Idempotency Service
Key Deduplication] LS[Ledger Service
Double-Entry Bookkeeping] WS[Webhook Service
Event Delivery] RS[Reconciliation Service
Bank Matching] TS[Tokenization Service
PCI Vault] end subgraph Data Stores PSQL[(PostgreSQL
Payment Intents)] Ledger[(Ledger DB
Immutable Log)] Redis[(Redis
Idempotency Cache)] Queue[Kafka/SQS
Event Queue] end subgraph External Systems PSP1[PSP: Visa/Mastercard] PSP2[PSP: Bank Network] PSP3[Backup PSP] Bank[Bank Statements API] end Client -->|HTTPS| LB LB --> AG AG --> PS AG --> IS PS --> PSQL PS --> LS PS --> TS PS -->|Route to PSP| PSP1 PS -->|Fallback| PSP2 PS -->|Fallback| PSP3 IS --> Redis LS --> Ledger PS -->|Publish Events| Queue Queue --> WS Queue --> RS WS -->|HTTP POST| Client RS --> Bank RS --> Ledger TS -->|Store Tokens| PSQL
Component Responsibilities
| Component | Responsibility | Technology Choice |
|---|---|---|
| API Gateway | Authentication (API keys), rate limiting, request validation | Kong, AWS API Gateway, custom Go service |
| Payment Service | Orchestrate payment flow, manage state machine, route to PSPs | Java/Go microservice, horizontally scaled |
| Idempotency Service | Check idempotency keys, return cached responses for duplicates | Redis with 24-hour TTL, fallback to PostgreSQL |
| Ledger Service | Record every financial transaction as double-entry, immutable append | PostgreSQL with append-only constraint, sharded by merchant_id |
| Webhook Service | Deliver events to merchant endpoints with retry logic | Distributed queue (Kafka), worker pool, exponential backoff |
| Reconciliation Service | Match internal ledger with bank statements, detect discrepancies | Batch job (Airflow), runs hourly, flags mismatches for review |
| Tokenization Service | Convert raw card numbers to tokens, store in PCI-compliant vault | HSM (Hardware Security Module), AES-256 encryption |
| PSP Integration | Communicate with card networks (Visa/MC), handle timeouts/retries | ISO 8583 protocol for card networks, HTTP for modern PSPs |
Interview Callout: Explain why you chose specific databases. PostgreSQL for ledger (ACID, strong consistency), Redis for idempotency (fast lookups, TTL), Kafka for events (high throughput, replay capability).
4. Payment Flow (Two-Phase Commit)
Authorize β Capture β Settle
| Phase | Action | State Transition | Timing |
|---|---|---|---|
| 1. Authorize | Hold funds on customer's card (pending charge) | CREATED β AUTHORIZED | Immediate (~2s) |
| 2. Capture | Confirm the charge, move from "hold" to actual debit | AUTHORIZED β CAPTURED | Within 7 days (merchant triggers) |
| 3. Settle | Bank transfers funds from customer to merchant account | CAPTURED β SETTLED | T+2 days (batch settlement) |
Why Two-Phase?
| Inventory Reservation: Hotel books room during authorize, captures when guest checks in |
| Fraud Prevention: Merchant reviews high-value orders before capturing |
| Partial Captures: Authorize $100, capture $80 if item out of stock |
| Authorization Expiry: Holds expire after 7 days, requiring reauthorization |
Sequence Diagram
sequenceDiagram
participant Client
participant API
participant Payment Service
participant Idempotency
participant Ledger
participant PSP
participant Webhook
Client->>API: POST /v1/payment_intents
{amount, currency, card_token}
Idempotency-Key: abc123 API->>Idempotency: Check key "abc123" Idempotency-->>API: Not found (new request) API->>Payment Service: Create PaymentIntent Payment Service->>Ledger: Record CREATED state Payment Service->>PSP: Authorize $50.00 PSP-->>Payment Service: Authorization approved (auth_code: XYZ) Payment Service->>Ledger: Record AUTHORIZED state + auth_code Payment Service->>Idempotency: Cache response for "abc123" (24h TTL) Payment Service-->>API: {id: pi_123, status: authorized} API-->>Client: 200 OK Payment Service->>Webhook: Publish event: payment_intent.authorized Webhook->>Client: POST merchant.com/webhook
{event: payment_intent.authorized} Note over Client,Webhook: --- Later: Merchant captures payment --- Client->>API: POST /v1/payment_intents/pi_123/capture API->>Payment Service: Capture pi_123 Payment Service->>PSP: Capture authorization XYZ PSP-->>Payment Service: Capture confirmed Payment Service->>Ledger: Record CAPTURED state Payment Service->>Ledger: Double-entry: Debit Customer, Credit Merchant Payment Service->>Webhook: Publish event: payment_intent.captured Webhook->>Client: POST merchant.com/webhook
{amount, currency, card_token}
Idempotency-Key: abc123 API->>Idempotency: Check key "abc123" Idempotency-->>API: Not found (new request) API->>Payment Service: Create PaymentIntent Payment Service->>Ledger: Record CREATED state Payment Service->>PSP: Authorize $50.00 PSP-->>Payment Service: Authorization approved (auth_code: XYZ) Payment Service->>Ledger: Record AUTHORIZED state + auth_code Payment Service->>Idempotency: Cache response for "abc123" (24h TTL) Payment Service-->>API: {id: pi_123, status: authorized} API-->>Client: 200 OK Payment Service->>Webhook: Publish event: payment_intent.authorized Webhook->>Client: POST merchant.com/webhook
{event: payment_intent.authorized} Note over Client,Webhook: --- Later: Merchant captures payment --- Client->>API: POST /v1/payment_intents/pi_123/capture API->>Payment Service: Capture pi_123 Payment Service->>PSP: Capture authorization XYZ PSP-->>Payment Service: Capture confirmed Payment Service->>Ledger: Record CAPTURED state Payment Service->>Ledger: Double-entry: Debit Customer, Credit Merchant Payment Service->>Webhook: Publish event: payment_intent.captured Webhook->>Client: POST merchant.com/webhook
State Machine
| From State | Event | To State | Ledger Action |
|---|---|---|---|
| - | Create payment intent | CREATED | Log intent creation |
| CREATED | PSP authorizes | AUTHORIZED | Log authorization hold |
| AUTHORIZED | Merchant captures | CAPTURED | Debit customer, credit merchant |
| CAPTURED | Bank settles | SETTLED | Mark as reconciled |
| AUTHORIZED | Cancel before capture | CANCELED | Release hold, log cancellation |
| CAPTURED | Refund issued | REFUNDED | Credit customer, debit merchant |
| CREATED | PSP declines | FAILED | Log failure reason |
Interview Callout: Emphasize that state transitions are atomic and logged to the ledger. If a PSP call times out, the payment stays in the previous state until reconciliation determines the actual outcome.
5. Idempotency
Problem: Network Retries Cause Double Charges
| Scenario: Client sends payment request, network times out, client retries |
| Without idempotency: Server processes both requests β customer charged twice |
| With idempotency: Server detects duplicate key, returns cached response |
Idempotency Key Design
| Aspect | Implementation | Rationale |
|---|---|---|
| Key Format | UUID v4 or merchant-generated unique string | Client controls uniqueness, server validates format |
| Scope | Per merchant account | Key "abc123" for Merchant A doesn't collide with Merchant B |
| Storage | Redis (primary), PostgreSQL (fallback) | Redis fast lookup, DB for durability if Redis evicts |
| TTL | 24 hours | Balance between preventing duplicates and cache size |
| Response Caching | Store full HTTP response body + status code | Return identical response to client on retry |
| In-Progress Handling | If request still processing, return 409 Conflict | Client should retry with same key after delay |
Flow with Idempotency
graph LR
A[Client Request
Idempotency-Key: X] --> B{Key in Redis?} B -->|Yes, Complete| C[Return Cached Response] B -->|Yes, In-Progress| D[Return 409 Conflict] B -->|No| E[Mark Key as In-Progress] E --> F[Process Payment] F --> G[Store Response in Redis] G --> H[Return Response to Client]
Idempotency-Key: X] --> B{Key in Redis?} B -->|Yes, Complete| C[Return Cached Response] B -->|Yes, In-Progress| D[Return 409 Conflict] B -->|No| E[Mark Key as In-Progress] E --> F[Process Payment] F --> G[Store Response in Redis] G --> H[Return Response to Client]
Edge Cases
| Edge Case | Handling |
|---|---|
| Same key, different payload | Reject with 422 Unprocessable Entity (key reuse forbidden) |
| Server crashes mid-processing | Key stuck as "in-progress", timeout after 60s, retry allowed |
| Redis evicts key early | Fallback to DB lookup, if not found treat as new request |
| Client never retries | Key expires after 24h, no issue (one-time payment) |
Interview Callout: Idempotency is critical for payment systems. Always ask: "What happens if the client retries?" Store not just the payment ID, but the full response to ensure identical behavior.
6. Ledger & Double-Entry Bookkeeping
Double-Entry Accounting Principle
| Every transaction has two sides: Debit (money leaving) and Credit (money arriving) |
| Sum of debits = Sum of credits: System is always balanced, catch bugs early |
| Immutable log: Entries never deleted/updated, corrections via new entries |
Example: $100 Payment from Customer to Merchant
| Account | Debit | Credit | Description |
|---|---|---|---|
| Customer Card Account | $100 | - | Money leaving customer |
| Merchant Revenue Account | - | $97 | Merchant receives $97 (after fees) |
| Stripe Fee Account | - | $3 | Stripe earns $3 fee (3%) |
| Total | $100 | $100 | Balanced |
Ledger Schema
| Column | Type | Description |
|---|---|---|
| entry_id | UUID | Unique ID for each ledger entry |
| transaction_id | UUID | Groups debit + credit entries (payment_intent_id) |
| account_id | String | Customer/merchant/Stripe account identifier |
| debit_amount | Decimal(19,4) | Amount debited (or NULL if credit entry) |
| credit_amount | Decimal(19,4) | Amount credited (or NULL if debit entry) |
| currency | String(3) | ISO 4217 code (USD, EUR, GBP) |
| timestamp | Timestamp | When entry was created (immutable) |
| description | Text | Human-readable description (e.g., "Payment from John") |
| idempotency_key | String | Link back to original API request |
Ledger Architecture Diagram
graph TB
subgraph Payment Flow
PS[Payment Service]
end
subgraph Ledger Service
LS[Ledger Service
Validates Double-Entry] Check{Sum Debits =
Sum Credits?} end subgraph Storage PG[(PostgreSQL
Append-Only Table)] Archive[(Cold Storage
S3 for old entries)] end subgraph Reconciliation Recon[Reconciliation Job
Hourly] Bank[Bank Statement API] end PS -->|Submit Transaction| LS LS --> Check Check -->|Yes| PG Check -->|No| Error[Reject Transaction
Log Alert] PG -->|Archive entries > 1 year| Archive Recon --> PG Recon --> Bank Recon -->|Flag Discrepancies| Alert[Alert System]
Validates Double-Entry] Check{Sum Debits =
Sum Credits?} end subgraph Storage PG[(PostgreSQL
Append-Only Table)] Archive[(Cold Storage
S3 for old entries)] end subgraph Reconciliation Recon[Reconciliation Job
Hourly] Bank[Bank Statement API] end PS -->|Submit Transaction| LS LS --> Check Check -->|Yes| PG Check -->|No| Error[Reject Transaction
Log Alert] PG -->|Archive entries > 1 year| Archive Recon --> PG Recon --> Bank Recon -->|Flag Discrepancies| Alert[Alert System]
Refund Handling (Correction Entry)
| Original Payment | Debit | Credit |
|---|---|---|
| Customer Card | $100 | - |
| Merchant Revenue | - | $100 |
| Refund Transaction | Debit | Credit |
|---|---|---|
| Merchant Revenue | $100 | - |
| Customer Card | - | $100 |
Interview Callout: Never UPDATE or DELETE ledger entries. Refunds/corrections are new entries that reverse the original. This creates an audit trail and prevents data loss.
7. PSP (Payment Service Provider) Integration
What is a PSP?
| Definition: Intermediary between merchants and card networks (Visa, Mastercard) |
| Examples: Stripe connects to multiple PSPs (Worldpay, Adyen, Chase Paymentech) |
| Role: Route payment to issuing bank, handle auth/capture/settle, return success/failure |
PSP Integration Challenges
| Challenge | Solution | Interview Callout |
|---|---|---|
| Timeout Handling | Set 30s timeout, if no response assume unknown state | Query PSP's status API to confirm final state |
| PSP Downtime | Route to backup PSP (circuit breaker pattern) | Monitor PSP health, failover if success rate < 95% |
| Retry Logic | Retry transient errors (5xx), not permanent (card declined) | Exponential backoff: 1s, 2s, 4s, 8s (max 3 retries) |
| Rate Limits | PSP may limit to 1000 TPS per merchant | Queue requests, apply token bucket rate limiting |
| Multi-Currency | Different PSPs support different regions/currencies | Route EUR to European PSP, USD to US PSP |
| Protocol Variance | Old PSPs use ISO 8583, modern use JSON REST APIs | Adapter pattern to normalize PSP responses |
Circuit Breaker Pattern
stateDiagram-v2
[*] --> Closed
Closed --> Open: Failure rate > 50%
(5+ failures in 10s) Open --> HalfOpen: After 60s cooldown HalfOpen --> Closed: Test request succeeds HalfOpen --> Open: Test request fails note right of Closed: Normal operation, All requests sent to PSP note right of Open: Stop sending requests, Return cached error, Route to backup PSP note right of HalfOpen: Send 1 test request, If success resume normal
(5+ failures in 10s) Open --> HalfOpen: After 60s cooldown HalfOpen --> Closed: Test request succeeds HalfOpen --> Open: Test request fails note right of Closed: Normal operation, All requests sent to PSP note right of Open: Stop sending requests, Return cached error, Route to backup PSP note right of HalfOpen: Send 1 test request, If success resume normal
PSP Routing Strategy
| Criteria | Primary PSP | Backup PSP | Reason |
|---|---|---|---|
| Currency: USD | Chase Paymentech | Worldpay | Lowest fees for US transactions |
| Currency: EUR | Adyen | Checkout.com | European PSPs have better rates for EUR |
| High-Risk Merchant | Specialized PSP | - | Gambling/pharma require specific PSPs |
| 3D Secure Required | PSP with 3DS support | - | EU Strong Customer Authentication (SCA) |
Timeout Recovery Flow
| Step | Action | Reasoning |
|---|---|---|
| 1 | Send authorize request to PSP, 30s timeout | Standard payment authorization |
| 2 | Timeout occurs, no response from PSP | Network issue or PSP overloaded |
| 3 | Mark payment as PENDING, return 202 Accepted to client | Honest response: "We don't know yet" |
| 4 | Background job queries PSP status API every 10s | Poll for actual result (authorized/declined) |
| 5 | PSP returns final status β update payment state | Reconcile actual outcome |
| 6 | Send webhook to merchant with final result | Notify merchant of resolution |
Interview Callout: Never assume a timeout means failure. The PSP may have processed the payment but the response was lost. Always query the PSP's status endpoint to confirm.
8. Reconciliation
What is Reconciliation?
| Definition: Match internal ledger against external bank statements to ensure consistency |
| Frequency: Hourly for recent transactions, daily for settled transactions |
| Goal: Detect discrepancies (missing payments, double charges, incorrect amounts) |
Reconciliation Flow
graph TB
subgraph Internal Systems
Ledger[(Ledger DB
Expected Transactions)] end subgraph External Systems Bank[Bank Statements API
Actual Settlements] end subgraph Reconciliation Service Fetch[Fetch Transactions
Last Hour] Match{Match by
transaction_id
+ amount?} Resolve[Resolve Discrepancies] end subgraph Outcomes Success[Mark as RECONCILED] Missing[Missing in Bank
β Query PSP Status] Extra[Extra in Bank
β Flag for Review] Mismatch[Amount Mismatch
β Alert Finance Team] end Ledger --> Fetch Bank --> Fetch Fetch --> Match Match -->|Found in Both| Success Match -->|In Ledger, Not Bank| Missing Match -->|In Bank, Not Ledger| Extra Match -->|Amounts Differ| Mismatch Missing --> Resolve Extra --> Resolve Mismatch --> Resolve
Expected Transactions)] end subgraph External Systems Bank[Bank Statements API
Actual Settlements] end subgraph Reconciliation Service Fetch[Fetch Transactions
Last Hour] Match{Match by
transaction_id
+ amount?} Resolve[Resolve Discrepancies] end subgraph Outcomes Success[Mark as RECONCILED] Missing[Missing in Bank
β Query PSP Status] Extra[Extra in Bank
β Flag for Review] Mismatch[Amount Mismatch
β Alert Finance Team] end Ledger --> Fetch Bank --> Fetch Fetch --> Match Match -->|Found in Both| Success Match -->|In Ledger, Not Bank| Missing Match -->|In Bank, Not Ledger| Extra Match -->|Amounts Differ| Mismatch Missing --> Resolve Extra --> Resolve Mismatch --> Resolve
Reconciliation Logic
| Discrepancy Type | Root Cause | Resolution |
|---|---|---|
| In Ledger, Not in Bank | Payment authorized but not captured, or capture failed | Query PSP status, if failed β mark as FAILED, refund customer |
| In Bank, Not in Ledger | PSP sent settlement, but webhook never arrived | Create ledger entry retroactively, investigate webhook failure |
| Amount Mismatch | Currency conversion issue, or fee calculation error | Create adjustment entry in ledger, alert finance team |
| Duplicate in Bank | PSP processed payment twice (PSP bug) | Initiate chargeback to PSP, refund customer |
| Delayed Settlement | Bank statement lags by T+2 days | Normal, retry reconciliation after 3 days |
Reconciliation Job Schedule
| Job Type | Frequency | Scope | Purpose |
|---|---|---|---|
| Real-Time | Per transaction | Single payment | Immediate feedback on PSP response |
| Hourly | Every hour | Last 1 hour | Catch timeouts, delayed PSP responses |
| Daily | 3 AM UTC | Previous day | Match against bank statements (T+2) |
| Weekly | Sunday 1 AM | Previous 7 days | Final cleanup, detect long-delayed issues |
Alerting Thresholds
| Metric | Threshold | Action |
|---|---|---|
| Discrepancy Rate | > 0.1% | Page on-call engineer (indicates systemic issue) |
| Missing Transactions | > 10 in an hour | Investigate PSP connection, possible downtime |
| Amount Mismatches | > $1000 cumulative | Alert finance team, potential fee calculation bug |
| Reconciliation Job Failure | Any failure | Page immediately (blind to discrepancies) |
Interview Callout: Reconciliation is not optionalβit's how you detect silent failures. A payment system without reconciliation will slowly leak money through undetected errors.
9. Webhook System
Why Webhooks?
| Push vs Pull: Merchants get instant notifications instead of polling for status |
| Events: payment_intent.succeeded, payment_intent.failed, refund.created, etc. |
| Delivery Guarantee: At-least-once (may deliver duplicates, merchants must deduplicate) |
Webhook Architecture
graph LR
PS[Payment Service] -->|Publish Event| Queue[Kafka/SQS Queue]
Queue --> Worker1[Webhook Worker 1]
Queue --> Worker2[Webhook Worker 2]
Queue --> Worker3[Webhook Worker N]
Worker1 -->|POST /webhook| Merchant1[Merchant A Endpoint]
Worker2 -->|POST /webhook| Merchant2[Merchant B Endpoint]
Worker3 -->|POST /webhook| Merchant3[Merchant C Endpoint]
Merchant1 -->|200 OK| Worker1
Merchant1 -->|5xx Error| Retry[Retry Queue
Exponential Backoff] Retry -->|Retry 1: 10s| Worker1 Retry -->|Retry 2: 30s| Worker1 Retry -->|Retry 3: 1m| Worker1 Retry -->|Retry 10: 24h| DLQ[Dead Letter Queue
Manual Review]
Exponential Backoff] Retry -->|Retry 1: 10s| Worker1 Retry -->|Retry 2: 30s| Worker1 Retry -->|Retry 3: 1m| Worker1 Retry -->|Retry 10: 24h| DLQ[Dead Letter Queue
Manual Review]
Webhook Delivery Logic
| Step | Action | Details |
|---|---|---|
| 1. Publish Event | Payment service publishes to Kafka | Event payload includes: event_id, type, timestamp, data |
| 2. Worker Consumes | Webhook worker reads from queue | Scalable worker pool (autoscale based on queue depth) |
| 3. Sign Payload | Generate HMAC-SHA256 signature | Signature = HMAC(secret, event_payload), sent in header |
| 4. Send HTTP POST | POST to merchant endpoint with 30s timeout | Include signature in Stripe-Signature header |
| 5. Check Response | 2xx = success, 5xx = retry, 4xx = discard | Merchant should return 200 within 30s |
| 6. Retry Logic | Exponential backoff: 10s, 30s, 1m, 5m, ... 24h | Max 10 retries over 3 days |
| 7. Dead Letter Queue | After 10 failures, move to DLQ | Operations team manually investigates, contacts merchant |
Signature Verification (Merchant Side)
| Step | Implementation |
|---|---|
| 1. Extract Header | Read Stripe-Signature header from HTTP request |
| 2. Parse Signature | Format: t=1234567890,v1=abc123..., extract timestamp + signature |
| 3. Compute Expected | expected_sig = HMAC-SHA256(webhook_secret, timestamp + "." + body) |
| 4. Compare | If expected_sig == received_sig, webhook is authentic |
| 5. Check Timestamp | Reject if timestamp > 5 minutes old (replay attack prevention) |
Webhook Event Types
| Event Type | Trigger | Merchant Action |
|---|---|---|
| payment_intent.created | Payment intent created | Log for tracking |
| payment_intent.succeeded | Payment captured successfully | Fulfill order, ship product |
| payment_intent.payment_failed | Card declined or PSP error | Notify customer, prompt for new payment method |
| charge.refunded | Refund issued | Cancel shipment, update order status |
| charge.dispute.created | Customer initiated chargeback | Provide evidence to Stripe within 7 days |
| payout.paid | Funds transferred to merchant bank account | Update cash flow forecast |
Idempotency for Webhooks (Merchant Responsibility)
| Problem: Webhook may be delivered multiple times (network retry) |
| Solution: Merchant stores event_id, skips processing if already seen |
| Example: If "payment_succeeded" webhook arrives twice, only fulfill order once |
Interview Callout: Webhooks are at-least-once, not exactly-once. Merchants must handle duplicates. Always include event_id in payload so merchants can deduplicate.
10. Security & PCI Compliance
PCI DSS (Payment Card Industry Data Security Standard)
| Requirement | Implementation | Stripe's Approach |
|---|---|---|
| Never Store Raw Card Numbers | Tokenization: Replace card with token immediately | Stripe.js collects card, sends directly to Stripe (bypasses merchant) |
| Encryption at Rest | AES-256 encryption for all sensitive data | Tokens encrypted in HSM (Hardware Security Module) |
| Encryption in Transit | TLS 1.3 for all API communication | Enforce HTTPS, reject HTTP, certificate pinning |
| Network Segmentation | Isolate cardholder data environment (CDE) | Tokenization vault in separate VPC, no internet access |
| Access Control | Least privilege, MFA for all employees | Role-based access, audit logs for every action |
| Regular Audits | Annual PCI audit by Qualified Security Assessor (QSA) | Stripe maintains Level 1 PCI DSS compliance |
Tokenization Flow
sequenceDiagram
participant Browser
participant StripeJS as Stripe.js Client-Side
participant TokenVault as Tokenization Service - PCI Scope
participant MerchantAPI as Merchant Backend - Non-PCI
Browser->>StripeJS: User enters card: 4242 4242 4242 4242
StripeJS->>TokenVault: POST /v1/tokens
{card: 4242...} Note over TokenVault: Encrypt card in HSM
Generate token TokenVault-->>StripeJS: {token: tok_abc123} StripeJS->>MerchantAPI: POST /checkout
{token: tok_abc123} Note over MerchantAPI: Merchant never sees
raw card number MerchantAPI->>TokenVault: POST /v1/charges
{token: tok_abc123, amount: 1000} TokenVault-->>MerchantAPI: {charge: ch_xyz, status: succeeded}
{card: 4242...} Note over TokenVault: Encrypt card in HSM
Generate token TokenVault-->>StripeJS: {token: tok_abc123} StripeJS->>MerchantAPI: POST /checkout
{token: tok_abc123} Note over MerchantAPI: Merchant never sees
raw card number MerchantAPI->>TokenVault: POST /v1/charges
{token: tok_abc123, amount: 1000} TokenVault-->>MerchantAPI: {charge: ch_xyz, status: succeeded}
Security Layers
| Layer | Mechanism | Threat Mitigated |
|---|---|---|
| API Authentication | Secret API keys (sk_live_...), rotated quarterly | Unauthorized access to merchant accounts |
| Rate Limiting | 100 requests/sec per merchant, 1000/sec global | DDoS attacks, brute force |
| Fraud Detection | Radar ML model scores each transaction (0-100 risk) | Stolen cards, synthetic identity fraud |
| 3D Secure (3DS) | Redirect to bank for additional auth (password/SMS) | Card-not-present fraud (EU SCA requirement) |
| CVV Verification | Validate 3-digit CVV code, never stored | Card-present β card-not-present attacks |
| Address Verification (AVS) | Match billing address with card issuer records | Stolen card with incorrect address |
| Webhook Signatures | HMAC-SHA256 prevents payload tampering | Man-in-the-middle, replay attacks |
| Audit Logging | Immutable logs of every API call, admin action | Insider threats, forensic investigations |
PCI Scope Reduction
| Without Tokenization | With Tokenization |
|---|---|
| Merchant stores card numbers | Merchant only stores tokens |
| Entire merchant infrastructure in PCI scope | Only Stripe's tokenization vault in PCI scope |
| Annual PCI audit costs $50k-$500k | Merchant fills out simple SAQ-A form (self-assessment) |
| Breach of merchant DB exposes millions of cards | Breach exposes useless tokens (can't be reused) |
Interview Callout: Tokenization is the key to PCI compliance. By never touching raw card numbers, merchants dramatically reduce their compliance burden and risk.
11. Scalability & Availability
Horizontal Scaling Strategy
| Component | Scaling Approach | Bottleneck Mitigation |
|---|---|---|
| API Gateway | Stateless, auto-scale based on CPU (target: 70%) | Add instances in 30 seconds during traffic spikes |
| Payment Service | Stateless, scale to 1000+ instances | Partition by merchant_id hash for cache locality |
| Ledger Service | Stateless writers, append-only log | Shard PostgreSQL by merchant_id (100 shards) |
| Webhook Workers | Auto-scale based on Kafka queue depth | Target: queue lag < 10 seconds |
| PostgreSQL | Vertical scaling (large instances) + read replicas | Route reads to replicas, writes to primary |
| Redis | Redis Cluster with 100+ nodes | Hash slot sharding, replicas for failover |
| Kafka | Partitioned by merchant_id (1000 partitions) | Parallel webhook delivery per partition |
Multi-Region Architecture (Active-Active)
graph TB
subgraph US Region
LB1[Load Balancer]
API1[API Gateway]
PS1[Payment Service]
PG1[(PostgreSQL Primary)]
Redis1[(Redis Cluster)]
end
subgraph EU Region
LB2[Load Balancer]
API2[API Gateway]
PS2[Payment Service]
PG2[(PostgreSQL Primary)]
Redis2[(Redis Cluster)]
end
subgraph APAC Region
LB3[Load Balancer]
API3[API Gateway]
PS3[Payment Service]
PG3[(PostgreSQL Primary)]
Redis3[(Redis Cluster)]
end
Client1[US Client] -->|Low Latency| LB1
Client2[EU Client] -->|Low Latency| LB2
Client3[APAC Client] -->|Low Latency| LB3
PG1 -.->|Async Replication| PG2
PG2 -.->|Async Replication| PG3
PG3 -.->|Async Replication| PG1
Redis1 -.->|Idempotency Sync| Redis2
Redis2 -.->|Idempotency Sync| Redis3
Database Sharding Strategy
| Aspect | Design Decision | Rationale |
|---|---|---|
| Shard Key | merchant_id | Most queries scoped to single merchant, no cross-shard joins |
| Shard Count | 100 shards (expandable to 1000) | Balance between overhead and future growth |
| Routing | Consistent hashing: shard = hash(merchant_id) % 100 | Even distribution, minimal rebalancing when adding shards |
| Replication | 3 replicas per shard (1 primary, 2 read replicas) | Survive 2 failures, read scaling |
| Cross-Shard Queries | Analytics DB (Redshift) replicates from all shards | OLTP shards optimized for writes, OLAP for aggregations |
Event Sourcing for Audit Trail
| Definition: Store every state change as an event, reconstruct current state by replaying events |
| Benefit: Complete history, can replay to debug issues, audit compliance |
| Example: payment_intent_created β payment_authorized β payment_captured β payment_settled |
| Storage: Kafka topic with infinite retention (compaction for snapshots) |
Availability Tactics
| Tactic | Implementation | Impact on Availability |
|---|---|---|
| No Single Point of Failure | Every component replicated 3x across AZs | Survive zone failure (3% β 0.001% failure rate) |
| Health Checks | Load balancer pings /health every 5s, removes unhealthy instances | Automatic failover in 15 seconds |
| Circuit Breakers | Stop calling failed PSPs, route to backup | Prevent cascading failures |
| Graceful Degradation | If fraud check times out, allow payment (review later) | Maintain core functionality during partial outage |
| Database Failover | Automatic promotion of replica to primary in 30s | Recover from primary failure quickly |
| Chaos Engineering | Randomly kill 1% of instances daily (Chaos Monkey) | Validate resilience continuously |
Capacity Planning
| Metric | Current | Peak | Headroom |
|---|---|---|---|
| API Instances | 100 | 300 (Black Friday) | Auto-scale to 500 max |
| Database Connections | 1000/shard | 3000/shard | PgBouncer connection pooling |
| Kafka Throughput | 50k events/sec | 150k events/sec | Provision for 200k/sec |
| Webhook Workers | 500 | 2000 | Auto-scale based on queue lag |
Interview Callout: 99.999% availability means only 5 minutes of downtime per year. Achieve this through multi-region active-active deployment, eliminating all single points of failure, and aggressive failover automation.
Summary: Key Interview Talking Points
| Topic | Key Point |
|---|---|
| Idempotency | Use idempotency keys to prevent double charges on retries, cache responses for 24h |
| Two-Phase Commit | Authorize holds funds, capture charges them (within 7 days), settle transfers to merchant (T+2) |
| Ledger | Double-entry bookkeeping ensures sum(debits) = sum(credits), immutable append-only log |
| PSP Integration | Circuit breaker pattern for failover, query status API on timeout (never assume failure) |
| Reconciliation | Match internal ledger with bank statements hourly, detect and resolve discrepancies |
| Webhooks | At-least-once delivery, exponential backoff retry, HMAC signatures for security |
| PCI Compliance | Tokenization removes merchant from PCI scope, never store raw card numbers |
| Scalability | Shard by merchant_id, multi-region active-active, event sourcing for audit trail |
| Availability | 99.999% via no single points of failure, automatic failover, chaos engineering |
Clarifying Questions to Ask in Interview
| 1. What transaction volume are we targeting? (10 TPS vs 10,000 TPS changes design) |
| 2. Are we building the full PSP or integrating with existing PSPs? |
| 3. What payment methods? (Cards only vs ACH, wire, crypto) |
| 4. Multi-currency support? (FX conversion, regional PSPs) |
| 5. Is this B2C or B2B? (B2B may need invoicing, net-30 terms) |
| 6. What's the acceptable latency? (Checkout should be <3s end-to-end) |
| 7. Are we responsible for fraud detection or integrating third-party? |
Operational Readiness (Production-Grade)
Service Level Indicators (SLIs)
- Payment authorization P99 < 3s: Time from API request to PSP authorization response (includes network + PSP processing)
- Success rate > 99.5%: Ratio of successful payment intents to total payment attempts (excludes legitimate declines)
- Reconciliation discrepancy < 0.01%: Ratio of mismatched transactions to total daily volume (internal ledger vs bank statements)
Monitoring & Alerting
- PSP response times: Track P50/P95/P99 latency per PSP (alert if P95 > 5s for any PSP, trigger circuit breaker)
- Payment success/failure rates by PSP: Monitor success rate per PSP (alert if drops below 95%, route to backup PSP)
- Ledger balance drift: Daily sum of all debit entries must equal sum of all credit entries (alert if mismatch > $100, critical bug)
- Webhook delivery rate: Track webhook delivery success rate (alert if < 95%, indicates merchant endpoint issues or queue backup)
Deployment Strategy
- Shadow mode: New PSP integration processes payments in parallel, results compared but not used (validation mode before cutover)
- Canary for API changes: Deploy breaking API changes to 1% of merchants first, monitor error rates and support tickets for 48 hours
Circuit Breakers
- Payment service β PSP: If PSP error rate > 50% over 1 minute, open circuit and fallback to backup PSP (restore after 60s cooldown)
- Webhook delivery β merchant endpoint: If merchant endpoint returns 5xx for 5 consecutive attempts, send to DLQ after exponential backoff retries
Disaster Recovery
- Ledger is source of truth: Event-sourced ledger never lost (replicated to 3 regions synchronously), all other state can be rebuilt
- Multi-region active-passive for payment processing: US-East primary, US-West standby with 1-minute replication lag (RPO: 1 min), DNS failover RTO: 5 minutes
- RPO: 0 (synchronous ledger writes): Ledger writes acknowledged only after replication to 2+ regions (strong consistency guarantee)
Testing
- Chaos: simulate PSP timeout - Inject 30s timeout to 10% of PSP calls, verify payment marked PENDING and background job recovers state
- Chaos: duplicate webhook delivery - Send same webhook event twice, verify merchant deduplicates using event_id
- Chaos: clock skew between services - Advance clock on one service by 5 minutes, verify idempotency keys still work (use timestamp tolerance)