system-design57 min read

How to Design a Payment Processing System - A Senior+ Guide | Ayodhyya

Designing a Payment Processing System

A comprehensive deep-dive into building a Stripe-like payment infrastructure at scale

Senior+ Architecture Guide 20 Sections Estimated Read: 50 min

1. Introduction - Why Payment Systems Are Hard

Designing a payment processing system is one of the most challenging problems in software engineering. Unlike most distributed systems, payment processing demands absolute correctness. A lost tweet is annoying; a lost payment is catastrophic. When Stripe, Square, or Adyen process billions of dollars per day, there is zero tolerance for data loss, duplicate charges, or inconsistent state. The system must be simultaneously highly available, strongly consistent, and globally scalable - a set of requirements that are often at odds with one another in distributed systems theory.

A payment processing system sits at the intersection of multiple complex domains: financial regulations, banking networks, fraud prevention, cryptography, distributed systems, and real-time event processing. Every design decision carries implications not just for performance, but for legal compliance, financial accuracy, and customer trust. A single bug can result in millions of dollars in incorrect charges, regulatory fines, or the loss of a banking partner.

In this guide, we will design a complete payment processing system from the ground up - similar in scope to what Stripe, Adyen, or PayPal's core infrastructure provides. We will cover everything from low-level tokenization of card data, through multi-party settlement and ledger design, to webhook delivery guarantees and fraud detection pipelines. The system we design will handle card payments, bank transfers, subscription billing, multi-currency transactions, refunds, disputes, and reconciliation - all while maintaining PCI DSS compliance and financial accuracy.

Target Audience: This article assumes familiarity with distributed systems fundamentals (CAP theorem, consensus protocols, eventual consistency), database internals, and basic financial concepts. We will build upon these foundations throughout the design. If you are preparing for a Staff or Principal Engineer interview, this guide will give you a thorough understanding of how production payment systems operate at scale, including the subtle failure modes that senior engineers must anticipate and design around.

Why This Problem Matters

The global digital payments market processes over 9 trillion dollars annually and is growing at roughly 15 percent per year. Every e-commerce transaction, subscription service, marketplace payout, and peer-to-peer transfer flows through some form of payment processing infrastructure. The systems that handle this volume must operate with the reliability characteristics of critical infrastructure - think power grids, not typical web applications.

The unique challenges of payment systems include:

  • Financial correctness is non-negotiable: Every cent must be accounted for. There is no eventually consistent when it comes to money - the system must guarantee that every transaction is recorded exactly once with absolute precision.
  • External system dependencies: Payment systems must integrate with banks, card networks (Visa, Mastercard), payment processors, and fraud detection services - all of which have their own latency characteristics, availability guarantees, and failure modes.
  • Regulatory compliance: PCI DSS, PSD2, SOX, GDPR, and dozens of other regulations impose strict requirements on how cardholder data is stored, processed, and transmitted.
  • Multi-party settlement: A single card payment involves the merchant, the payment facilitator, the acquiring bank, the card network, and the issuing bank - each needing accurate financial records.
  • Global operations: Currencies, payment methods, banking regulations, and fraud patterns vary dramatically by region. A system designed for the US must also work seamlessly in Japan, Brazil, and Nigeria.

Lessons from Production Systems

Having worked with and studied payment infrastructure at scale, several hard-won lessons stand out. First, never underestimate the complexity of idempotency. Network partitions, retries, and partial failures mean that every payment operation must be safely retryable without creating duplicates. Second, your ledger is sacred - it must be immutable, append-only, and designed to survive catastrophic failures. Third, reconciliation is not optional - it is a continuous, real-time process that catches discrepancies before they compound. Fourth, the most dangerous bugs in payment systems are the ones that appear to work correctly under normal conditions but fail silently during edge cases - partial authorizations, network timeouts mid-transaction, or race conditions in concurrent payment attempts.

Key Design Principles

The entire system is built around five non-negotiable principles. First, correctness over performance - we will always choose the slower, correct path over the faster, risky one. Second, defense in depth - multiple layers of validation, encryption, and consistency checks ensure that a failure at any single layer does not compromise the system. Third, auditability - every action must be traceable, every state change must be recorded, and the complete history must be reconstructable. Fourth, graceful degradation - when external dependencies fail, the system must degrade gracefully rather than failing completely. Fifth, financial completeness - every dollar must be accounted for at every stage, from the initial authorization through settlement and payout.

2. Functional and Non-Functional Requirements

Functional Requirements

Before designing any system, we must precisely define what it needs to do. For a payment processing platform, the functional requirements span several categories that cover every aspect of money movement and financial record-keeping.

Core Payment Operations

  • Charge Processing: Accept payment details, tokenize sensitive data, route to the appropriate payment processor, handle authorization, and capture funds. Support one-time payments as well as authorized-but-not-captured flows (common in hospitality and car rental).
  • Payment Methods: Store and manage customer payment methods - credit/debit cards, bank accounts (ACH), digital wallets (Apple Pay, Google Pay), and local payment methods (iDEAL, SEPA, Boleto). Each method has different authorization and settlement characteristics.
  • Refunds and Partial Refunds: Support full and partial refunds against original transactions, with proper ledger entries and settlement adjustments.
  • Disputes and Chargebacks: Handle the full lifecycle of payment disputes - from receiving a chargeback notification, through evidence submission, to final resolution. This involves strict timelines and specific response formats dictated by card networks.
  • Subscriptions and Recurring: Support recurring billing with configurable intervals, trial periods, proration, plan changes, and dunning (automatic retry of failed payments).

Financial Operations

  • Multi-Currency: Process payments in 135+ currencies with real-time or daily foreign exchange rates. Support currency conversion at the payment level and at the settlement level.
  • Payouts and Settlement: Aggregate merchant transactions and disburse funds on configurable schedules (daily, weekly, monthly), accounting for refunds, chargebacks, and fees.
  • Ledger and Double-Entry Bookkeeping: Maintain a complete, immutable audit trail of every financial event with proper double-entry accounting where every debit has a corresponding credit.
  • Reconciliation: Automatically match internal records against processor reports, bank statements, and network settlement files to detect discrepancies.

Integration and Developer Experience

  • REST API: Comprehensive API covering all payment operations with consistent design, pagination, filtering, and sorting.
  • Webhooks: Real-time event delivery for payment state changes with guaranteed delivery semantics.
  • Sandboxes and Test Mode: Full simulation environment for development and testing, including test card numbers for various scenarios (success, decline, 3DS, fraud).
  • SDKs: Client-side libraries for tokenization (JS, iOS, Android) that keep sensitive data off merchant servers.

Non-Functional Requirements

Requirement Target Rationale
Availability 99.999% (five nines) Downtime directly equals lost revenue; a 5-minute outage during peak can cost millions
Latency (p50/p99) 100ms / 500ms Checkout flows must feel instant; card network timeouts are typically 30s
Throughput 50,000+ TPS peak Must handle flash sales, Black Friday traffic, and viral product launches
Durability Zero data loss Every payment record must survive any single failure; financial records cannot be lost
Consistency Strong consistency for financial records Double-entry bookkeeping requires ACID guarantees; cannot accept eventual consistency for ledger
Security PCI DSS Level 1 Mandatory for any entity processing card payments at scale
Global Latency 200ms from any continent Global merchants expect consistent performance regardless of geography
Data Retention 7+ years Regulatory and audit requirements mandate long-term retention of financial records
Key Insight: The availability target of 99.999% means approximately 5.26 minutes of downtime per year. This requires active-active multi-region deployment with automatic failover, as no single region or availability zone can achieve this target alone. Design for this from day one - retrofitting multi-region active-active onto a system designed for single-region operation is extraordinarily difficult.

3. Capacity Estimation and Scale

Understanding the scale of a payment processing system is critical for making appropriate technology choices. Let us work through the numbers for a Stripe-scale platform processing hundreds of millions of transactions daily.

Transaction Volume

Stripe processes approximately 500 million transactions per day. Let us use this as our baseline and design for growth:

  • Daily transactions: 500 million
  • Average transactions per second (TPS): 500M / 86,400 = approximately 5,800 TPS average
  • Peak TPS (5x average): approximately 30,000 TPS during Black Friday or major sales events
  • Peak burst TPS: approximately 100,000+ TPS for flash sales (must be handled via queuing and rate limiting)

Data Volume Estimates

Data Type Record Size Daily Volume Daily Storage Annual Storage
Payment Intent 2 KB 500M 1 TB 365 TB
Authorization Response 0.5 KB 500M 250 GB 91 TB
Ledger Entry 1 KB 1.5B (avg 3 entries per txn) 1.5 TB 547 TB
Audit Log 0.3 KB 2.5B (avg 5 events per txn) 750 GB 274 TB
Webhook Events 1 KB 1B 1 TB 365 TB
Total ~4.5 TB/day ~1.6 PB/year

Network and Bandwidth

  • Inbound (API requests): 500M requests times 2 KB average equals approximately 1 TB per day, approximately 12 MB/s average, approximately 120 MB/s peak
  • Outbound (API responses plus webhooks): approximately 1 TB per day, approximately 12 MB/s average
  • Internal (inter-service): approximately 5 TB per day of internal service-to-service communication
  • Total daily network transfer: approximately 7 TB across all tiers

Cache and Hot Data

  • Payment method tokens: approximately 50M unique active payment methods times 500 bytes equals approximately 25 GB (fits in a few Redis clusters)
  • Fraud rules and models: approximately 1 GB of model parameters, updated hourly
  • FX rates: approximately 50 KB of rate data, updated every 15-60 seconds
  • Merchant configuration: approximately 10 GB total, heavily read, rarely written
Key Takeaway: The dominant cost driver in a payment system is not compute - it is storage and the write throughput to durable storage. With 4.5 TB per day of new data and 7+ year retention requirements, you must design your storage architecture for petabyte-scale from the beginning. This means partitioned databases, tiered storage (hot/warm/cold), and careful attention to write amplification in your storage engine.

Query Patterns

Understanding access patterns is essential for choosing the right storage technologies:

  • Point lookups by payment ID: approximately 40% of all reads - must be sub-millisecond
  • Lookups by customer ID (recent payments): approximately 25% of all reads
  • Merchant dashboard queries (date range plus filters): approximately 15% of reads, can tolerate 100-200ms
  • Reconciliation batch queries: approximately 10% of reads, scan-heavy, can run on replicas
  • Analytics and reporting queries: approximately 10% of reads, complex aggregations, run on data warehouse

Storage Technology Choices

Given the scale and access patterns, the recommended storage stack is:

  • Primary OLTP: PostgreSQL with Citus extension for horizontal sharding. The strong ACID guarantees are essential for ledger operations, while Citus provides the horizontal scaling needed for 500M+ rows.
  • Cache layer: Redis Cluster for hot data (payment methods, fraud models, FX rates, idempotency keys). Typically 3-5 shards with replicas.
  • Event streaming: Apache Kafka for event sourcing, webhook delivery, and inter-service communication. Retain events for 7+ days for replay capability.
  • Data warehouse: ClickHouse or BigQuery for analytics, reporting, and reconciliation queries that scan large date ranges.
  • Object storage: S3 or GCS for long-term archival of settlement files, dispute evidence, and compliance documents.

4. Data Model and Entity Design

The data model of a payment system is its most critical design element. Get it wrong, and you will spend years fighting inconsistencies. Get it right, and the rest of the system becomes dramatically simpler. The core principle is that the data model must make illegal states unrepresentable - if a payment cannot logically be in a certain state, the schema should prevent it.

Core Entities

C#
public enum PaymentStatus
{
    Pending,
    Authorized,
    Captured,
    PartiallyCaptured,
    Settled,
    Refunded,
    PartiallyRefunded,
    Disputed,
    WonDispute,
    LostDispute,
    Cancelled,
    Failed
}

public enum PaymentMethodType
{
    Card,
    BankAccount,
    DigitalWallet,
    LocalPaymentMethod
}

public class PaymentIntent
{
    public string Id { get; set; }                    // pi_xxxxxxxxxxxx (prefixed, collision-resistant)
    public string MerchantId { get; set; }            // References the merchant account
    public string CustomerId { get; set; }            // Optional, for returning customers
    public long Amount { get; set; }                  // In smallest currency unit (cents)
    public string Currency { get; set; }              // ISO 4217 uppercase (USD, EUR, JPY)
    public PaymentStatus Status { get; set; }
    public string PaymentMethodId { get; set; }       // Tokenized payment method reference
    public string IdempotencyKey { get; set; }        // Client-provided, unique per operation
    public Dictionary<string, string> Metadata { get; set; }
    public PaymentCapabilities Capabilities { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? CapturedAt { get; set; }
    public DateTime? SettledAt { get; set; }
    public long AmountReceived { get; set; }
    public long AmountRefunded { get; set; }
    public List<Charge> Charges { get; set; }
    public List<Refund> Refunds { get; set; }
    public string ClientSecret { get; set; }
    public string NextAction { get; set; }
}

public class Charge
{
    public string Id { get; set; }                     // ch_xxxxxxxxxxxx
    public string PaymentIntentId { get; set; }
    public long Amount { get; set; }
    public long AmountCaptured { get; set; }
    public string Currency { get; set; }
    public PaymentMethodType PaymentMethodType { get; set; }
    public string ProcessorTransactionId { get; set; }
    public AuthorizationResult Authorization { get; set; }
    public bool IsLive { get; set; }
    public DateTime CreatedAt { get; set; }
    public List<Fee> Fees { get; set; }
    public Outcome Outcome { get; set; }
}

public class AuthorizationResult
{
    public string AuthorizationCode { get; set; }
    public string ResponseCode { get; set; }
    public string DeclineReason { get; set; }
    public string NetworkTransactionId { get; set; }
    public string AvsResult { get; set; }
    public string CvvResult { get; set; }
    public string ProcessorResponseId { get; set; }
    public RiskAssessment Risk { get; set; }
    public DateTime AuthorizedAt { get; set; }
    public DateTime AuthorizationExpiry { get; set; }
}

public class RiskAssessment
{
    public decimal Score { get; set; }                 // 0.0 - 99.9
    public RiskLevel Level { get; set; }
    public List<string> TriggeredRules { get; set; }
    public FraudCheckResult FraudCheck { get; set; }
    public bool RequiresThreeDS { get; set; }
}

public enum RiskLevel
{
    Low,
    Medium,
    High,
    VeryHigh
}

Refund and Dispute Entities

C#
public class Refund
{
    public string Id { get; set; }                     // re_xxxxxxxxxxxx
    public string PaymentIntentId { get; set; }
    public string ChargeId { get; set; }
    public long Amount { get; set; }
    public string Currency { get; set; }
    public RefundStatus Status { get; set; }
    public string Reason { get; set; }
    public string ProcessorRefundId { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? CompletedAt { get; set; }
    public Dictionary<string, string> Metadata { get; set; }
}

public enum RefundStatus
{
    Pending,
    Succeeded,
    Failed,
    Cancelled
}

public class Dispute
{
    public string Id { get; set; }                     // dp_xxxxxxxxxxxx
    public string PaymentIntentId { get; set; }
    public string ChargeId { get; set; }
    public long Amount { get; set; }
    public string Currency { get; set; }
    public DisputeReason Reason { get; set; }
    public DisputeStatus Status { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime EvidenceDueBy { get; set; }
    public Evidence Evidence { get; set; }
    public DateTime? ResolvedAt { get; set; }
    public string Resolution { get; set; }
}

public enum DisputeStatus
{
    NeedsResponse,
    UnderReview,
    Won,
    Lost,
    WarningNeedsResponse,
    WarningUnderReview
}

public class MerchantAccount
{
    public string Id { get; set; }                     // acct_xxxxxxxxxxxx
    public string BusinessName { get; set; }
    public string Country { get; set; }
    public string DefaultCurrency { get; set; }
    public MerchantStatus Status { get; set; }
    public PayoutSchedule PayoutSchedule { get; set; }
    public FeeStructure Fees { get; set; }
    public VerificationStatus Verification { get; set; }
    public List<string> SupportedCurrencies { get; set; }
    public Capabilities EnabledCapabilities { get; set; }
    public DateTime CreatedAt { get; set; }
    public BusinessProfile BusinessProfile { get; set; }
    public RiskSettings RiskSettings { get; set; }
}

Payment Method Storage

Payment methods require special handling due to PCI requirements. We never store raw card numbers - instead, we use tokenized references that point to encrypted data in the PCI vault.

C#
public class PaymentMethod
{
    public string Id { get; set; }                     // pm_xxxxxxxxxxxx
    public string CustomerId { get; set; }
    public PaymentMethodType Type { get; set; }
    public BillingDetails BillingDetails { get; set; }
    public CardDetails Card { get; set; }
    public BankAccountDetails BankAccount { get; set; }
    public DateTime CreatedAt { get; set; }
    public bool IsDefault { get; set; }
    public DateTime? LastUsedAt { get; set; }
    public int Fingerprint { get; set; }
}

public class CardDetails
{
    public string Brand { get; set; }                  // visa, mastercard, amex
    public string Funding { get; set; }                // credit, debit, prepaid
    public string Country { get; set; }
    public int ExpMonth { get; set; }
    public int ExpYear { get; set; }
    public string Last4 { get; set; }                  // Last 4 digits (safe to store)
    public string Fingerprint { get; set; }
    public string IssuerName { get; set; }
    public List<string> SupportedNetworks { get; set; }
    public ThreeDSecureStatus ThreeDSecure { get; set; }
    public string NetworkToken { get; set; }
}

public class BillingDetails
{
    public string Name { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public Address Address { get; set; }
}
Partitioning Strategy: All payment-related tables should be partitioned by merchant_id as the first level and by time as the second level. This ensures that queries for a specific merchant are localized to specific partitions, enabling efficient range scans for merchant dashboards while maintaining write distribution across the cluster. The payment_intents table alone will grow to hundreds of billions of rows over several years - without partitioning, no single database instance can handle this.

Schema Design Principles

  • Immutable event log: Core entities (PaymentIntent, Charge, Refund) are append-only. State changes create new records rather than updating existing ones, providing a complete audit trail.
  • Prefixed IDs: Every entity type has a unique prefix (pi_, ch_, re_, dp_, pm_, acct_) that makes it immediately identifiable and prevents cross-type ID collisions.
  • Amounts in smallest currency units: All monetary amounts are stored as longs (cents, not dollars) to avoid floating-point precision issues. This is critical - .99 is stored as 1999.
  • Separation of concerns: The payment flow (PaymentIntent), the financial record (Charge), the accounting record (Ledger), and the customer-facing object (PaymentMethod) are distinct entities with clear boundaries.
  • Soft deletes: Financial records are never physically deleted. Instead, they are marked with a deleted_at timestamp and excluded from normal queries. This preserves the complete audit trail for compliance and debugging.

5. High-Level Architecture

The architecture of a payment processing system follows a layered approach with clear separation of concerns. Each layer has specific responsibilities and communicates with adjacent layers through well-defined interfaces. The system is designed as a set of microservices, each owning its data and communicating asynchronously where possible.

graph TB subgraph "Client Layer" ClientSDK["Client SDK (JS / iOS / Android)"] DashboardAPI["Dashboard API"] end subgraph "API Gateway and Edge" RateLimit["Rate Limiter and Throttling"] APIGateway["API Gateway (Auth, Routing, Validation)"] end subgraph "Core Services" PaymentService["Payment Service (Intent Lifecycle)"] ChargeService["Charge Service (Auth and Capture)"] RefundService["Refund Service"] DisputeService["Dispute Service"] PayoutService["Payout and Settlement"] SubscriptionService["Subscription and Billing"] WebhookService["Webhook Delivery (Event Bus)"] end subgraph "Supporting Services" TokenVault["Token Vault (PCI Vault)"] FraudService["Fraud Detection and Risk Engine"] FXService["FX Rate Service"] IdempotencyService["Idempotency (Redis + DB)"] end subgraph "Financial Layer" LedgerService["Ledger Service (Double-Entry)"] ReconciliationService["Reconciliation Engine"] ReportingService["Reporting and Analytics"] end subgraph "Integration Layer" ProcessorRouter["Processor Router"] StripeAdapter["Stripe Adapter"] AdyenAdapter["Adyen Adapter"] BankAdapter["Bank/ACH Adapter"] CardNetwork["Card Network (Visa/MC/Amex)"] end subgraph "Data Layer" PrimaryDB[("Primary DB (PostgreSQL Cluster)")] Redis[("Redis Cluster")] EventStore[("Event Store (Kafka)")] DataWarehouse[("Data Warehouse")] end ClientSDK --> RateLimit DashboardAPI --> APIGateway RateLimit --> APIGateway APIGateway --> PaymentService APIGateway --> ChargeService APIGateway --> RefundService PaymentService --> TokenVault PaymentService --> FraudService PaymentService --> FXService ChargeService --> ProcessorRouter ChargeService --> LedgerService ProcessorRouter --> StripeAdapter ProcessorRouter --> AdyenAdapter StripeAdapter --> CardNetwork AdyenAdapter --> CardNetwork WebhookService --> EventStore FraudService --> Redis PaymentService --> PrimaryDB LedgerService --> PrimaryDB

Layer Responsibilities

Layer Components Responsibility Latency Budget
Client/Edge SDK, API Gateway, Rate Limiter Authentication, input validation, DDoS protection, request routing less than 10ms
Core Payment, Charge, Refund, Dispute Business logic, state machine management, orchestration less than 50ms
Supporting Token Vault, Fraud, FX, Idempotency Cross-cutting concerns, security, risk assessment less than 30ms cached, less than 200ms API calls
Financial Ledger, Reconciliation, Reporting Accounting accuracy, compliance, financial integrity Async (non-blocking for payments)
Integration Processor Router, Adapters External system integration, protocol translation, failover less than 2s (includes network I/O)
Data DB, Cache, Event Store Durable storage, caching, event streaming, analytics less than 5ms cache, less than 20ms DB
Architectural Decision - Sync vs. Async: The payment authorization path must be synchronous from the client perspective - they need to know immediately if their payment succeeded. However, downstream operations (ledger posting, webhook delivery, fraud scoring, reconciliation) can and should be asynchronous. The key insight is that the synchronous path must be as narrow as possible, touching only the minimum number of services needed to return a definitive answer to the caller.

Multi-Region Deployment

For a payment system requiring 99.999% availability, multi-region active-active deployment is mandatory. The architecture uses a geo-distributed database cluster with automatic failover:

  • US-East (Primary): Serves approximately 50% of global traffic, hosts primary database write leader
  • EU-West: Serves approximately 30% of global traffic, hosts EU merchant data, acts as disaster recovery for US
  • AP-Southeast: Serves approximately 20% of global traffic, hosts APAC merchant data
  • Database replication: Synchronous replication within region (3 replicas), asynchronous cross-region with bounded staleness (less than 500ms lag)
  • Failover: Automated with DNS-based traffic shifting, targeting less than 30 second RTO

6. Payment Flow - Tokenization, Authorization, and Capture

The payment flow is the heart of the system. Every design decision in this flow directly impacts security, reliability, and user experience. We will trace a payment from the moment a customer enters their card details through to the funds being captured and settled.

Complete Payment Lifecycle

sequenceDiagram participant C as Client participant S as Payment Service participant V as Token Vault participant F as Fraud Engine participant P as Processor Router participant G as Gateway participant N as Card Network participant I as Issuing Bank participant L as Ledger Service participant W as Webhook Service C->>S: Create PaymentIntent S->>L: Create pending ledger entry S-->>C: Return PaymentIntent + client_secret C->>S: Confirm PaymentIntent S->>V: Verify payment method token V-->>S: Token valid S->>F: Risk assessment F-->>S: Risk score Low S->>P: Authorize payment P->>G: Authorization request G->>N: Forward to card network N->>I: Card verification and hold I-->>N: Approved N-->>G: Auth response G-->>P: Authorized P-->>S: Authorization successful S->>L: Update ledger S->>W: Emit payment.authorized event S-->>C: Payment confirmed Note over S: Capture window opens (up to 7 days) S->>P: Capture payment P->>G: Capture request G->>N: Capture N->>I: Settle funds I-->>N: Settled N-->>G: Captured G-->>P: Captured P-->>S: Capture successful S->>L: Update ledger (fees computed) S->>W: Emit payment.captured event

Step 1: Tokenization

Tokenization converts sensitive card data into a non-reversible token that can be safely stored and transmitted. This is the first and most critical step in PCI compliance - it ensures that raw card data never touches your servers. The client-side SDK intercepts card data and sends it directly to the token vault, which returns a token that references the encrypted card data without ever exposing the full card number.

C#
public class TokenizationService
{
    private readonly ITokenVault _vault;
    private readonly IEncryptionService _encryption;

    public async Task<TokenizationResult> TokenizeCardAsync(
        CardTokenizationRequest request)
    {
        if (!LuhnAlgorithm.IsValid(request.CardNumber))
            return TokenizationResult.Failed("Invalid card number");

        var cardInfo = CardDetector.Detect(request.CardNumber);
        var encryptedTrackData = await _encryption.EncryptAsync(
            request.CardNumber,
            keyVersion: await _vault.GetCurrentKeyVersionAsync());

        var fingerprint = FingerprintGenerator.Generate(
            request.CardNumber, request.ExpMonth, request.ExpYear);

        var token = new TokenizedCard
        {
            Id = GeneratePrefixedId("pm_"),
            Fingerprint = fingerprint,
            EncryptedData = encryptedTrackData,
            Last4 = request.CardNumber[^4..],
            Brand = cardInfo.Brand,
            ExpMonth = request.ExpMonth,
            ExpYear = request.ExpYear,
            Funding = cardInfo.Funding,
            Country = cardInfo.Country,
            CreatedAt = DateTime.UtcNow
        };

        await _vault.StoreTokenAsync(token);
        var networkToken = await TryGetNetworkTokenAsync(cardInfo, token);
        return TokenizationResult.Success(token.Id, networkToken);
    }
}

public class LuhnAlgorithm
{
    public static bool IsValid(string cardNumber)
    {
        int sum = 0;
        bool alternate = false;
        for (int i = cardNumber.Length - 1; i >= 0; i--)
        {
            int digit = int.Parse(cardNumber[i].ToString());
            if (alternate) { digit *= 2; if (digit > 9) digit -= 9; }
            sum += digit;
            alternate = !alternate;
        }
        return sum % 10 == 0;
    }
}

Step 2: Authorization

Authorization verifies that the card is valid and has sufficient funds, placing a hold on the requested amount. This is a synchronous operation completing in 1-3 seconds.

C#
public class AuthorizationService
{
    private readonly IProcessorRouter _processorRouter;
    private readonly IFraudEngine _fraudEngine;
    private readonly IFXService _fxService;

    public async Task<AuthorizationOutcome> AuthorizeAsync(
        PaymentIntent paymentIntent)
    {
        var riskAssessment = await _fraudEngine.AssessRiskAsync(
            new RiskContext
        {
            PaymentIntentId = paymentIntent.Id,
            Amount = paymentIntent.Amount,
            Currency = paymentIntent.Currency,
            MerchantId = paymentIntent.MerchantId,
            CustomerId = paymentIntent.CustomerId,
            PaymentMethodFingerprint = paymentIntent.PaymentMethodId,
            DeviceFingerprint = paymentIntent.Metadata
                .GetValueOrDefault("device_fp"),
            IpAddress = paymentIntent.Metadata.GetValueOrDefault("ip_address")
        });

        if (riskAssessment.Level == RiskLevel.VeryHigh)
            return AuthorizationOutcome.Declined("Blocked by fraud screening");

        var (chargeCurrency, chargeAmount, fxRate) =
            await ResolveCurrencyAsync(paymentIntent);

        var processor = await _processorRouter.SelectProcessorAsync(
            paymentIntent.MerchantId, paymentIntent.PaymentMethodId,
            chargeCurrency, riskAssessment);

        var authRequest = new AuthorizationRequest
        {
            Amount = chargeAmount,
            Currency = chargeCurrency,
            PaymentToken = paymentIntent.PaymentMethodId,
            IdempotencyKey = $"auth_{paymentIntent.Id}",
            MerchantId = processor.MerchantIdentifier,
            Metadata = paymentIntent.Metadata,
            ThreeDSecure = riskAssessment.RequiresThreeDS
                ? "required" : "optional",
            RiskData = riskAssessment.ToProcessorFormat()
        };

        var authResponse = await processor.AuthorizeAsync(authRequest);

        if (authResponse.IsApproved)
            return AuthorizationOutcome.Approved(
                authResponse.AuthorizationCode,
                authResponse.NetworkTransactionId, fxRate);

        if (authResponse.IsRequiresThreeDS)
            return AuthorizationOutcome.Requires3DS(
                authResponse.RedirectUrl);

        return AuthorizationOutcome.Declined(authResponse.DeclineReason);
    }
}

Step 3: Capture

Capture converts an authorization into an actual fund transfer. The capture window varies by card network - Visa typically allows 7 days, Mastercard allows 30 days for certain merchant categories.

C#
public class CaptureService
{
    private readonly IPaymentRepository _payments;
    private readonly IProcessorRouter _processorRouter;
    private readonly ILedgerService _ledger;
    private readonly IEventPublisher _events;

    public async Task<CaptureResult> CaptureAsync(
        string paymentIntentId, long? amountToCapture = null)
    {
        var payment = await _payments.GetForUpdateAsync(paymentIntentId);
        if (payment == null)
            throw new PaymentNotFoundException(paymentIntentId);

        if (payment.Status != PaymentStatus.Authorized)
            throw new InvalidStateTransitionException(
                payment.Status, PaymentStatus.Captured);

        var authorizedAmount = payment.Charges.Last().Amount;
        var remainingAmount = authorizedAmount - payment.AmountCaptured;
        var captureAmount = amountToCapture ?? remainingAmount;

        if (captureAmount > remainingAmount)
            throw new CaptureAmountExceededException(
                captureAmount, remainingAmount);

        var processor = _processorRouter.GetProcessor(
            payment.Charges.Last().ProcessorTransactionId);
        var captureResult = await processor.CaptureAsync(
            transactionId: payment.Charges.Last().ProcessorTransactionId,
            amount: captureAmount);

        if (captureResult.IsSuccessful)
        {
            payment.AmountCaptured += captureAmount;
            payment.Status = payment.AmountCaptured == authorizedAmount
                ? PaymentStatus.Captured
                : PaymentStatus.PartiallyCaptured;
            payment.CapturedAt ??= DateTime.UtcNow;
            await _payments.UpdateAsync(payment);

            await _ledger.PostCaptureAsync(new CaptureLedgerEntry
            {
                PaymentIntentId = paymentIntentId,
                Amount = captureAmount,
                Currency = payment.Currency,
                MerchantId = payment.MerchantId,
                ProcessorFee = captureResult.Fee,
                NetworkFee = captureResult.NetworkFee
            });

            await _events.PublishAsync(new PaymentCapturedEvent
            {
                PaymentIntentId = paymentIntentId,
                AmountCaptured = captureAmount
            });

            return CaptureResult.Success(captureAmount);
        }
        return CaptureResult.Failed(captureResult.ErrorMessage);
    }
}
Critical Failure Mode - Partial Capture Race Condition: When multiple processes attempt to capture the same payment intent concurrently, you must use database-level locking (SELECT FOR UPDATE) or optimistic concurrency control (version numbers) to prevent double capture. Without this safeguard, you will over-charge the customer.

7. Payment Gateway Integration

A production payment system never relies on a single payment processor. The gateway integration layer provides a unified abstraction over multiple underlying processors (Stripe, Adyen, Worldpay, etc.), enabling intelligent routing, failover, and cost optimization. This adapter pattern is essential for reliability - when one processor has an outage, traffic automatically shifts to another.

Processor Router Architecture

graph LR Request["Payment Request"] --> Router["Smart Router"] Router --> Failover["Failover Manager"] Failover --> CB["Circuit Breaker"] CB --> Stripe["Stripe Adapter"] CB --> Adyen["Adyen Adapter"] CB --> Worldpay["Worldpay Adapter"] Stripe --> Visa["Visa"] Stripe --> MC["Mastercard"] Adyen --> Visa2["Visa"] Adyen --> MC2["Mastercard"]
C#
public interface IPaymentProcessor
{
    string Name { get; }
    Task<AuthorizationResponse> AuthorizeAsync(
        AuthorizationRequest request);
    Task<CaptureResponse> CaptureAsync(
        string transactionId, long amount);
    Task<RefundResponse> RefundAsync(
        string transactionId, long amount, string reason);
    Task<ProcessorStatus> GetStatusAsync(string transactionId);
    bool SupportsCurrency(string currency);
    ProcessorCapabilities Capabilities { get; }
}

public class ProcessorRouter
{
    private readonly IEnumerable<IPaymentProcessor> _processors;
    private readonly ICircuitBreakerFactory _circuitBreakers;
    private readonly IRouteSelector _routeSelector;

    public async Task<IPaymentProcessor> SelectProcessorAsync(
        string merchantId, string paymentMethodId,
        string currency, RiskAssessment risk)
    {
        var context = new RoutingContext
        {
            MerchantId = merchantId,
            Currency = currency,
            CardBrand = await GetCardBrandAsync(paymentMethodId),
            RiskLevel = risk.Level,
            Amount = risk.Amount
        };

        var candidates = await _routeSelector
            .GetRankedProcessorsAsync(context);

        var available = candidates
            .Where(p => _circuitBreakers.Get(p.Name).State
                != CircuitState.Open)
            .Where(p => p.SupportsCurrency(currency))
            .Where(p => p.Capabilities.MeetsRequirements(context))
            .ToList();

        if (available.Count == 0)
            throw new NoAvailableProcessorException(currency);

        return new FaultTolerantProcessorSelection(
            available, _circuitBreakers);
    }
}

public class FaultTolerantProcessorSelection : IPaymentProcessor
{
    private readonly List<IPaymentProcessor> _candidates;
    private readonly ICircuitBreakerFactory _circuitBreakers;

    public async Task<AuthorizationResponse> AuthorizeAsync(
        AuthorizationRequest request)
    {
        for (int i = 0; i < _candidates.Count; i++)
        {
            var processor = _candidates[i];
            var cb = _circuitBreakers.Get(processor.Name);
            try
            {
                using var _ = cb.AcquireSlot();
                var response = await processor
                    .AuthorizeAsync(request);
                cb.RecordSuccess();
                return response;
            }
            catch (Exception ex) when (IsTransient(ex))
            {
                cb.RecordFailure();
                await _routeSelector
                    .RecordFailureAsync(processor.Name, request, ex);
            }
        }
        throw new AllProcessorsFailedException(
            _candidates.Select(p => p.Name).ToList());
    }
}

public class CircuitBreaker
{
    private int _failureCount = 0;
    private DateTime _lastFailureTime = DateTime.MinValue;
    private CircuitState _state = CircuitState.Closed;
    private readonly int _failureThreshold = 5;
    private readonly TimeSpan _openDuration =
        TimeSpan.FromMinutes(1);

    public CircuitState State
    {
        get
        {
            if (_state == CircuitState.Open &&
                DateTime.UtcNow - _lastFailureTime > _openDuration)
                _state = CircuitState.HalfOpen;
            return _state;
        }
    }

    public void RecordFailure()
    {
        _failureCount++;
        _lastFailureTime = DateTime.UtcNow;
        if (_failureCount >= _failureThreshold)
            _state = CircuitState.Open;
    }

    public void RecordSuccess()
    {
        _failureCount = 0;
        _state = CircuitState.Closed;
    }
}
Routing Intelligence: The processor router should learn from historical data. By tracking authorization success rates, latency, and fees per card brand, currency, and amount range, the router can make increasingly optimal decisions. Over time, this can improve overall authorization rates by 2-5 percent, which translates to millions of dollars in additional revenue for merchants.

8. Idempotency and Exactly-Once Processing

In a distributed payment system, at-least-once delivery is the norm - networks timeout, servers crash mid-request, and clients retry. Without idempotency, these retries create duplicate charges, which are catastrophic for customer trust and financial accuracy. Idempotency ensures that performing the same operation multiple times produces the same result as performing it once.

Idempotency Key Architecture

sequenceDiagram participant C as Client participant G as API Gateway participant R as Redis participant D as Database participant P as Payment Service C->>G: POST /v1/payments (Key: abc123) G->>R: GET idem:abc123 alt Key exists R-->>G: Return cached response G-->>C: Return cached result else Key not found R-->>G: Not found G->>D: INSERT idempotency key alt Insert succeeded (first request) G->>P: Process payment P-->>G: Result succeeded G->>D: Store result G->>R: Cache result G-->>C: Return result else Insert failed (duplicate) G->>D: SELECT cached response G-->>C: Return cached result end end
C#
public class IdempotencyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IDistributedCache _cache;
    private readonly IDbContextFactory<PaymentDbContext> _dbFactory;

    public async Task InvokeAsync(HttpContext context)
    {
        var idempotencyKey =
            context.Request.Headers["Idempotency-Key"]
                .FirstOrDefault();

        if (string.IsNullOrEmpty(idempotencyKey))
        {
            if (context.Request.Method != "GET")
            {
                context.Response.StatusCode = 400;
                await context.Response.WriteAsync(
                    "Idempotency-Key header required");
                return;
            }
            await _next(context);
            return;
        }

        if (idempotencyKey.Length > 255 ||
            !IsValidKeyFormat(idempotencyKey))
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsync(
                "Invalid Idempotency-Key format");
            return;
        }

        var cachedResult = await _cache.GetStringAsync(
            $"idem:{idempotencyKey}");
        if (cachedResult != null)
        {
            await WriteCachedResponse(context, cachedResult);
            return;
        }

        await using var db =
            await _dbFactory.CreateDbContextAsync();
        await using var transaction =
            await db.Database.BeginTransactionAsync();

        try
        {
            var existing = await db.IdempotencyKeys
                .FirstOrDefaultAsync(
                    k => k.Key == idempotencyKey);

            if (existing == null)
            {
                var newKey = new IdempotencyKeyRecord
                {
                    Key = idempotencyKey,
                    Status = IdempotencyStatus.InProgress,
                    CreatedAt = DateTime.UtcNow,
                    ExpiresAt = DateTime.UtcNow.AddDays(24),
                    RequestHash = HashRequest(context.Request)
                };
                db.IdempotencyKeys.Add(newKey);
                await db.SaveChangesAsync();
                await transaction.CommitAsync();

                var responseBody =
                    await CaptureResponseAsync(context, _next);

                await StoreResultAsync(idempotencyKey,
                    responseBody, IdempotencyStatus.Completed);
                await _cache.SetStringAsync(
                    $"idem:{idempotencyKey}", responseBody,
                    new DistributedCacheEntryOptions
                    {
                        AbsoluteExpirationRelativeToNow =
                            TimeSpan.FromHours(24)
                    });
            }
            else if (existing.Status ==
                IdempotencyStatus.Completed)
            {
                await transaction.RollbackAsync();
                await WriteCachedResponse(
                    context, existing.ResponseBody);
            }
            else
            {
                await transaction.RollbackAsync();
                context.Response.StatusCode = 409;
                await context.Response.WriteAsJsonAsync(
                    new { error = new
                    {
                        message = "Request in progress",
                        type = "conflict"
                    }});
            }
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

public class IdempotencyKeyRecord
{
    public string Key { get; set; }
    public IdempotencyStatus Status { get; set; }
    public string RequestHash { get; set; }
    public string ResponseBody { get; set; }
    public string PaymentIntentId { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? CompletedAt { get; set; }
    public DateTime ExpiresAt { get; set; }
}

public enum IdempotencyStatus
{
    InProgress,
    Completed,
    Failed
}
Idempotency Key Lifecycle: Idempotency keys must be stored for at least 24 hours (Stripe uses 48 hours). During this window, repeated requests with the same key return the same result. Keys should be scoped to the API key - the same key from different API keys should be treated as different operations.

9. Ledger and Double-Entry Bookkeeping

The ledger is the financial backbone of the payment system. It must be an immutable, append-only record of every financial event, following the principles of double-entry bookkeeping. Every financial transaction must have equal debits and credits - if money moves from one account, it must arrive somewhere else. The ledger must be independently auditable and must survive any single infrastructure failure.

Double-Entry Ledger Design

C#
public class LedgerEntry
{
    public long Id { get; set; }
    public string EntryId { get; set; }
    public string TransactionId { get; set; }
    public LedgerEntryType Type { get; set; }
    public AccountId DebitAccount { get; set; }
    public AccountId CreditAccount { get; set; }
    public long Amount { get; set; }
    public string Currency { get; set; }
    public string PaymentIntentId { get; set; }
    public string MerchantId { get; set; }
    public DateTime PostedAt { get; set; }
    public DateTime? ReconciledAt { get; set; }
    public string PreviousEntryHash { get; set; }
    public string EntryHash { get; set; }
}

public class AccountId
{
    public string AccountNumber { get; set; }
    public AccountType Type { get; set; }
    public string Currency { get; set; }
}

public enum AccountType
{
    Assets, Liabilities, Revenue, Expenses, Equity
}

public enum LedgerEntryType
{
    PaymentAuthorization, PaymentCapture, Refund,
    Chargeback, ChargebackReversal, Payout,
    PayoutReversal, PlatformFee, ProcessingFee,
    NetworkFee, Reserve, ReserveRelease,
    CurrencyConversion
}

Ledger Posting for Payment Capture

C#
public class LedgerService
{
    private readonly ILedgerStore _store;
    private readonly IEventPublisher _events;

    public async Task<LedgerResult> PostCaptureAsync(
        CaptureLedgerEntry entry)
    {
        var txnId = $"txn_{entry.PaymentIntentId}" +
            $"_{DateTime.UtcNow.Ticks}";

        var entries = new List<LedgerEntry>
        {
            new LedgerEntry
            {
                EntryId = GenerateULID(),
                TransactionId = txnId,
                Type = LedgerEntryType.PaymentCapture,
                DebitAccount = new AccountId
                {
                    AccountNumber =
                        $"processor:stripe:receivable:" +
                        $"{entry.Currency}",
                    Type = AccountType.Assets,
                    Currency = entry.Currency
                },
                CreditAccount = new AccountId
                {
                    AccountNumber =
                        $"merchant:{entry.MerchantId}:" +
                        $"available:{entry.Currency}",
                    Type = AccountType.Liabilities,
                    Currency = entry.Currency
                },
                Amount = entry.Amount - entry.ProcessorFee
                    - entry.NetworkFee,
                Currency = entry.Currency,
                PaymentIntentId = entry.PaymentIntentId,
                MerchantId = entry.MerchantId,
                PostedAt = DateTime.UtcNow
            },
            new LedgerEntry
            {
                EntryId = GenerateULID(),
                TransactionId = txnId,
                Type = LedgerEntryType.ProcessingFee,
                DebitAccount = new AccountId
                {
                    AccountNumber =
                        $"merchant:{entry.MerchantId}:" +
                        $"available:{entry.Currency}",
                    Type = AccountType.Liabilities,
                    Currency = entry.Currency
                },
                CreditAccount = new AccountId
                {
                    AccountNumber =
                        $"revenue:processing_fees:" +
                        $"{entry.Currency}",
                    Type = AccountType.Revenue,
                    Currency = entry.Currency
                },
                Amount = entry.ProcessorFee,
                Currency = entry.Currency,
                PaymentIntentId = entry.PaymentIntentId,
                MerchantId = entry.MerchantId,
                PostedAt = DateTime.UtcNow
            },
            new LedgerEntry
            {
                EntryId = GenerateULID(),
                TransactionId = txnId,
                Type = LedgerEntryType.NetworkFee,
                DebitAccount = new AccountId
                {
                    AccountNumber =
                        $"merchant:{entry.MerchantId}:" +
                        $"available:{entry.Currency}",
                    Type = AccountType.Liabilities,
                    Currency = entry.Currency
                },
                CreditAccount = new AccountId
                {
                    AccountNumber =
                        $"revenue:network_fees:" +
                        $"{entry.Currency}",
                    Type = AccountType.Revenue,
                    Currency = entry.Currency
                },
                Amount = entry.NetworkFee,
                Currency = entry.Currency,
                PaymentIntentId = entry.PaymentIntentId,
                MerchantId = entry.MerchantId,
                PostedAt = DateTime.UtcNow
            }
        };

        VerifyDoubleEntryBalance(entries);

        var previousHash =
            await _store.GetLatestHashAsync(entry.MerchantId);
        foreach (var e in entries)
        {
            e.PreviousEntryHash = previousHash;
            e.EntryHash = ComputeEntryHash(e);
            previousHash = e.EntryHash;
        }

        await _store.AppendEntriesAsync(entries);

        await _events.PublishAsync(new LedgerPostedEvent
        {
            TransactionId = txnId,
            Entries = entries.Select(e => e.EntryId).ToList()
        });

        return LedgerResult.Success(txnId, entries.Count);
    }

    private void VerifyDoubleEntryBalance(
        List<LedgerEntry> entries)
    {
        var debits = entries
            .GroupBy(e => e.Currency)
            .ToDictionary(g => g.Key,
                g => g.Sum(e => e.Amount));
        var credits = entries
            .GroupBy(e => e.Currency)
            .ToDictionary(g => g.Key,
                g => g.Sum(e => e.Amount));

        foreach (var currency in debits.Keys)
        {
            if (debits[currency] != credits[currency])
                throw new LedgerBalanceException(
                    $"Violation for {currency}");
        }
    }
}

Ledger Account Hierarchy

Account Type Purpose Constraint
merchant:id:available Liability Funds available for payout Must be >= 0
merchant:id:pending Liability Funds authorized but not settled Temporary hold
merchant:id:reserve Liability Rolling reserve for chargebacks 5-10% of 90-day volume
processor:name:receivable Asset Funds owed by processor Reconciled daily
revenue:processing_fees Revenue Platform processing fees Recognized on capture
revenue:network_fees Revenue Network fees (pass-through) Charged to merchant
Hash Chain Integrity: Each ledger entry includes a SHA-256 hash of its contents plus the hash of the previous entry. This creates a tamper-evident chain - if anyone modifies a historical entry, all subsequent hashes break. Auditors can verify the entire ledger by re-computing hashes from the genesis entry forward.

10. Reconciliation System

Reconciliation is the process of comparing internal records against external sources to ensure they match. In a payment system, this means matching our ledger entries against processor settlement reports, bank statements, and card network settlement files. Discrepancies must be detected quickly - ideally in real-time - and investigated before they compound into larger financial issues.

Reconciliation Pipeline Architecture

graph TB subgraph "External Data" ProcessorReport["Processor Reports"] BankStatement["Bank Statements"] NetworkSettle["Network Settlement"] end subgraph "Ingestion" FileIngest["File Ingestion"] Parser["Format Parser"] Normalizer["Normalizer"] end subgraph "Matching" ExactMatch["Exact Match"] FuzzyMatch["Fuzzy Match"] ManualReview["Manual Review"] end subgraph "Resolution" AutoResolve["Auto-Resolution"] AlertSystem["Alert System"] Investigation["Investigation"] end subgraph "Reporting" ReconReport["Recon Report"] Dashboard["Dashboard"] end ProcessorReport --> FileIngest BankStatement --> FileIngest NetworkSettle --> FileIngest FileIngest --> Parser Parser --> Normalizer Normalizer --> ExactMatch Normalizer --> FuzzyMatch ExactMatch --> ReconReport FuzzyMatch --> AutoResolve FuzzyMatch --> ManualReview AutoResolve --> ReconReport ManualReview --> Investigation ReconReport --> Dashboard
C#
public class ReconciliationService
{
    private readonly ILedgerService _ledger;
    private readonly IProcessorReportFetcher _processorReports;
    private readonly IReconciliationStore _store;
    private readonly IAlertService _alerts;

    public async Task<ReconciliationResult> ReconcileAsync(
        string processorName, DateOnly date,
        CancellationToken ct = default)
    {
        var result = new ReconciliationResult
        {
            Processor = processorName,
            Date = date,
            StartedAt = DateTime.UtcNow
        };

        var processorRecords = await _processorReports
            .FetchSettlementAsync(processorName, date);

        var internalRecords = await _ledger
            .GetSettlementRecordsAsync(
                processorName,
                date.StartOfDay(),
                date.EndOfDay());

        var internalByTxnId = internalRecords
            .ToDictionary(
                r => r.ProcessorTransactionId, r => r);

        var matchedIds = new HashSet<string>();

        foreach (var procRec in processorRecords)
        {
            if (internalByTxnId.TryGetValue(
                procRec.TransactionId, out var intRec))
            {
                var match = CompareRecords(procRec, intRec);
                if (match.IsExactMatch)
                {
                    matchedIds.Add(procRec.TransactionId);
                    result.ExactMatches++;
                    continue;
                }

                result.Discrepancies.Add(new Discrepancy
                {
                    Type = match.AmountMismatch
                        ? DiscrepancyType.AmountMismatch
                        : DiscrepancyType.StatusMismatch,
                    ProcessorRecord = procRec,
                    InternalRecord = intRec,
                    Difference =
                        procRec.Amount - intRec.Amount,
                    Severity = CalculateSeverity(match)
                });
            }
            else
            {
                result.Discrepancies.Add(new Discrepancy
                {
                    Type =
                        DiscrepancyType.MissingInternal,
                    ProcessorRecord = procRec,
                    Severity = DiscrepancySeverity.High
                });
            }
        }

        foreach (var kvp in internalByTxnId)
        {
            if (!matchedIds.Contains(kvp.Key))
            {
                result.Discrepancies.Add(new Discrepancy
                {
                    Type =
                        DiscrepancyType.MissingProcessor,
                    InternalRecord = kvp.Value,
                    Severity = DiscrepancySeverity.Medium
                });
            }
        }

        var autoResolved =
            await AutoResolveDiscrepanciesAsync(
                result.Discrepancies);
        result.AutoResolved = autoResolved;

        var critical = result.Discrepancies
            .Where(d => d.Severity ==
                DiscrepancySeverity.Critical)
            .ToList();

        if (critical.Any())
        {
            await _alerts.SendCriticalAlertAsync(
                new ReconciliationAlert
            {
                Processor = processorName,
                Date = date,
                CriticalCount = critical.Count,
                TotalDifference = critical
                    .Sum(d => Math.Abs(d.Difference))
            });
        }

        result.CompletedAt = DateTime.UtcNow;
        await _store.SaveResultAsync(result);
        return result;
    }
}
Three-Way Reconciliation: For maximum financial accuracy, perform three-way reconciliation: compare your internal ledger against (1) the processor settlement report and (2) the bank statement of funds received. A discrepancy between your ledger and the processor might be a system bug; a discrepancy between the processor and the bank might be a settlement delay; but a discrepancy in all three indicates a more serious issue requiring immediate investigation.

11. Fraud Detection and Risk Scoring

Fraud detection is a continuous arms race. Payment systems must balance two competing objectives: approving legitimate transactions (minimizing false declines) and blocking fraudulent ones (minimizing false positives). A false decline costs you the sale and potentially the customer; a false positive lets fraud through. The best systems achieve both goals by combining rule-based checks, machine learning models, and behavioral analysis.

Fraud Detection Pipeline

graph TB subgraph "Data Collection" DeviceFP["Device Fingerprint"] IPIntel["IP Intelligence"] Behavioral["Behavioral Analysis"] Velocity["Velocity Checks"] end subgraph "Risk Scoring" RuleEngine["Rule Engine"] MLModel["ML Model"] GraphNetwork["Graph Analysis"] Ensemble["Ensemble Aggregator"] end subgraph "Decision" AutoApprove["Auto-Approve"] Challenge["3DS Challenge"] ManualReview["Manual Review"] AutoDecline["Auto-Decline"] end subgraph "Feedback" ChargebackData["Chargeback Data"] ModelRetrain["Model Retraining"] end DeviceFP --> RuleEngine IPIntel --> RuleEngine Behavioral --> MLModel Velocity --> MLModel RuleEngine --> Ensemble MLModel --> Ensemble GraphNetwork --> Ensemble Ensemble --> AutoApprove Ensemble --> Challenge Ensemble --> ManualReview Ensemble --> AutoDecline ChargebackData --> ModelRetrain ModelRetrain --> MLModel
C#
public class FraudDetectionEngine
{
    private readonly IRuleEngine _rules;
    private readonly IMLModelClient _mlModel;
    private readonly IGraphAnalyzer _graphAnalyzer;
    private readonly IVelocityTracker _velocityTracker;
    private readonly IIPIntelligence _ipIntel;

    public async Task<RiskAssessment> AssessRiskAsync(
        RiskContext context)
    {
        var signals = new List<RiskSignal>();

        // Rule-based checks (less than 5ms)
        var ruleResults =
            await _rules.EvaluateAsync(context);
        signals.AddRange(ruleResults);

        // Velocity checks (less than 10ms)
        var velocity =
            await _velocityTracker.CheckVelocityAsync(
                new VelocityQuery
        {
            CustomerId = context.CustomerId,
            CardFingerprint =
                context.PaymentMethodFingerprint,
            MerchantId = context.MerchantId,
            IpAddress = context.IpAddress,
            TimeWindows = new[] { "1h", "24h", "7d", "30d" }
        });
        signals.AddRange(velocity.ToRiskSignals());

        // IP intelligence (less than 20ms)
        var ipAnalysis =
            await _ipIntel.AnalyzeAsync(context.IpAddress);
        if (ipAnalysis.IsProxy || ipAnalysis.IsVPN)
            signals.Add(new RiskSignal(
                "network:proxy_detected", 0.15m));
        if (ipAnalysis.Country != context.ExpectedCountry)
            signals.Add(new RiskSignal(
                "geo:mismatch", 0.20m));

        // ML model scoring (less than 50ms)
        var mlFeatures = BuildFeatureVector(
            context, signals, velocity, ipAnalysis);
        var mlScore =
            await _mlModel.PredictAsync(mlFeatures);

        // Graph analysis (less than 100ms, async)
        var graphScore =
            await _graphAnalyzer.AnalyzeAsync(
                new GraphQuery
        {
            CardFingerprint =
                context.PaymentMethodFingerprint,
            DeviceFingerprint =
                context.DeviceFingerprint,
            MerchantId = context.MerchantId,
            IpSubnet = ipAnalysis.Subnet
        });

        // Ensemble scoring
        var finalScore = ComputeEnsembleScore(
            ruleResults, mlScore, graphScore,
            velocity.ToRiskSignals(),
            new[] { ipAnalysis });

        var riskLevel = finalScore switch
        {
            < 0.20m => RiskLevel.Low,
            < 0.50m => RiskLevel.Medium,
            < 0.80m => RiskLevel.High,
            _ => RiskLevel.VeryHigh
        };

        return new RiskAssessment
        {
            Score = finalScore,
            Level = riskLevel,
            TriggeredRules = ruleResults
                .Where(r => r.Triggered)
                .Select(r => r.RuleId).ToList(),
            RequiresThreeDS =
                riskLevel == RiskLevel.Medium ||
                riskLevel == RiskLevel.High,
            FraudCheck = new FraudCheckResult
            {
                MLScore = mlScore,
                GraphScore = graphScore,
                VelocityFlags = velocity.Flags,
                IPAnalysis = ipAnalysis
            }
        };
    }
}

Key Fraud Signals

Signal Description Risk Weight Source
Velocity Anomaly Unusual number of transactions in short window High Transaction logs
Geo Mismatch Card from US, IP from Nigeria, ship to Russia High Card + IP + shipping
First-Purchase High Value New customer making expensive first purchase Medium Customer history
Device Fingerprint Match Same device used with multiple cards High Device service
Proxy/VPN Usage Customer masking real IP address Low-Medium IP intelligence
Graph Anomaly Card linked to known fraud ring Critical Graph analysis
Card Testing Pattern Multiple small transactions then large one High Velocity + amount

12. PCI DSS Compliance and Security

PCI DSS (Payment Card Industry Data Security Standard) is a mandatory compliance framework for any entity that stores, processes, or transmits cardholder data. For a payment processing platform, PCI DSS Level 1 compliance is not optional - it is a prerequisite for doing business with card networks.

PCI DSS Requirements for Payment Systems

Requirement Description Implementation
Req 1: Firewalls Network security controls WAF + microsegmentation
Req 2: Secure Config Secure configurations Hardened images, CIS benchmarks
Req 3: Protect Data Protect stored account data AES-256, tokenization, no raw PAN
Req 4: Encrypt Transit Cryptography in transit TLS 1.3, certificate pinning
Req 5: Malware Protect from malware Runtime protection, scanning
Req 6: Secure Dev Secure development SDLC, SAST/DAST, dep scanning
Req 7: Access Control Restrict access by need-to-know RBAC, least privilege, JIT access
Req 8: Authentication Identify and authenticate users MFA, short-lived tokens
Req 9: Physical Access Physical access restrictions Cloud provider handles
Req 10: Logging Log and monitor all access Centralized logs, 7-year retention
Req 11: Testing Regular security testing Quarterly ASV scans, annual pen tests
Req 12: Policy Information security policies Written policies, training, IR plan

Token Vault Architecture

C#
public class SecureTokenVault
{
    private readonly IKeyManagementService _kms;
    private readonly IEncryptionEngine _encryption;
    private readonly IAuditLogger _auditLog;

    public async Task<TokenizedCard> StoreCardAsync(
        RawCardData cardData)
    {
        var keyVersion =
            await _kms.GetCurrentKeyVersionAsync();

        var encryptedPan = await _encryption.EncryptAsync(
            plaintext: cardData.CardNumber,
            keyVersion: keyVersion,
            associatedData: Encoding.UTF8.GetBytes(
                cardData.MerchantId));

        var fingerprint =
            await ComputeFingerprintAsync(
                cardData.CardNumber);

        var token = new TokenizedCard
        {
            Id = GenerateSecureId("pm_"),
            Fingerprint = fingerprint,
            EncryptedPan = encryptedPan,
            KeyVersion = keyVersion,
            Last4 = cardData.CardNumber[^4..],
            ExpMonth = cardData.ExpMonth,
            ExpYear = cardData.ExpYear,
            CreatedAt = DateTime.UtcNow
        };

        await _vaultRepository.InsertAsync(token);

        await _auditLog.LogAsync(new AuditEntry
        {
            Action = "card_tokenized",
            ResourceId = token.Id,
            MerchantId = cardData.MerchantId,
            Timestamp = DateTime.UtcNow,
            Details = $"Card tokenized: ****{token.Last4}"
        });

        return token;
    }

    public async Task RotateKeysAsync()
    {
        var newKeyVersion =
            await _kms.RotateKeyAsync();

        await _backgroundJobs.EnqueueAsync(
            new KeyRotationJob
        {
            NewKeyVersion = newKeyVersion,
            StartedAt = DateTime.UtcNow,
            BatchSize = 10000
        });

        await _auditLog.LogAsync(new AuditEntry
        {
            Action = "key_rotation_initiated",
            Details = $"New key: {newKeyVersion}",
            Timestamp = DateTime.UtcNow
        });
    }

    private async Task<int> ComputeFingerprintAsync(
        string pan)
    {
        var hmacKey =
            await _kms.GetFingerprintKeyAsync();
        var hmac = new HMACSHA256(hmacKey);
        var hash = hmac.ComputeHash(
            Encoding.UTF8.GetBytes(pan));
        return BitConverter.ToInt32(hash, 0);
    }
}
Critical PCI Rule: Never store CVV/CVC after authorization. Never log or display full card numbers. Never allow raw card data to pass through your application servers - always tokenize at the edge. Violation of these rules can result in fines of ,000-,000 per month and loss of ability to process card payments.

13. Subscription and Recurring Billing

Subscription billing adds significant complexity to a payment system. Unlike one-time payments, subscriptions require managing time-based state machines, handling plan changes with proration, dealing with failed payment retries (dunning), and supporting complex billing models (usage-based, tiered, per-seat). The subscription engine must be reliable enough to handle millions of recurring charges without missing a beat.

Subscription State Machine

stateDiagram-v2 [*] --> Active: Created Active --> PastDue: Payment Failed Active --> Canceled: Cancel Active --> Paused: Pause PastDue --> Active: Retry Succeeded PastDue --> Canceled: Max Retries PastDue --> Unpaid: Dunning Exhausted Paused --> Active: Resume Unpaid --> Canceled: Admin Action Canceled --> [*]
C#
public class Subscription
{
    public string Id { get; set; }
    public string CustomerId { get; set; }
    public string MerchantId { get; set; }
    public string PriceId { get; set; }
    public SubscriptionStatus Status { get; set; }
    public BillingCycle Cycle { get; set; }
    public DateTime CurrentPeriodStart { get; set; }
    public DateTime CurrentPeriodEnd { get; set; }
    public DateTime? TrialEnd { get; set; }
    public DateTime? CanceledAt { get; set; }
    public DateTime? CancelAt { get; set; }
    public string DefaultPaymentMethodId { get; set; }
    public int FailedPaymentAttempts { get; set; }
    public DateTime? NextRetryAt { get; set; }
    public SubscriptionItem[] Items { get; set; }
    public Dictionary<string, string> Metadata { get; set; }
}

public class SubscriptionBillingEngine
{
    private readonly ISubscriptionRepository _subscriptions;
    private readonly IPaymentService _payments;
    private readonly IInvoiceService _invoices;
    private readonly IProrationEngine _proration;
    private readonly IDunningManager _dunning;

    public async Task<BillingResult>
        ProcessRecurringBillingAsync()
    {
        var dueSubscriptions =
            await _subscriptions.GetDueForBillingAsync(
                before: DateTime.UtcNow,
                status: new[] { SubscriptionStatus.Active },
                limit: 10000);

        var results = new List<BillingResult>();
        var batches = dueSubscriptions.Chunk(100);
        var batchTasks = batches.Select(
            batch => ProcessBatchAsync(batch));
        var batchResults =
            await Task.WhenAll(batchTasks);

        return BillingResult.Aggregate(batchResults);
    }

    private async Task<BillingResult> ProcessBatchAsync(
        Subscription[] batch)
    {
        var result = new BillingResult();

        foreach (var subscription in batch)
        {
            try
            {
                var invoice =
                    await _invoices
                        .CreateForSubscriptionAsync(
                            subscription);

                var paymentResult =
                    await _payments.ChargeAsync(
                        new ChargeRequest
                {
                    Amount = invoice.AmountDue,
                    Currency = invoice.Currency,
                    CustomerId = subscription.CustomerId,
                    PaymentMethodId =
                        subscription.DefaultPaymentMethodId,
                    IdempotencyKey =
                        $"sub_{subscription.Id}_" +
                        $"{subscription.CurrentPeriodEnd:yyyyMMdd}",
                    Metadata = new Dictionary<string, string>
                    {
                        ["subscription_id"] =
                            subscription.Id,
                        ["invoice_id"] = invoice.Id
                    }
                });

                if (paymentResult.IsSuccessful)
                {
                    await AdvanceSubscriptionAsync(
                        subscription);
                    await _invoices.MarkPaidAsync(
                        invoice,
                        paymentResult.PaymentIntentId);
                    result.Successful++;
                }
                else
                {
                    await _dunning
                        .HandleFailedPaymentAsync(
                            subscription,
                            paymentResult, invoice);
                    result.Failed++;
                }
            }
            catch (Exception ex)
            {
                result.Errors.Add(
                    new BillingError(
                        subscription.Id, ex.Message));
            }
        }

        return result;
    }

    public async Task<PlanChangeResult> ChangePlanAsync(
        string subscriptionId,
        string newPriceId,
        ProrationBehavior proration)
    {
        var subscription =
            await _subscriptions.GetByIdAsync(subscriptionId);
        var newPrice =
            await _prices.GetByIdAsync(newPriceId);

        var prorationAmount = proration switch
        {
            ProrationBehavior.CreateProrations =>
                await _proration.CalculateCreditAsync(
                    subscription, DateTime.UtcNow),
            ProrationBehavior.None => 0,
            _ => throw new ArgumentOutOfRangeException(
                nameof(proration))
        };

        subscription.PriceId = newPriceId;
        subscription.Items =
            BuildItemsFromPrice(newPrice);
        await _subscriptions.UpdateAsync(subscription);

        if (prorationAmount < 0)
        {
            await _invoices.CreateCreditAsync(
                subscription,
                Math.Abs(prorationAmount));
        }
        else if (prorationAmount > 0)
        {
            await _payments.ChargeAsync(
                new ChargeRequest
            {
                Amount = prorationAmount,
                Currency = subscription.Currency,
                CustomerId = subscription.CustomerId,
                PaymentMethodId =
                    subscription.DefaultPaymentMethodId,
                IdempotencyKey =
                    $"proration_{subscriptionId}_" +
                    $"{DateTime.UtcNow.Ticks}"
            });
        }

        return PlanChangeResult.Success(
            subscription, prorationAmount);
    }
}

public enum SubscriptionStatus
{
    Active, PastDue, Unpaid, Canceled,
    Paused, Trialing
}

public enum ProrationBehavior
{
    CreateProrations,
    None
}

Dunning (Failed Payment Recovery)

Dunning is the automated process of retrying failed recurring payments and communicating with customers about payment issues. A well-designed dunning system maximizes revenue recovery while avoiding customer harassment. The retry schedule is carefully tuned based on the failure reason - insufficient funds may resolve quickly, while a lost card requires customer action.

Dunning Best Practices: Implement a progressive retry schedule: retry after 1 day, then 3 days, then 7 days, then 14 days. For card declines specifically, check if the failure is retryable - insufficient funds can be retried, while card expired requires the customer to update their payment method. Send email notifications at each retry, clearly explaining the issue and providing a one-click payment update link. Smart dunning systems can reduce involuntary churn by 20-30 percent.

14. Multi-Currency and Foreign Exchange

Supporting multi-currency payments is essential for a global payment platform. The system must handle 135+ currencies, each with different formatting rules (JPY has no decimal places, BHD has 3), different minimum transaction amounts, and different settlement requirements. Foreign exchange adds another layer of complexity - rates change constantly, and the system must handle both real-time conversion at payment time and batch conversion at settlement time.

C#
public class CurrencyService
{
    private readonly IFXRateProvider _rateProvider;
    private readonly ICurrencyConfig _config;

    private static readonly HashSet<string>
        ZeroDecimalCurrencies = new()
    {
        "JPY", "KRW", "VND", "CLP", "ISK",
        "UGX", "RWF", "VUV", "XAF", "XOF",
        "XPF", "BIF", "DJF", "GNF", "KMF",
        "MGA", "PYG", "TWD"
    };

    private static readonly HashSet<string>
        ThreeDecimalCurrencies = new()
    {
        "BHD", "KWD", "OMR", "JOD", "LYD",
        "TND", "IQD"
    };

    public long ToSmallestUnit(
        decimal amount, string currency)
    {
        var cur = currency.ToUpperInvariant();
        return cur switch
        {
            _ when ZeroDecimalCurrencies.Contains(cur)
                => (long)Math.Round(amount),
            _ when ThreeDecimalCurrencies.Contains(cur)
                => (long)Math.Round(amount * 1000),
            _ => (long)Math.Round(amount * 100)
        };
    }

    public decimal FromSmallestUnit(
        long amount, string currency)
    {
        var cur = currency.ToUpperInvariant();
        return cur switch
        {
            _ when ZeroDecimalCurrencies.Contains(cur)
                => amount,
            _ when ThreeDecimalCurrencies.Contains(cur)
                => amount / 1000m,
            _ => amount / 100m
        };
    }

    public async Task<ConversionResult> ConvertAsync(
        long amount, string fromCurrency,
        string toCurrency,
        ConversionType type = ConversionType.Payment)
    {
        if (fromCurrency == toCurrency)
            return ConversionResult.NoConversion(amount);

        var rate = type switch
        {
            ConversionType.Payment =>
                await _rateProvider
                    .GetPaymentRateAsync(
                        fromCurrency, toCurrency),
            ConversionType.Settlement =>
                await _rateProvider
                    .GetSettlementRateAsync(
                        fromCurrency, toCurrency),
            ConversionType.Display =>
                await _rateProvider
                    .GetDisplayRateAsync(
                        fromCurrency, toCurrency),
            _ => throw new ArgumentOutOfRangeException(
                nameof(type))
        };

        var convertedAmount =
            (long)Math.Round(amount * rate.Factor);

        var minAmount = _config
            .GetMinimumChargeAmount(toCurrency);
        if (convertedAmount < minAmount)
            throw new AmountBelowMinimumException(
                toCurrency, convertedAmount, minAmount);

        return ConversionResult.Converted(
            originalAmount: amount,
            convertedAmount: convertedAmount,
            rate: rate.Value,
            rateTimestamp: rate.Timestamp,
            markup: rate.Markup);
    }
}

public class FXRateProvider : IFXRateProvider
{
    private readonly IMarketDataClient _marketData;
    private readonly IDistributedCache _cache;

    public async Task<FXRate> GetPaymentRateAsync(
        string from, string to)
    {
        var cacheKey = $"fx:payment:{from}:{to}";
        var cached =
            await _cache.GetAsync<FXRate>(cacheKey);
        if (cached != null) return cached;

        var rates = await Task.WhenAll(
            _marketData.GetRateAsync(from, to, "reuters"),
            _marketData.GetRateAsync(from, to, "ecb"),
            _marketData.GetRateAsync(from, to, "bloomberg")
        );

        var medianRate = rates
            .OrderBy(r => r.Value).ToArray()[1];

        var markup = 0.01m;
        var finalRate = new FXRate
        {
            BaseCurrency = from,
            QuoteCurrency = to,
            Value = medianRate.Value * (1 + markup),
            Markup = markup,
            Timestamp = DateTime.UtcNow,
            Source = "composite"
        };

        await _cache.SetAsync(cacheKey, finalRate,
            TimeSpan.FromSeconds(60));
        return finalRate;
    }
}
FX Rate Risk: When you quote a rate to a customer and then settle later, the rate may have moved. This creates FX risk. There are two approaches: (1) lock the rate at authorization time and absorb the risk yourself, or (2) pass the rate through to the merchant and let them bear the risk. Most payment facilitators use approach 1 for customer experience and hedge the risk using forward contracts.

15. Refund and Dispute Management

Refunds and disputes are an inevitable part of any payment system. A well-designed refund system must handle partial refunds, full refunds, and the complex dispute (chargeback) process mandated by card networks. The dispute process involves strict timelines, specific evidence formats, and significant financial implications - losing a dispute means losing both the money and paying additional fees.

Refund and Dispute Lifecycle

graph TB subgraph "Refund Flow" MerchantRequest["Merchant Requests Refund"] RefundValidation["Validate Refund"] RefundProcessor["Submit to Processor"] RefundLedger["Update Ledger"] RefundComplete["Refund Complete"] end subgraph "Dispute Flow" DisputeReceived["Chargeback Received"] DisputeNotification["Notify Merchant"] EvidenceCollection["Evidence Collection"] EvidenceSubmission["Submit Evidence"] ReviewPeriod["Network Review"] Resolution{"Resolution"} Won["Dispute Won"] Lost["Dispute Lost"] end MerchantRequest --> RefundValidation RefundValidation --> RefundProcessor RefundProcessor --> RefundLedger RefundLedger --> RefundComplete DisputeReceived --> DisputeNotification DisputeNotification --> EvidenceCollection EvidenceCollection --> EvidenceSubmission EvidenceSubmission --> ReviewPeriod ReviewPeriod --> Resolution Resolution --> Won Resolution --> Lost
C#
public class DisputeManagementService
{
    private readonly IDisputeRepository _disputes;
    private readonly IPaymentRepository _payments;
    private readonly IProcessorRouter _processors;
    private readonly ILedgerService _ledger;
    private readonly IEventPublisher _events;
    private readonly IAlertService _alerts;

    public async Task<DisputeResponse>
        HandleIncomingChargebackAsync(
            ChargebackNotification notification)
    {
        var payment = await _payments
            .GetByProcessorTransactionIdAsync(
                notification.TransactionId);
        if (payment == null)
            return DisputeResponse
                .Failed("Unknown transaction");

        var dispute = new Dispute
        {
            Id = GeneratePrefixedId("dp_"),
            PaymentIntentId = payment.Id,
            ChargeId = payment.Charges.Last().Id,
            Amount = notification.DisputedAmount,
            Currency = notification.Currency,
            Reason = MapDisputeReason(notification.Reason),
            Status = DisputeStatus.NeedsResponse,
            CreatedAt = DateTime.UtcNow,
            EvidenceDueBy = notification.DueByDate,
            ProcessorDisputeId = notification.DisputeId
        };

        await _disputes.CreateAsync(dispute);

        await _ledger.PostDisputeAsync(
            new DisputeLedgerEntry
        {
            DisputeId = dispute.Id,
            PaymentIntentId = payment.Id,
            MerchantId = payment.MerchantId,
            Amount = dispute.Amount,
            Currency = dispute.Currency,
            Type = LedgerEntryType.Chargeback
        });

        await _events.PublishAsync(
            new DisputeCreatedEvent
        {
            DisputeId = dispute.Id,
            PaymentIntentId = payment.Id,
            Amount = dispute.Amount,
            Reason = dispute.Reason,
            EvidenceDueBy = dispute.EvidenceDueBy
        });

        await _alerts.ScheduleEvidenceReminderAsync(
            dispute);

        return DisputeResponse.Success(dispute);
    }

    public async Task<EvidenceSubmissionResult>
        SubmitEvidenceAsync(
            string disputeId, Evidence evidence)
    {
        var dispute =
            await _disputes.GetByIdAsync(disputeId);

        if (DateTime.UtcNow > dispute.EvidenceDueBy)
            throw new EvidenceDeadlinePassedException(
                dispute.EvidenceDueBy);

        var validation =
            ValidateEvidence(evidence, dispute.Reason);
        if (!validation.IsValid)
            return EvidenceSubmissionResult.Incomplete(
                validation.MissingFields);

        var formattedEvidence =
            FormatEvidenceForNetwork(
                evidence, dispute.Reason);

        var processor = _processors
            .GetProcessorForDispute(
                dispute.ProcessorDisputeId);
        var submission =
            await processor.SubmitEvidenceAsync(
                disputeId: dispute.ProcessorDisputeId,
                evidence: formattedEvidence);

        dispute.Status =
            DisputeStatus.UnderReview;
        await _disputes.UpdateAsync(dispute);

        return EvidenceSubmissionResult.Success(
            submission.ReferenceId);
    }

    public async Task<DisputeResolutionResult>
        ResolveDisputeAsync(
            string disputeId, DisputeOutcome outcome)
    {
        var dispute =
            await _disputes.GetByIdAsync(disputeId);
        var payment = await _payments
            .GetByIdAsync(dispute.PaymentIntentId);

        switch (outcome)
        {
            case DisputeOutcome.Won:
                dispute.Status = DisputeStatus.Won;
                dispute.ResolvedAt = DateTime.UtcNow;
                await _ledger
                    .PostDisputeReversalAsync(
                        new DisputeReversalEntry
                {
                    DisputeId = disputeId,
                    PaymentIntentId = payment.Id,
                    MerchantId = payment.MerchantId,
                    Amount = dispute.Amount,
                    Currency = dispute.Currency,
                    Type =
                        LedgerEntryType.ChargebackReversal
                });
                break;

            case DisputeOutcome.Lost:
                dispute.Status = DisputeStatus.Lost;
                dispute.ResolvedAt = DateTime.UtcNow;
                var fee = 1500;
                await _ledger.PostFeeAsync(new FeeEntry
                {
                    PaymentIntentId = payment.Id,
                    MerchantId = payment.MerchantId,
                    Amount = fee,
                    Currency = dispute.Currency,
                    FeeType = "chargeback_fee"
                });
                break;
        }

        await _disputes.UpdateAsync(dispute);
        return DisputeResolutionResult.Success(dispute);
    }
}
Evidence Quality Matters: Card networks decide disputes based on the evidence submitted. The most compelling evidence includes: (1) proof of delivery with tracking and signature, (2) AVS and CVV match results, (3) clear communication records with the customer, (4) terms of service acceptance proof, (5) IP address and device fingerprint matching the cardholder usual pattern. Invest in automatically collecting and organizing this evidence at the time of the transaction - by the time a dispute arrives, it is too late to gather it.

16. Webhooks and Event Delivery

Webhooks are the primary mechanism by which payment systems notify merchants about payment events in real-time. When a payment succeeds, a refund is processed, or a dispute is filed, the merchant system needs to know immediately. Webhook delivery must be reliable - a missed webhook means a merchant does not update their order status, leading to a poor customer experience. The system must guarantee at-least-once delivery with ordered events per resource.

Webhook Delivery Architecture

graph TB subgraph "Event Producers" PaymentEvents["Payment Events"] ChargeEvents["Charge Events"] RefundEvents["Refund Events"] DisputeEvents["Dispute Events"] end subgraph "Event Bus" Kafka["Kafka"] EventProcessor["Event Processor"] end subgraph "Webhook System" WebhookRouter["Webhook Router"] EndpointManager["Endpoint Manager"] SignatureService["HMAC Signing"] RateLimiter["Rate Limiting"] end subgraph "Delivery" DeliveryWorker["Delivery Worker"] RetryQueue["Retry Queue"] DLQ["Dead Letter Queue"] end PaymentEvents --> Kafka ChargeEvents --> Kafka RefundEvents --> Kafka DisputeEvents --> Kafka Kafka --> EventProcessor EventProcessor --> WebhookRouter WebhookRouter --> SignatureService WebhookRouter --> RateLimiter RateLimiter --> DeliveryWorker DeliveryWorker --> RetryQueue RetryQueue --> DLQ
C#
public class WebhookDeliveryService
{
    private readonly IWebhookEndpointStore _endpoints;
    private readonly ISignatureService _signatures;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IEventStore _eventStore;
    private readonly IDeliveryLogStore _deliveryLog;
    private readonly ILogger<WebhookDeliveryService> _logger;

    private const int MaxRetries = 6;
    private static readonly TimeSpan[] RetryDelays =
        new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(30),
        TimeSpan.FromMinutes(5),
        TimeSpan.FromMinutes(30),
        TimeSpan.FromHours(2)
    };

    public async Task DeliverEventAsync(
        WebhookEvent webhookEvent)
    {
        var endpoints = await _endpoints
            .GetSubscribedEndpointsAsync(
                webhookEvent.MerchantId,
                webhookEvent.EventType);

        var deliveryTasks = endpoints.Select(
            endpoint => DeliverToEndpointAsync(
                webhookEvent, endpoint, 0));

        await Task.WhenAll(deliveryTasks);
    }

    private async Task DeliverToEndpointAsync(
        WebhookEvent webhookEvent,
        WebhookEndpoint endpoint,
        int attempt)
    {
        var payload = JsonSerializer.Serialize(new
        {
            id = webhookEvent.Id,
            type = webhookEvent.EventType,
            created = webhookEvent.CreatedAt
                .ToUnixTimeSeconds(),
            data = new
            {
                @object = webhookEvent.Data
            }
        });

        var signature = _signatures.ComputeSignature(
            payload: payload,
            secret: endpoint.SigningSecret,
            timestamp: webhookEvent.CreatedAt);

        if (!await CheckRateLimitAsync(endpoint.Id))
        {
            await RequeueForLaterAsync(
                webhookEvent, endpoint,
                attempt, RetryDelays[attempt]);
            return;
        }

        try
        {
            var client =
                _httpClientFactory.CreateClient();
            client.Timeout = TimeSpan.FromSeconds(30);

            var request = new HttpRequestMessage(
                HttpMethod.Post, endpoint.Url)
            {
                Content = new StringContent(
                    payload, Encoding.UTF8,
                    "application/json")
            };
            request.Headers.Add(
                "Webhook-Id", webhookEvent.Id);
            request.Headers.Add(
                "Webhook-Timestamp",
                webhookEvent.CreatedAt
                    .ToUnixTimeSeconds().ToString());
            request.Headers.Add(
                "Webhook-Signature",
                $"v1,{signature}");

            var response = await client
                .SendAsync(request);

            await _deliveryLog.LogAsync(
                new DeliveryAttempt
            {
                WebhookEventId = webhookEvent.Id,
                EndpointId = endpoint.Id,
                Attempt = attempt,
                ResponseCode =
                    (int)response.StatusCode,
                Success =
                    response.IsSuccessStatusCode,
                Timestamp = DateTime.UtcNow
            });

            if (response.IsSuccessStatusCode)
                return;

            if (IsNonRetryable(response.StatusCode))
            {
                await DisableEndpointAsync(
                    endpoint.Id,
                    $"Non-retryable: " +
                    $"{response.StatusCode}");
                return;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Webhook delivery failed");
        }

        if (attempt < MaxRetries - 1)
        {
            await RequeueForLaterAsync(
                webhookEvent, endpoint,
                attempt + 1,
                RetryDelays[attempt + 1]);
        }
        else
        {
            await MoveToDeadLetterAsync(
                webhookEvent, endpoint);
        }
    }

    private bool IsNonRetryable(
        HttpStatusCode statusCode)
    {
        return statusCode is
            HttpStatusCode.BadRequest or
            HttpStatusCode.Unauthorized or
            HttpStatusCode.Forbidden or
            HttpStatusCode.Gone;
    }
}

public class WebhookEvent
{
    public string Id { get; set; }
    public string MerchantId { get; set; }
    public string EventType { get; set; }
    public DateTime CreatedAt { get; set; }
    public object Data { get; set; }
}

public class WebhookEndpoint
{
    public string Id { get; set; }
    public string MerchantId { get; set; }
    public string Url { get; set; }
    public string[] EnabledEvents { get; set; }
    public string SigningSecret { get; set; }
    public EndpointStatus Status { get; set; }
}
Webhook Verification: Merchants MUST verify webhook signatures before processing events. Without verification, an attacker could send fake webhook events to trigger fraudulent refunds or order fulfillments. Our signing uses HMAC-SHA256 with the merchant unique signing secret. The signature is computed over the event ID, timestamp, and payload - this prevents both forgery and replay attacks.

17. Payment Method Management

Modern payment platforms must support a wide variety of payment methods beyond just credit cards. Different regions have strong preferences - iDEAL in the Netherlands, Boleto in Brazil, UPI in India, Alipay in China. Each method has different authorization flows, settlement timelines, and refund capabilities. The payment method management system provides a unified abstraction while respecting each method unique characteristics.

Payment Method Abstraction

C#
public interface IPaymentMethodHandler
{
    PaymentMethodType SupportedType { get; }
    bool SupportsCurrency(string currency);
    bool SupportsRecurring { get; }
    bool SupportsRefund { get; }
    bool SupportsPartialCapture { get; }
    PaymentMethodCapabilities Capabilities { get; }
    Task<TokenizationResult> TokenizeAsync(
        TokenizationRequest request);
    Task<AuthorizationResult> AuthorizeAsync(
        AuthorizationContext context);
    Task<ConfirmationResult> ConfirmAsync(
        ConfirmationContext context);
    PaymentFlowType FlowType { get; }
}

public class CardPaymentMethodHandler :
    IPaymentMethodHandler
{
    public PaymentMethodType SupportedType =>
        PaymentMethodType.Card;
    public bool SupportsRecurring => true;
    public bool SupportsRefund => true;
    public bool SupportsPartialCapture => true;
    public PaymentFlowType FlowType =>
        PaymentFlowType.Synchronous;

    public PaymentMethodCapabilities Capabilities =>
        new()
    {
        MaxAmount = 999999999,
        SupportedCurrencies = new[]
        {
            "USD", "EUR", "GBP", "JPY"
        },
        Requires3DSecure = true,
        AverageAuthorizationTime =
            TimeSpan.FromSeconds(2)
    };

    public async Task<AuthorizationResult> AuthorizeAsync(
        AuthorizationContext context)
    {
        if (context.Requires3DS)
        {
            return AuthorizationResult
                .RequiresThreeDSecure(
                    threeDSecureUrl:
                        await Generate3DSUrlAsync(context),
                    pareq:
                        await GeneratePaReqAsync(context));
        }
        return await ProcessCardAuthorizationAsync(
            context);
    }
}

public class IDEALHandler : IPaymentMethodHandler
{
    public PaymentMethodType SupportedType =>
        PaymentMethodType.LocalPaymentMethod;
    public bool SupportsRecurring => false;
    public bool SupportsRefund => true;
    public bool SupportsPartialCapture => false;
    public PaymentFlowType FlowType =>
        PaymentFlowType.Redirect;

    public async Task<AuthorizationResult> AuthorizeAsync(
        AuthorizationContext context)
    {
        var session =
            await _idealClient.CreatePaymentAsync(
                new IdealPaymentRequest
        {
            Amount = context.Amount,
            Currency = "EUR",
            Description = context.Description,
            ReturnUrl = context.ReturnUrl,
            MerchantOrderId =
                context.PaymentIntentId
        });

        return AuthorizationResult.RequiresRedirect(
            redirectUrl: session.IssuerUrl,
            sessionId: session.Id);
    }
}

public class ACHBankTransferHandler :
    IPaymentMethodHandler
{
    public PaymentMethodType SupportedType =>
        PaymentMethodType.BankAccount;
    public bool SupportsRecurring => true;
    public bool SupportsRefund => true;
    public bool SupportsPartialCapture => false;
    public PaymentFlowType FlowType =>
        PaymentFlowType.Asynchronous;

    public async Task<AuthorizationResult> AuthorizeAsync(
        AuthorizationContext context)
    {
        var achSubmission =
            await _achClient.InitiatePaymentAsync(
                new ACHPaymentRequest
        {
            Amount = context.Amount,
            RoutingNumber =
                context.BankAccount.RoutingNumber,
            AccountNumber =
                context.BankAccount.AccountNumber,
            AccountType =
                context.BankAccount.AccountType,
            CompanyName = context.MerchantName,
            TraceNumber = GenerateTraceNumber()
        });

        return AuthorizationResult
            .PendingSettlement(
                estimatedSettlementDate:
                    DateTime.UtcNow.AddDays(2),
                traceNumber:
                    achSubmission.TraceNumber);
    }
}

Payment Method Comparison

Method Flow Type Settlement Recurring Refund
Credit/Debit Card Synchronous 1-3 business days Yes Yes (full/partial)
ACH Bank Transfer Asynchronous 2-5 business days Yes Yes (limited)
iDEAL Redirect 1 business day No Yes
SEPA Direct Debit Asynchronous 3-5 business days Yes Yes (8-week window)
Apple Pay / Google Pay Synchronous Same as underlying card Yes Yes
Boleto Redirect + Manual 1-3 business days No No
Unified API Surface: Despite the vastly different underlying flows (synchronous, redirect, asynchronous), the API should present a consistent interface to merchants. The PaymentIntent abstraction handles this - the merchant creates a PaymentIntent, and the system handles the complexity of the specific payment method flow, including redirects and pending states. The merchant polls or receives webhooks for state transitions.

18. Reliability and Failure Modes

Payment systems face a unique set of failure modes that do not exist in typical web applications. The consequences of failures are measured in dollars and customer trust, not just page views. Every failure mode must be anticipated, and the system must handle each gracefully without losing data or double-charging customers.

Critical Failure Modes

Failure Mode Impact Detection Mitigation
Processor timeout (partial auth) Unknown payment state Saga log monitoring Poll processor status, timeout-based reversal after 30s
Double capture race Over-charging customer Idempotency violation alerts SELECT FOR UPDATE, optimistic locking, unique constraints
Ledger imbalance Financial inaccuracy Continuous balance checks Atomic ledger postings, automatic hold on imbalance
Webhook delivery failure Merchant state out of sync Delivery rate monitoring Retry queue, dead letter, merchant dashboard polling fallback
Database split-brain Data inconsistency Consensus monitoring Majority quorum writes, fencing tokens
Fraud engine failure Let fraud through or block legit Health checks, scoring distribution Fallback to rule-only, manual review queue
FX rate staleness Incorrect conversion amounts Rate age monitoring Reject payment if rate too old, use last-known-good

Saga Pattern for Payment Operations

C#
public class PaymentSaga
{
    private readonly IPaymentRepository _payments;
    private readonly ILedgerService _ledger;
    private readonly IEventPublisher _events;
    private readonly ISagaLog _sagaLog;

    public async Task ExecutePaymentAsync(
        PaymentIntent payment)
    {
        var saga = new SagaLog("payment", payment.Id);

        // Step 1: Create pending ledger entry
        try
        {
            await _ledger.PostPendingAsync(
                payment);
            await saga.RecordStep(
                "ledger_pending", SagaStatus.Completed);
        }
        catch (Exception ex)
        {
            await saga.RecordStep(
                "ledger_pending",
                SagaStatus.Failed, ex.Message);
            throw;
        }

        // Step 2: Authorize
        try
        {
            var authResult =
                await AuthorizePaymentAsync(payment);
            await saga.RecordStep(
                "authorize", SagaStatus.Completed,
                authResult);

            if (!authResult.IsSuccess)
            {
                await _ledger
                    .ReversePendingAsync(payment);
                await saga.RecordStep(
                    "ledger_reverse",
                    SagaStatus.Completed);
                return;
            }
        }
        catch (Exception ex)
        {
            await saga.RecordStep(
                "authorize",
                SagaStatus.Failed, ex.Message);

            // Check if authorization actually went through
            var status = await PollAuthorizationStatus(
                payment.Id);
            if (status == AuthorizationStatus.Approved)
            {
                // Authorization succeeded but we did
                // not get the response - continue
                await saga.RecordStep(
                    "authorize_recovery",
                    SagaStatus.Completed);
            }
            else
            {
                await _ledger
                    .ReversePendingAsync(payment);
                throw;
            }
        }

        // Step 3: Capture
        try
        {
            var captureResult =
                await CapturePaymentAsync(payment);
            await saga.RecordStep(
                "capture", SagaStatus.Completed);

            await _ledger.PostCaptureAsync(payment);
            await saga.RecordStep(
                "ledger_capture", SagaStatus.Completed);
        }
        catch (Exception ex)
        {
            await saga.RecordStep(
                "capture",
                SagaStatus.Failed, ex.Message);

            // Authorization is still valid, retry capture
            await RetryCaptureLaterAsync(payment);
        }
    }
}

Graceful Degradation Strategy

  • Fraud engine down: Fall back to rule-only checks. If rules are also unavailable, route to manual review queue rather than blocking all payments.
  • Primary processor down: Automatic failover to secondary processor via circuit breaker pattern. No user-visible impact.
  • Database degraded: Serve reads from replicas, queue writes to durable buffer for replay when primary recovers.
  • Redis down: Bypass cache layer, fall through to database queries. Performance degrades but correctness is maintained.
  • Webhook delivery failing: Queue events for later delivery, maintain merchant polling API as fallback.
The Most Dangerous Failure: The most dangerous failure in a payment system is a silent data corruption - where the system appears to be operating normally but is recording incorrect amounts, applying wrong fees, or losing transaction records. This is why continuous reconciliation, balance checks, and audit trails are not optional features but core requirements. Design your system to fail loudly and obviously rather than silently and subtly.

19. Cost Estimation and Infrastructure

Understanding the infrastructure cost of a payment processing system is essential for business planning and architectural decision-making. At Stripe-scale, the costs are significant but the revenue per transaction makes the unit economics highly favorable. Let us break down the infrastructure costs for a platform processing 500 million transactions per month.

Compute Costs

Component Specification Count Monthly Cost
API Gateway (Load Balancers) High-memory instances 12 ,800
Payment Service (Core) 8 vCPU, 32GB RAM 48 ,200
Charge Service (Auth + Capture) 8 vCPU, 32GB RAM 36 ,400
Fraud Engine (ML inference) GPU instances (inference) 8 ,800
Webhook Delivery Workers 4 vCPU, 16GB RAM 24 ,800
Ledger Service 8 vCPU, 32GB RAM 12 ,800
Other Microservices Mixed规格 60 ,000
Compute Total ,800

Storage and Data Costs

Component Specification Monthly Cost
PostgreSQL Cluster (Primary + 6 Replicas) 128 vCPU, 512GB RAM, 20TB NVMe ,000
Redis Cluster (6 shards + replicas) 192GB RAM total ,400
Kafka Cluster (12 brokers) 50TB storage, 100K msgs/sec ,000
Object Storage (S3/GCS) 2PB stored, 10TB/month transfer ,000
Data Warehouse (ClickHouse) 50TB compressed ,000
PCI Vault (Encrypted storage) 5TB, HSM-managed keys ,000
Storage Total ,400

Operational and Security Costs

Category Monthly Cost
Network (multi-region, dedicated links) ,000
DDoS Protection (WAF + Shield) ,000
Monitoring and Observability (Datadog/Splunk) ,000
HSM (Hardware Security Modules) ,000
PCI DSS Compliance (ASV scans, pen tests, audit) ,000 (amortized)
Multi-region deployment overhead ,000
Operational Total ,000

Total Infrastructure Cost Summary

Category Monthly Annual
Compute ,800 ,600
Storage and Data ,400 ,132,800
Operations and Security ,000 ,000
Total Infrastructure ,200 ,858,400
Cost per Transaction .000476
Unit Economics: At .000476 per transaction in infrastructure cost, and typical payment processing fees of 2.9% + .30 per transaction (on an average transaction of ), the revenue per transaction is approximately .75. This means infrastructure costs represent roughly 0.027% of revenue - an extraordinarily favorable ratio that enables massive scale before infrastructure becomes a meaningful cost factor. The dominant costs in payment processing are interchange fees (paid to card-issuing banks), fraud losses, and customer support - not infrastructure.

20. Interview Q and A

The following questions and answers cover the most common system design interview topics for payment processing systems at the Staff and Principal Engineer level. These are designed to demonstrate deep understanding of both the technical and business aspects of payment infrastructure.

Q1: How would you handle a situation where the payment processor responds with an ambiguous result (timeout after the authorization may have gone through)?

Answer: This is one of the most critical failure modes in payment systems. The approach is: (1) Record the pending state locally before making the processor call, including the timestamp and idempotency key. (2) If the response times out, implement a polling mechanism that queries the processor for the status of the authorization using the idempotency key. (3) Most processors support a GET endpoint to retrieve transaction status by their transaction ID. (4) Set a timeout of 30 seconds for this polling - if the processor still cannot confirm the status, we hold the payment in a pending state and schedule background reconciliation. (5) We must never assume failure and automatically reverse - this could result in a double charge if the original authorization actually succeeded. The key principle is: when in doubt, hold the money in a known pending state rather than guessing.

Q2: How do you ensure that your ledger stays in balance even during partial failures?

Answer: The ledger must be treated as the single source of truth for all financial data. The approach is: (1) Every payment operation must first create a pending ledger entry as part of the same database transaction that updates the payment state. This uses the database ACID guarantees to ensure atomicity. (2) The pending entry is created with a status that indicates it is not yet finalized. (3) When the operation completes (capture succeeds, refund settles), the entry is updated to its final status. (4) A background reconciliation process scans for entries that have been in pending state for too long (typically more than 7 days) and triggers investigation. (5) Double-entry bookkeeping provides an additional safety net - if debits and credits do not balance for any transaction, an alert fires immediately. (6) The hash chain provides tamper detection - any modification to a historical entry breaks the chain, which the reconciliation process catches. The key insight is that you cannot rely on any single mechanism - you need multiple layers of defense.

Q3: Design the payout system for a marketplace where sellers need to receive funds after each transaction.

Answer: Marketplace payouts are complex because they involve splitting payments, holding reserves, and handling rolling balances. The design: (1) When a payment is captured, the full amount is credited to a holding account in the ledger, not directly to the seller. (2) A payout engine runs on a configurable schedule (daily, weekly) that calculates the net amount owed to each seller: captured amounts minus refunds minus chargebacks minus platform fees minus reserve holdback. (3) The reserve is typically 5-10% of rolling 90-day volume, held for 90 days to cover potential chargebacks. (4) Payouts are batched - individual seller payouts are aggregated into a single ACH/wire transfer to reduce banking fees. (5) The payout process uses a two-phase commit: first, a pending payout entry is created and the seller available balance is debited; second, when the bank confirms the transfer, the payout entry is finalized. (6) If the bank transfer fails (invalid account, etc.), the pending payout is reversed and the seller is notified.

Q4: How would you design the system to handle 10x traffic spikes during flash sales?

Answer: Flash sales create extreme traffic patterns that must be handled without degrading the payment experience. The approach: (1) Pre-provision compute capacity based on merchant forecasts - work with key merchants to understand expected peak volumes. (2) Implement a queue-based architecture for the authorization path: payments are accepted into a durable Kafka queue (which can handle 100K+ TPS) and processed by a pool of workers that auto-scale based on queue depth. (3) The client receives a processing status immediately, with a promise to confirm within N seconds. (4) Use rate limiting per merchant and per customer to prevent any single actor from consuming disproportionate resources. (5) Pre-compute and cache fraud models and FX rates to minimize per-request computation. (6) Use read replicas aggressively - dashboard queries, analytics, and reconciliation can all run on replicas. (7) Implement circuit breakers on processor connections to prevent cascade failures if one processor cannot handle the load. The key insight is that queuing provides natural load leveling - you do not need to size your entire infrastructure for peak burst, just your queue ingestion.

Q5: Explain how you would prevent double-charging a customer during a network partition.

Answer: Double-charging is one of the most severe bugs in a payment system. Prevention requires multiple layers: (1) Client-side idempotency: the client generates a unique idempotency key for each payment attempt. If a network partition causes the client to not receive the response, it retries with the same key. (2) Server-side idempotency: the idempotency middleware checks for existing keys before processing. (3) Database-level protection: the payment_intents table has a unique constraint on (merchant_id, idempotency_key), and the charges table has a unique constraint on (payment_intent_id, capture_amount, idempotency_key). (4) Processor-level idempotency: we pass the same idempotency key to the processor on retries, so even if our server processes the same request twice, the processor deduplicates it. (5) Database locking: the capture operation uses SELECT FOR UPDATE on the payment row, preventing concurrent captures. (6) Monitoring: we run a continuous background job that checks for duplicate processor transactions and alerts immediately. No single layer is sufficient - the defense must be in depth.

Q6: How do you handle PCI DSS compliance while still providing a good developer experience?

Answer: The key is to minimize the PCI scope of the merchant while providing a seamless integration. (1) Client-side tokenization: provide JavaScript, iOS, and Android SDKs that handle card data directly. Card numbers are tokenized in the browser using a tokenization service running on PCI-certified infrastructure. The merchant server never sees raw card data. (2) Hosted payment fields: for merchants who want more control, provide hosted iframes for card input that post directly to our PCI zone. (3) For merchants who need to pass card data server-to-server (for example, for migration from a legacy system), provide a server-side tokenization endpoint that runs in a separate PCI-isolated network segment. (4) Use network tokens (Visa/MC tokens) instead of raw card data wherever possible - network tokens have lower PCI scope and better authorization rates. (5) Provide clear documentation and compliance guides for each integration method. The goal is that a merchant should be able to integrate with minimal PCI scope (SAQ A or SAQ A-EP) while still having full control over the payment experience.

Q7: Describe how you would build the reconciliation system to catch discrepancies in real-time.

Answer: Real-time reconciliation requires a streaming architecture rather than traditional batch processing. (1) As each payment is processed, we create an internal event that includes the processor transaction ID, amount, status, and timestamp. (2) We subscribe to processor webhooks or polling feeds that provide confirmation of each transaction. (3) A stream processing job (Kafka Streams or Flink) joins the internal events with processor confirmations in real-time, comparing amounts and statuses. (4) Discrepancies are immediately routed to an alert queue and a resolution workflow. (5) For settlement reconciliation, we subscribe to processor settlement feeds (usually daily files) and match them against our ledger entries. (6) For bank reconciliation, we ingest bank statements (MT940 format) and match transfers against payout records. (7) The system tracks a reconciliation score for each merchant and processor - when the score drops below a threshold, it triggers investigation. The key innovation is treating reconciliation as a continuous stream rather than a daily batch job.

Q8: How would you design the multi-currency settlement system?

Answer: Multi-currency settlement is complex because exchange rates fluctuate between payment time and settlement time. The design: (1) At payment time, record the payment in the original currency and lock the FX rate for a configurable window (typically 24-72 hours). (2) At settlement time, if the rate lock has not expired, settle at the locked rate. If it has expired, settle at the current rate and record the FX gain/loss. (3) The ledger must track amounts in both the original currency and the settlement currency, with separate entries for the FX component. (4) For merchants who operate in multiple currencies, provide the option to receive settlements in their preferred currency with automatic conversion. (5) For merchants who want to accept multi-currency payments but settle in a single currency, the conversion happens at capture time. (6) All FX rates are sourced from multiple providers and the median is used to prevent manipulation. (7) The reconciliation process separately reconciles the FX component to detect rate manipulation or data entry errors. The key principle is that every currency conversion must be explicitly recorded in the ledger with the rate used, the source, and the timestamp.

Payment Processing System - Senior+ Guide