Think Beyond the Happy Path
Payment systems aren't just about moving money — they're about exactly-once processing, PCI-compliant security, and reliable webhook delivery that merchants can depend on.
Before diving into the material, take a moment to ask yourself:
- Do you know how the system stays secure — what's the role of tokenization, and how do you avoid storing raw card numbers while still supporting recurring payments?
- Do you know how to guarantee exactly-once processing — what happens if a network timeout occurs after the bank approves but before you record the result?
- Do you know how to design reliable webhook delivery — how do you handle merchant endpoints that are temporarily down without losing payment notifications?
- Do you know how to handle partial failures in settlement — what happens when you've debited the customer but the merchant payout fails?
You don't need to answer all of these right away. A Senior/Staff+ Engineer doesn't stop at basic functionality — they anticipate edge cases, design for resilience, and push for production-grade reliability.
But great systems start with great questions. What would you ask next?
Problem Statement
Design a Stripe-like payment system that supports payment initiation, authorization, status tracking, and daily batch settlement. The system should:
- Allow merchants to submit payment requests.
- Allow customers to complete payments using a payment method.
- Provide real-time status tracking for merchants.
- Settle all authorized payments once daily. And the scale is up to 10,000 QPS sustained throughput.
What is Stripe?
Stripe is a modern payment processing platform that enables businesses to accept online payments quickly and securely. It abstracts the complexities of payment infrastructure—such as integrating with card networks, handling authorization and settlement, and complying with financial regulations—into a set of developer-friendly APIs.
Basic Payment Workflow in Stripe
- Payment Initiation: A user submits a payment request (e.g., credit card purchase) through a merchant's website or app.
- Authorization: Stripe forwards the request to an external payment provider (e.g., Visa, Mastercard, or a bank) to authorize the payment. If approved, the funds are held (not immediately transferred).
- Status Tracking: The merchant or user can check the status of the payment (e.g., authorized, declined, pending).
- Batch Settlement: Once per day, Stripe aggregates authorized payments and submits them to the payment network for actual fund transfer and final settlement.
Functional Requirements
FR1 – Merchant can submit a payment request
The system should allow a merchant to initiate a payment with amount, currency, and customer details.
FR2 – Customer can complete the payment using a payment method
The system should accept and process a customer's payment method and trigger authorization with the external provider.
FR3 – Merchant can view the current status of any payment
The system should return real-time or persisted status updates of a payment, including authorization outcome and settlement state.
FR4 – System should settle all authorized payments once daily
The system should batch authorized payments at the end of each day and submit them for final settlement with the external provider.
Non-Functional Requirements
NFR1 – Security — No confidential data leak
The system must protect all sensitive payment data by enforcing industry-standard security measures: encrypting all traffic with TLS 1.2 or higher, encrypting stored data with AES-256, and enforcing role-based access control and audit logging. This is essential to prevent data breaches, meet regulatory requirements, and build trust with users and financial partners.
NFR2 – Strong Consistency — Exactly-once processing, no duplicate settlement
The system must ensure that each payment is processed exactly once and moves through a well-defined state machine (e.g., pending → authorized → settled) without duplication or loss. Inaccurate payment states can lead to financial loss, double charges, or regulatory issues, which are unacceptable in a production payment system.
NFR3 – Durability — ≥ 99.9999999% (nine 9s) data durability
All accepted payments must be written to a durable, replicated data store before acknowledging the client. A payment system cannot lose data due to crashes, network failures, or region outages; persistence guarantees are foundational to user trust and financial correctness.
NFR4 – High Scalability — ≥ 10,000 QPS sustained throughput
The system must be able to ingest and process at least 10,000 payments per second at steady load, with the ability to scale horizontally across stateless services and background workers. This ensures the system can handle real-world traffic spikes, global usage, and future growth without becoming a bottleneck.
Requirement Summary
| Functional Requirements (FRs) | |
|---|---|
| Name | Description |
1. Submit Payment Request | Merchant initiates a payment with amount, currency, and customer details. |
2. Complete Payment | Customer completes payment using a payment method, triggering authorization. |
3. View Payment Status | Merchant views real-time status including authorization outcome and settlement state. |
4. Daily Settlement | System batches authorized payments daily and submits for final settlement. |
| Non-Functional Requirements (NFRs) | |
|---|---|
| Name | Description |
1. Security | No confidential data leak; TLS 1.2+, AES-256, RBAC, audit logging. |
2. Strong Consistency | Exactly-once processing, no duplicate settlement. |
3. Durability | ≥ 99.9999999% (nine 9s) data durability. |
4. High Scalability | ≥ 10,000 QPS sustained throughput. |
Core Entities
To tie these requirements together into a coherent architecture, we begin with three foundational building blocks: the Merchant, the Payment, and the Transaction.
Merchant — The Business Entity
Represents a business integrating with the payment platform to accept payments.
| Merchants Table | |||
|---|---|---|---|
| Field | Type | Key | Description |
merchant_id | UUID | PK | Unique ID for the merchant. |
name | String | NOT NULL | Merchant or business name. |
api_key | String | NOT NULL | Credential used to authenticate API calls. |
status | Enum | NOT NULL | ACTIVE, SUSPENDED, INACTIVE. |
created_at | Timestamp | NOT NULL | When the merchant was onboarded. |
Payment — The Business Intent
Represents a payment request initiated by a merchant and fulfilled by a customer.
| Payments Table | |||
|---|---|---|---|
| Field | Type | Key | Description |
payment_id | UUID | PK | Unique ID for the payment. |
merchant_id | UUID | FK → merchants(merchant_id) | Merchant receiving the payment. |
amount | Decimal | NOT NULL | Payment amount. |
currency | String | NOT NULL | ISO code (e.g., USD). |
status | Enum | NOT NULL | PENDING, AUTHORIZED, DECLINED, SETTLED. |
customer_info | Object | NULLABLE | Optional customer metadata (e.g., ID, email, session token). |
created_at | Timestamp | NOT NULL | When the payment was initiated. |
payment_info | Object | NULLABLE | Payment Raw Data, including credit card number, cvc etc. |
[Common Pitfall]
Many candidates include fields like this in their Payment entity:
"payment_info": {
"card_number": "4242 4242 4242 4242",
"exp_month": 12,
"exp_year": 2026,
"cvc": "123"
}
This instantly implies that your backend:
- Receives raw card data (puts you in PCI DSS Level 1 scope)
- Might store or log that data somewhere (a major compliance breach)
- Doesn't properly separate data handling responsibilities
[Great Answer]
We use tokenization to avoid handling raw payment data. The customer submits their card directly to Stripe (or another vault provider), and our backend receives a token like pm_12345abc, which we store in the payments table. That ensures we never touch sensitive data directly. (Please check Deep Dive 1 later for updated schema)
Transaction — A Specific Processing Step
Represents a discrete step taken to process a payment — such as authorization or settlement.
| Transactions Table | |||
|---|---|---|---|
| Field | Type | Key | Description |
transaction_id | UUID | PK | Unique ID for the transaction. |
payment_id | UUID | FK → payments(payment_id) | Which payment this transaction belongs to. |
type | Enum | NOT NULL | AUTHORIZATION, SETTLEMENT, REFUND. |
status | Enum | NOT NULL | SUCCESS, FAILED, RETRYING. |
provider_id | String | NULLABLE | External payment provider reference (e.g., auth code). |
actor_type | Enum | NOT NULL | CUSTOMER, MERCHANT, SYSTEM — who triggered the action. |
response_code | String | NULLABLE | Provider result code or message. |
processed_at | Timestamp | NULLABLE | Timestamp of when this transaction was processed. |
Payment Terminology
Many candidates don't have experience in payment system development, so we provide you some fundamental understandings on the core concepts. Still we use real-world analogy, i.e. Stripe
- Payment = The "PaymentIntent" in Stripe — a complete business unit initiated by a merchant.
- Transaction = A processing event like an authorization, a capture, or a refund attempt.
Relationship
- Each Payment can have many Transactions (1 to N)
- Transactions are always tied to a specific
payment_id - Transactions allow retries, multiple steps, and eventual settlement tracking without modifying the original Payment intent
Comparison
| Payment vs Transaction | ||
|---|---|---|
| Category | Payment | Transaction |
Definition | A business-level intent to transfer funds | A processing-level record of an action taken on a payment |
Granularity | One payment per customer/merchant event | One or more transactions per payment |
Examples | Charge $120 to customer for an order | Authorize $120, Settle $120, Refund $120 |
Lifespan | Created once per user request | May have multiple entries (e.g., retries, stages, failures) |
Status | PENDING, AUTHORIZED, DECLINED, SETTLED | SUCCESS, FAILED, RETRYING |
Purpose | Represents the logical flow the merchant/user cares about | Captures the actual steps taken to fulfill the payment |
Who cares? | Exposed to external clients (e.g., merchants, UIs, APIs) | Mostly internal (for processing, retries, audit, reconciliation) |