system-design57 min read

How to Design Stripe Connect - Marketplace Payment Platform — A Senior+ Guide

How to Design Stripe Connect — Marketplace Payment Platform

A Senior+ Guide to Building a Multi-Sided Payment Infrastructure for Marketplaces, SaaS, and Platforms

Article #193 Published: April 29, 2024 ~45 min read Category: System Design

1. Introduction: Stripe Connect at Scale

Stripe Connect is one of the most sophisticated payment orchestration platforms in the world, powering marketplaces, SaaS platforms, and gig economy companies that process hundreds of billions of dollars annually. From companies like Shopify and Lyft to Instacart and DoorDash, Stripe Connect enables platforms to move money between multiple parties — buyers, sellers, service providers, and the platform itself — all within a unified, developer-friendly API surface. Understanding how to design a system of this magnitude is one of the most challenging and rewarding exercises in distributed systems engineering.

At its core, Stripe Connect solves the fundamental problem of multi-party payments. In a traditional e-commerce transaction, a buyer pays a merchant and the merchant receives the funds. But in a marketplace scenario, a single payment from a buyer must be intelligently split among a seller, a service provider, a delivery driver, and the platform itself, each receiving their designated portion at the appropriate time. This seemingly simple requirement introduces extraordinary complexity when you consider regulatory compliance, international banking, currency conversion, fraud prevention, dispute resolution, and real-time risk assessment.

The scale at which Stripe Connect operates is staggering. Processing over $1 trillion in total payment volume annually across millions of connected accounts, the platform must handle peak loads during events like Black Friday or Cyber Monday while maintaining sub-second latency for payment authorization decisions. Every transaction must pass through a gauntlet of validation steps including identity verification, sanctions screening, fraud detection, sufficient balance checks, and regulatory compliance — all completed within the narrow window of a customer's patience at checkout.

What makes Stripe Connect particularly fascinating from a system design perspective is the layering of concerns. The platform must simultaneously satisfy the needs of three distinct stakeholders: the platform operator who builds on Stripe, the connected accounts (sellers, merchants, service providers) who receive payments, and the end customers who make payments. Each stakeholder has different requirements for onboarding, reporting, compliance, and user experience, and the system must elegantly accommodate all three without compromising security or performance.

In this comprehensive guide, we will dissect the architecture of a Stripe Connect-like platform from the ground up. We will examine how onboarding flows differ based on account types, how payments are processed and split across multiple recipients, how the system handles international currencies and regulatory requirements, and how fraud detection operates in real-time. We will explore the event-driven webhook system that keeps platforms synchronized with payment state changes, the idempotent API design that ensures reliability, and the embedded UI components that allow platforms to offer white-labeled payment experiences.

Whether you are designing a marketplace, building a SaaS platform with payment facilitation, or preparing for a senior engineering interview, this guide will provide you with a deep understanding of the architectural patterns, trade-offs, and engineering decisions that underpin modern multi-party payment infrastructure. Let us begin by examining the three account types that form the foundation of the Connect model and understanding why each exists to serve different platform needs and risk profiles.

2. Connect Account Types (Standard, Express, Custom)

Stripe Connect offers three distinct account types — Standard, Express, and Custom — each representing a different point on the spectrum between platform control and user experience. Understanding these account types is essential because they determine how onboarding works, who manages the account lifecycle, and what level of customization the platform can achieve. The choice of account type has profound implications for both engineering complexity and regulatory responsibility.

Standard Accounts

Standard connected accounts are the simplest integration model. When a platform uses Standard accounts, the connected sellers or service providers interact directly with Stripe's dashboard for their account details, banking information, and identity verification. The platform benefits from minimal onboarding overhead because Stripe handles the entire KYC (Know Your Customer) process through its own hosted interface. The connected account holder can manage their own disputes, view their own analytics, and update their own information without any involvement from the platform.

The engineering advantage of Standard accounts is significant. The platform does not need to build or maintain any onboarding UI, identity verification flows, or account management interfaces. The API integration is straightforward: the platform creates a Checkout Session or Payment Intent with the account parameter pointing to the connected Standard account, and Stripe handles the rest. This model works well for platforms where sellers are sophisticated enough to manage their own payment accounts and where the platform does not need deep control over the payment experience.

Express Accounts

Express accounts represent the middle ground. The connected account holder uses Stripe's Express onboarding flow — a hosted, mobile-optimized interface that collects identity documents, tax information, and banking details. However, the platform retains more control over the payment experience than with Standard accounts. The Express Dashboard is a stripped-down interface that shows connected accounts their balance, payout history, and basic account status without exposing the full Stripe Dashboard complexity.

From an architectural perspective, Express accounts require the platform to implement an onboarding redirect flow. The platform generates an AccountLink that sends the connected account holder to Stripe's hosted onboarding page. Upon completion, Stripe fires a webhook event (account.external_account.created, account.updated) that the platform must process to update its internal state. The platform must also handle the case where onboarding is incomplete — perhaps the user abandoned the flow mid-way — and implement reminder mechanisms.

Custom Accounts

Custom accounts provide maximum platform control at the cost of maximum engineering complexity. The platform is entirely responsible for collecting identity information, tax documents, and banking details from connected accounts. The platform must build its own onboarding UI, its own identity verification flow (or integrate with a third-party KYC provider), and its own account management dashboard. Stripe never interacts directly with the connected account holder for account management purposes.

Custom accounts are the most challenging to implement but offer the most seamless experience for the end user. The connected account holder never sees a Stripe-branded page, and the platform can fully customize every aspect of the onboarding and account management experience. This model is preferred by large platforms that want to maintain complete brand consistency and control the user journey end-to-end.

Comparison of Stripe Connect Account Types
FeatureStandardExpressCustom
OnboardingStripe DashboardStripe Express hosted pagePlatform-built UI
Account ManagementStripe DashboardExpress DashboardPlatform-built UI
KYC/AML ResponsibilityStripeStripePlatform + Stripe
BrandingStripe brandingMinimal Stripe brandingFull platform branding
Setup ComplexityLowMediumHigh
Platform ResponsibilityMinimalModerateMaximum
Best ForSimple marketplacesGig platformsEnterprise SaaS
Payout ManagementStripe-managedStripe-managedPlatform-managed

The decision between account types is not merely a UX choice — it has significant implications for liability and regulatory compliance. With Custom accounts, the platform assumes greater responsibility for ensuring that connected accounts meet regulatory requirements, including maintaining records of identity verification, monitoring for suspicious activity, and filing required tax documents. This increased responsibility must be reflected in the platform's compliance infrastructure and legal framework. A well-designed platform will support multiple account types simultaneously, allowing different categories of connected accounts to choose the onboarding experience that best suits their needs and technical capabilities.

3. System Architecture Overview

The architecture of a Stripe Connect-like platform is a masterclass in distributed systems design. At the highest level, the system must coordinate between an API gateway layer that handles authentication and rate limiting, a core payment processing engine that orchestrates money movement, a compliance subsystem that performs real-time KYC and sanctions screening, a risk engine that scores every transaction, and a settlement system that interfaces with banking networks worldwide. Each of these subsystems must operate with extreme reliability because payment failures directly impact revenue and user trust.

graph TB Client[Client Applications] --> APIGateway[API Gateway
Rate Limiting and Auth] APIGateway --> AccountService[Account Service
Onboarding and KYC] APIGateway --> PaymentService[Payment Service
Intent and Confirmation] APIGateway --> TransferService[Transfer Service
Split Logic] APIGateway --> PayoutService[Payout Service
Bank Integration] AccountService --> KYCProvider[KYC/AML Provider] AccountService --> AccountDB[(Account Database)] PaymentService --> PaymentDB[(Payment Database)] PaymentService --> FraudEngine[Risk/Fraud Engine] PaymentService --> CardNetwork[Card Networks] TransferService --> TransferDB[(Transfer Database)] TransferService --> BalanceLedger[Balance Ledger] PayoutService --> BankingPartner[Banking Partner] PayoutService --> PayoutDB[(Payout Database)] FraudEngine --> RulesEngine[Rules Engine] FraudEngine --> MLModel[ML Risk Model] BalanceLedger --> AccountingService[Accounting Service] AccountingService --> TaxService[Tax Reporting] PaymentService --> WebhookService[Webhook Service] WebhookService --> EventStore[(Event Store)] WebhookService --> PlatformCallbacks[Platform Callbacks]

API Gateway Layer

The API gateway is the front door of the entire system. It must authenticate requests using OAuth 2.0 or API key-based authentication, enforce rate limits per-platform to prevent abuse, route requests to the appropriate microservice based on the endpoint, and provide request/response transformation for backward compatibility. The gateway must also handle idempotency keys — clients can pass an Idempotency-Key header on any POST request, and the gateway must ensure that repeated requests with the same key return the same response without re-executing the underlying operation.

Payment Processing Engine

The payment processing engine is the heart of the system. When a payment is initiated, the engine must validate the payment parameters, check the connected account's status and capabilities, assess fraud risk in real-time, determine the split of funds between recipients, submit the authorization request to the appropriate card network or bank, handle the asynchronous response, and update all relevant databases. This entire flow must complete in under 2 seconds for card payments and under 5 seconds for bank transfers.

Balance Ledger

The balance ledger is arguably the most critical component in the system. It must track the real-time balance of every connected account with absolute accuracy because it determines whether an account can receive payments, initiate payouts, or process refunds. The ledger follows double-entry bookkeeping principles — every credit to one account must have a corresponding debit elsewhere. The ledger must support concurrent access from multiple services while maintaining consistency guarantees, making it a prime candidate for implementation using a serialized event-sourced architecture.

Core Microservices and Their Responsibilities
ServiceResponsibilityLatency TargetAvailability
API GatewayAuthentication, rate limiting, routing< 10ms99.999%
Account ServiceOnboarding, KYC orchestration, capabilities< 200ms99.99%
Payment ServiceIntent creation, confirmation, capture< 1500ms99.999%
Transfer ServiceFund splitting, destination routing< 500ms99.99%
Payout ServiceBank file generation, payout schedulingBatch (hours)99.99%
Fraud EngineReal-time risk scoring, rules evaluation< 50ms99.99%
Balance LedgerDouble-entry bookkeeping, balance queries< 20ms99.9999%
Webhook ServiceEvent dispatch, retry delivery, monitoring< 500ms99.99%

The architecture must be designed for horizontal scalability from day one. Each microservice must be stateless or externalize state to databases and caches so that additional instances can be spun up during traffic spikes. The payment processing engine, in particular, must support automatic scaling based on queue depth — when a surge of payment authorizations arrives, the system must rapidly provision additional worker instances to handle the load without degrading latency for any individual transaction.

Database and Storage Layer

The data storage strategy must balance consistency, availability, and partition tolerance according to the CAP theorem. The balance ledger requires strong consistency — eventual consistency is unacceptable when dealing with money. The payment database must support ACID transactions with proper isolation levels to prevent double-spending. The event store for webhooks can tolerate eventual consistency since webhooks are inherently retry-based. In practice, a Stripe Connect-like system uses a combination of relational databases for financial data, distributed caches for hot data like rate limits and account status, and event streaming platforms for inter-service communication and event sourcing.

4. Onboarding and KYC/AML Verification

Onboarding connected accounts is a critical flow that determines whether a seller or service provider can participate in the platform's payment ecosystem. The onboarding process must satisfy multiple requirements simultaneously: collect accurate identity information for regulatory compliance, verify that the individual or business is not on any sanctions lists, assess the risk profile of the new account, and do all of this quickly enough that the user does not abandon the process. In practice, onboarding completion rates directly correlate with platform revenue, so every additional friction point must be carefully evaluated against its compliance value.

Identity Verification Pipeline

The identity verification pipeline begins the moment a platform initiates an account creation request. For Custom accounts, the platform collects the required information — legal name, date of birth, address, tax identification number, business registration documents, and beneficial ownership details — and submits it to the platform's verification service. This service orchestrates a multi-step verification process that includes document verification (checking that uploaded IDs are authentic), database checks (verifying the identity against government records), sanctions screening (checking against OFAC, EU, and UN sanctions lists), and PEP (Politically Exposed Person) checks.

The verification pipeline must be designed for both speed and accuracy. Document verification typically involves computer vision models that can detect forged documents, liveness checks that ensure the person submitting documents is physically present, and OCR (Optical Character Recognition) that extracts structured data from identity documents. These checks must complete in seconds for the best user experience, but the system must also handle cases where automated verification is inconclusive and human review is required.

sequenceDiagram participant P as Platform participant AS as Account Service participant KYC as KYC Provider participant Sanctions as Sanctions DB participant DB as Account DB participant WH as Webhook Service P->>AS: POST /accounts AS->>DB: Store account status=uninitialized AS->>KYC: Submit identity documents KYC-->>AS: Verification in progress KYC->>AS: Verification complete AS->>Sanctions: Screen against sanctions Sanctions-->>AS: Clear or Match AS->>DB: Update account status AS->>WH: Fire account.updated event

Progressive Onboarding

Modern marketplace platforms implement progressive onboarding — allowing accounts to begin receiving limited payment activity while full verification completes in the background. This approach dramatically improves the time-to-first-transaction for new sellers. For example, a new seller might be allowed to receive payments up to a threshold of $2,000 while their identity verification is still in progress. Once verification completes, the threshold is lifted, and the account gains full capabilities.

Progressive onboarding requires the system to maintain a detailed capability model for each connected account. Each capability — receiving payments, sending payouts, accepting international payments, processing high-value transactions — is independently gated by the account's verification status. The payment processing engine must check these capabilities before accepting each transaction, creating a real-time authorization matrix that maps account status to allowed operations.

Document Management and Retention

Regulatory requirements mandate that identity verification documents be retained for specific periods — typically 5 years after account closure in the United States, with variations by jurisdiction. The document management system must store these documents securely with encryption at rest, implement access controls that limit who can view sensitive documents, support efficient retrieval for regulatory audits, and eventually purge documents when retention periods expire. Given the sensitivity of this data, the document storage system is typically isolated from the rest of the infrastructure with its own security perimeter.

Onboarding Requirements by Account Type
Data FieldStandardExpressCustom
Legal NameCollected by StripeCollected by StripeCollected by Platform
Date of BirthCollected by StripeCollected by StripeCollected by Platform
AddressCollected by StripeCollected by StripeCollected by Platform
Tax IDCollected by StripeCollected by StripeCollected by Platform
Bank AccountManaged by sellerCollected by StripeCollected by Platform
ID DocumentCollected by StripeCollected by StripeCollected by Platform
Business RegistrationN/AIf applicableCollected by Platform
Beneficial OwnershipN/AIf applicableCollected by Platform

The onboarding system must also handle ongoing due diligence. KYC is not a one-time event — platforms must periodically re-verify account information, especially for accounts that have been flagged for unusual activity or that have reached certain transaction thresholds. The system should implement automated triggers that initiate re-verification when specific conditions are met, such as a sudden spike in transaction volume, a change in the account's business category, or the passage of a regulatory-mandated review period.

5. Payment Processing (Charges, Transfers, Destinations)

Payment processing in a Connect environment is significantly more complex than standard single-party payments because every transaction must determine not just whether the payment should be authorized, but how the resulting funds should be distributed among multiple recipients. The payment lifecycle in Connect involves creating a PaymentIntent, optionally attaching it to a connected account, confirming the payment, handling the authorization response from the card network, and then initiating the transfer of funds to the appropriate destinations.

PaymentIntent Lifecycle

The PaymentIntent is the central object in Stripe's payment model. It represents the intent to collect a payment from a customer and tracks the lifecycle of that payment from creation through settlement. In a Connect context, the PaymentIntent must also track how funds will be split. The platform creates a PaymentIntent with the total amount and currency, then attaches transfer data that specifies which connected accounts receive what portions. The PaymentIntent transitions through states: requires_payment_method, requires_confirmation, requires_action (for 3D Secure), processing, succeeded, requires_capture (for manual capture), or canceled.

C#// PaymentIntent creation for a marketplace split payment
public class PaymentIntentService
{
    private readonly IPaymentGateway _gateway;
    private readonly ITransferService _transferService;
    private readonly IFraudEngine _fraudEngine;

    public async Task<PaymentResult> CreateMarketplacePayment(
        MarketplacePaymentRequest request)
    {
        // Step 1: Validate the platform and connected accounts
        var platform = await _gateway.GetPlatform(request.PlatformId);
        if (platform.Status != PlatformStatus.Active)
            throw new PlatformInactiveException(request.PlatformId);

        var sellerAccount = await _gateway.GetConnectedAccount(
            request.SellerAccountId);
        if (!sellerAccount.Capabilities.Contains("transfers"))
            throw new AccountCapabilityException(
                "Seller cannot receive transfers");

        // Step 2: Calculate the split amounts
        var splits = CalculateSplit(
            request.TotalAmount,
            request.PlatformFeePercent,
            request.SellerAmount);

        // Step 3: Assess fraud risk before creating the intent
        var riskScore = await _fraudEngine.ScoreTransaction(
            new TransactionContext
            {
                Amount = request.TotalAmount,
                Currency = request.Currency,
                CustomerId = request.CustomerId,
                SellerAccountId = request.SellerAccountId,
                IPAddress = request.CustomerIPAddress,
                DeviceFingerprint = request.DeviceFingerprint
            });

        if (riskScore.Recommendation == RiskRecommendation.Reject)
            throw new FraudRejectionException(riskScore.Reason);

        // Step 4: Create the PaymentIntent with transfer data
        var intent = await _gateway.CreatePaymentIntent(
            new PaymentIntentParams
            {
                Amount = request.TotalAmount,
                Currency = request.Currency,
                ApplicationFeeAmount = splits.PlatformFee,
                TransferData = new TransferDestination
                {
                    Destination = request.SellerAccountId,
                    Amount = splits.SellerPayout
                },
                CaptureMethod = request.AutoCapture
                    ? CaptureMethod.Automatic
                    : CaptureMethod.Manual,
                Metadata = new Dictionary<string, string>
                {
                    ["platform_id"] = request.PlatformId,
                    ["order_id"] = request.OrderId,
                    ["risk_score"] = riskScore.Score.ToString("F2")
                }
            });

        return new PaymentResult
        {
            PaymentIntentId = intent.Id,
            ClientSecret = intent.ClientSecret,
            RiskScore = riskScore.Score,
            Status = intent.Status
        };
    }

    private SplitAmounts CalculateSplit(
        decimal totalAmount,
        decimal platformFeePercent,
        decimal? sellerAmount)
    {
        var platformFee = (long)(
            totalAmount * platformFeePercent / 100m);
        var sellerPayout = totalAmount - platformFee;
        return new SplitAmounts
        {
            PlatformFee = platformFee,
            SellerPayout = sellerPayout,
            TotalAmount = totalAmount
        };
    }
}

Destination Charges vs. Direct Charges

Stripe Connect supports two primary models for directing funds to connected accounts: destination charges and direct charges. With destination charges, the platform's own Stripe account receives the full payment amount, then initiates a transfer to the connected account minus the platform's fee. The platform essentially acts as the merchant of record. With direct charges, the connected account is the merchant of record — the payment goes directly to the connected account, and the platform fee is collected separately as an application fee.

The choice between these models has significant implications. Destination charges give the platform more control over the payment flow and simplify refunds (the platform can refund from its own balance). Direct charges are simpler for the platform to implement but require the connected account to have sufficient balance to cover refunds and chargebacks. The platform must carefully evaluate its business model, risk tolerance, and technical requirements when choosing between these approaches.

graph LR subgraph Destination Charge Flow Customer1[Customer] -->|Pays $100| Platform1[Platform Account] Platform1 -->|Transfers $85| Seller1[Seller Account] Platform1 -->|Retains $15| Fee1[Platform Fee] end subgraph Direct Charge Flow Customer2[Customer] -->|Pays $100| Seller2[Seller Account] Platform2[Platform Account] -->|Collects $15 App Fee| Seller2 Platform2 -->|Retains $15| Fee2[Platform Fee] end

Authorization and Capture

The authorization and capture flow in Connect must account for the additional complexity of split payments. When a card is authorized for $100, the card network holds $100 from the customer's available credit. However, the actual movement of funds happens later during settlement. The platform must ensure that the transfer amounts sum correctly to the authorized amount minus any Stripe fees and that no funds are created or destroyed in the process. The system must also handle partial captures — if the platform authorized $100 but only captures $80, the transfer amounts must be adjusted proportionally.

Payment Flow Comparison
AspectDestination ChargeDirect ChargeSeparate Charges and Transfers
Merchant of RecordPlatformConnected AccountPlatform (charges) / Account (transfers)
Refund HandlingFrom platform balanceFrom connected account balanceFrom platform balance
Chargeback LiabilityPlatformConnected AccountPlatform
Split TimingAt payment timeAt payment timeAfter payment, separate transfer
ComplexityLowLowHigh
Use CaseMost marketplacesSimple platformsComplex multi-party payouts

The payment processing system must also handle various failure modes gracefully. Network timeouts during authorization must be handled with retry logic that is careful not to double-authorize. Declined transactions must be mapped to appropriate error codes that the platform can present to the customer. And the system must handle the case where a payment succeeds at the card network level but fails during the transfer to the connected account — perhaps because the account is restricted — by automatically refunding the customer and notifying the platform via webhooks.

6. Split Payments and Fee Management

Split payments are the defining feature of marketplace payment platforms. In a marketplace, a single customer payment must be intelligently divided among multiple recipients: the seller who provides the product or service, the platform that facilitates the transaction, and potentially additional parties such as delivery partners, payment facilitators, or insurance providers. The split payment engine must handle arbitrary split configurations, support dynamic fee calculations based on business rules, and maintain perfect accounting accuracy across millions of concurrent transactions.

Multi-Party Split Architecture

A marketplace platform might need to split a single $100 payment among five different recipients: $70 to the seller, $15 to the platform as a service fee, $10 to a delivery partner, $3 to a payment processing fee pool, and $2 to an escrow reserve for potential refunds. The system must support this level of granularity while ensuring that all splits sum exactly to the total payment amount. Any rounding errors or penny discrepancies must be caught and handled — typically by assigning the residual to the platform fee.

C#// Multi-party split payment engine
public class SplitPaymentEngine
{
    private readonly IBalanceLedger _ledger;
    private readonly IRoundingService _rounding;

    public async Task<SplitResult> ExecuteSplit(SplitRequest request)
    {
        var splits = new List<TransferInstruction>();
        var remainingAmount = request.TotalAmount;

        // Apply splits in priority order (highest priority first)
        var orderedRules = request.SplitRules
            .OrderBy(r => r.Priority).ToList();

        foreach (var rule in orderedRules)
        {
            long splitAmount;
            if (rule.Type == SplitType.Fixed)
                splitAmount = rule.FixedAmount;
            else if (rule.Type == SplitType.Percentage)
                splitAmount = _rounding.Round(
                    request.TotalAmount * rule.Percentage / 100m);
            else if (rule.Type == SplitType.Remainder)
                splitAmount = remainingAmount;
            else
                throw new UnknownSplitTypeException(rule.Type);

            if (splitAmount > remainingAmount &&
                rule.Type != SplitType.Remainder)
                throw new InsufficientAmountException(
                    $"Split for {rule.RecipientId} exceeds remaining");

            splits.Add(new TransferInstruction
            {
                RecipientId = rule.RecipientId,
                RecipientType = rule.RecipientType,
                Amount = splitAmount,
                Description = rule.Description
            });
            remainingAmount -= splitAmount;
        }

        if (remainingAmount != 0 &&
            !orderedRules.Any(r => r.Type == SplitType.Remainder))
            throw new SplitBalanceException(
                $"Split imbalance: {remainingAmount} unaccounted");

        // Execute all transfers atomically
        await _ledger.ExecuteAtomicTransfer(new AtomicTransferRequest
        {
            SourceAccountId = request.SourceAccountId,
            TotalDebit = request.TotalAmount,
            Transfers = splits
        });

        return new SplitResult
        {
            TotalAmount = request.TotalAmount,
            Transfers = splits,
            ExecutedAt = DateTime.UtcNow
        };
    }
}

Platform Fee Structures

Marketplace platforms typically employ one of several fee structures. The most common is a percentage-based fee, where the platform takes a fixed percentage of each transaction. Some platforms use a tiered fee structure where the percentage decreases as the seller's volume increases, incentivizing higher-volume sellers. Others use a combination of percentage and fixed fees. The fee calculation engine must support all these models and be flexible enough to handle custom fee arrangements for enterprise sellers.

Platform Fee Models
Fee ModelCalculationExampleBest For
Flat PercentageTotal x Rate$100 x 15% = $15Simple marketplaces
Percentage + Fixed(Total x Rate) + Fixed($100 x 2.9%) + $0.30 = $3.20SaaS platforms
TieredRate changes at thresholdsFirst $10K: 15%, Next: 12%Growing marketplaces
Flat FeeFixed amount per transaction$2.00 per transactionHigh-value goods
DynamicBased on category, risk, geoVaries per transactionEnterprise platforms

Escrow and Reserve Management

Many marketplace platforms hold funds in escrow to protect against disputes, refunds, and chargebacks. The escrow system must track reserved amounts separately from available balances, release reserves according to configurable holding periods (e.g., 14 days after delivery confirmation), and handle the case where a dispute is filed during the holding period. The reserve balance must be held in a segregated account to ensure that escrowed funds are not commingled with operational funds, satisfying both regulatory requirements and accounting best practices.

graph TB Payment[Customer Payment $100] --> SplitEngine[Split Engine] SplitEngine --> SellerPayout[Seller $70] SplitEngine --> PlatformFee[Platform Fee $15] SplitEngine --> DeliveryFee[Delivery Partner $10] SplitEngine --> ProcessingFee[Processing Pool $3] SplitEngine --> EscrowReserve[Escrow Reserve $2] EscrowReserve --> HoldingPool[Holding Pool] HoldingPool -->|After 14 days| SellerRelease[Seller Release $2] HoldingPool -->|On dispute| DisputeFund[Dispute Fund]

The split payment engine must also handle edge cases such as partial refunds, where only a portion of the original payment is refunded and the splits must be recalculated proportionally. If a $100 payment was split as $70 to the seller and $15 to the platform, and the customer receives a 50% refund, the system must refund $50 to the customer, claw back $35 from the seller and $7.50 from the platform fee, and update all balances accordingly. These partial refund calculations must account for rounding to ensure that the refund amount exactly matches the sum of the individual clawbacks.

7. Payout Scheduling and Banking Integration

Payout scheduling determines when and how connected accounts receive their accumulated balances. This is a critical system because it directly affects seller cash flow and satisfaction, while also creating significant operational complexity around banking integration, regulatory compliance, and failure handling. The payout system must support multiple payout schedules (daily, weekly, monthly, on-demand), interface with various banking networks worldwide (ACH in the US, SEPA in Europe, BACS in the UK, BECS in Australia), and handle the inevitable failures that occur when bank transfers are rejected due to invalid account details, closed accounts, or compliance holds.

Payout Pipeline Architecture

The payout pipeline operates as a multi-stage batch process. First, the system identifies all accounts that are eligible for payout based on their configured schedule and current balance. Next, it validates that each account meets the minimum payout threshold and is not subject to any holds or restrictions. Then it generates the payout instructions, grouping transactions into appropriate batches based on the banking network requirements. Finally, it submits the batch to the banking partner and monitors for confirmation or rejection.

C#// Payout scheduling and batch generation
public class PayoutScheduler
{
    private readonly IPayoutRepository _payoutRepo;
    private readonly IBalanceLedger _ledger;
    private readonly IBankingPartner _banking;
    private readonly IPayoutHoldService _holdService;

    public async Task<PayoutBatch> GenerateDailyPayouts()
    {
        var eligibleAccounts = await _payoutRepo
            .GetAccountsForPayout(PayoutSchedule.Daily);

        var batch = new PayoutBatch
        {
            BatchId = Guid.NewGuid(),
            CreatedAt = DateTime.UtcNow,
            TargetSettlementDate = DateTime.UtcNow.AddDays(2)
        };

        foreach (var account in eligibleAccounts)
        {
            var balance = await _ledger
                .GetAvailableBalance(account.Id);
            if (balance < account.MinimumPayoutThreshold)
                continue;

            var holds = await _holdService
                .GetActiveHolds(account.Id);
            var holdAmount = holds.Sum(h => h.Amount);
            var availableForPayout = balance - holdAmount;

            if (availableForPayout <= 0)
                continue;

            var bankAccount = await _payoutRepo
                .GetPrimaryBankAccount(account.Id);
            if (bankAccount == null ||
                bankAccount.Status != BankAccountStatus.Verified)
                continue;

            batch.Instructions.Add(new PayoutInstruction
            {
                AccountId = account.Id,
                Amount = availableForPayout,
                Currency = account.Currency,
                BankAccount = bankAccount,
                Method = DeterminePayoutMethod(account),
                Description = $"Payout for {account.BusinessName}"
            });
        }

        if (batch.Instructions.Any())
        {
            var result = await _banking.SubmitBatch(batch);
            batch.ExternalBatchId = result.BatchId;
            batch.Status = PayoutBatchStatus.Submitted;

            foreach (var instruction in batch.Instructions)
            {
                await _ledger.Debit(instruction.AccountId,
                    instruction.Amount,
                    $"Payout batch {batch.BatchId}");
                await _payoutRepo.SavePayout(new PayoutRecord
                {
                    AccountId = instruction.AccountId,
                    Amount = instruction.Amount,
                    Status = PayoutStatus.Processing,
                    BatchId = batch.BatchId,
                    ExpectedArrival = batch.TargetSettlementDate
                });
            }
        }
        return batch;
    }

    private PayoutMethod DeterminePayoutMethod(
        ConnectedAccount account)
    {
        return account.Country switch
        {
            "US" => PayoutMethod.ACH,
            "GB" => PayoutMethod.BACS,
            "AU" => PayoutMethod.BECS,
            _ when IsSEPACountry(account.Country) =>
                PayoutMethod.SEPA,
            _ => PayoutMethod.Wire
        };
    }
}

Payout Timing and Settlement

Payout timing is governed by the underlying banking network's settlement schedule. ACH transfers in the United States typically take 2-3 business days to settle. SEPA transfers in Europe take 1-2 business days. Instant payout methods can deliver funds within minutes or hours but typically incur additional fees. The platform must clearly communicate expected payout timing to connected accounts and handle the discrepancy between when a payout is initiated and when funds actually arrive in the seller's bank account.

Payout Methods by Region
RegionPrimary MethodSettlement TimeInstant AvailableFee
United StatesACH2-3 business daysYes (same-day ACH)$0.25 per payout
European UnionSEPA1-2 business daysYes (SEPA Instant)EUR 0.25 per payout
United KingdomBACS3 business daysYes (Faster Payments)GBP 0.20 per payout
AustraliaBECS2-3 business daysYes (NPP)AUD 0.30 per payout
CanadaEFT2-3 business daysYes (Interac e-Transfer)CAD 0.25 per payout
JapanBank Transfer1-2 business daysNoJPY 250 per payout

Payout Failure Handling

Payout failures are inevitable in any large-scale payment system. Banks reject transfers for various reasons: incorrect account numbers, closed accounts, frozen accounts, compliance restrictions, or exceeding daily transfer limits. The payout failure handler must record the failure reason, credit the funds back to the connected account's available balance, notify the platform via webhook, and potentially flag the account for review if failures are recurring.

graph TB Scheduler[Payout Scheduler] --> Eligibility[Eligibility Check] Eligibility --> BalanceCheck{Balance OK?} BalanceCheck -->|No| Skip[Skip Account] BalanceCheck -->|Yes| HoldCheck{Active Holds?} HoldCheck -->|Yes| ReducedPayout[Reduced Payout] HoldCheck -->|No| FullPayout[Full Payout] ReducedPayout --> BankValidation[Bank Validation] FullPayout --> BankValidation BankValidation -->|Valid| SubmitBatch[Submit to Banking] BankValidation -->|Invalid| FlagAccount[Flag Account] SubmitBatch --> Monitor[Monitor Settlement] Monitor -->|Success| CreditBalance[Credit Seller] Monitor -->|Failed| HandleFailure[Handle Failure] HandleFailure --> RecreditBalance[Recredit Balance] HandleFailure --> NotifyPlatform[Webhook: payout.failed]

The payout system must also handle cross-border payouts, where the connected account's bank is in a different country or currency than the platform's operating currency. Cross-border payouts involve additional complexity including currency conversion at the banking level, correspondent banking fees, and compliance with both the originating and receiving country's regulations. The platform must clearly disclose all fees and exchange rates to the connected account holder.

8. Multi-Currency Support and FX Conversion

Multi-currency support is essential for any marketplace platform that operates internationally. Sellers in Japan must receive payouts in Japanese Yen, buyers in Germany must pay in Euros, and the platform must handle the currency conversion and foreign exchange (FX) risk that arises when transactions span multiple currencies. The multi-currency subsystem must support currency detection, real-time FX rate management, currency conversion for payments and payouts, and accurate financial reporting across all supported currencies.

Currency Routing Architecture

When a customer in the United States purchases a product from a seller in Japan, the payment system must decide how to handle the currency mismatch. There are several approaches: the customer pays in their local currency (USD) and the platform absorbs the FX conversion, the customer pays in the seller's currency (JPY) and the card network handles the conversion, or the platform offers the customer a choice of currencies at checkout with transparent FX rates. Each approach has different implications for FX risk, customer experience, and regulatory compliance.

The currency routing engine must maintain a comprehensive mapping of supported currencies, their minor unit denominations (0 for JPY, 2 for USD, 3 for Bahraini Dinar), and the conversion relationships between currency pairs. The engine must also track which currencies are supported for which operations — a platform might support collecting payments in 135 currencies but only disburse payouts in 40 currencies, requiring automatic conversion for unsupported payout currencies.

C#// Multi-currency payment processing with FX handling
public class MultiCurrencyPaymentService
{
    private readonly IFxRateProvider _fxRates;
    private readonly ICurrencyConverter _converter;
    private readonly IPaymentGateway _gateway;

    public async Task<CurrencyPaymentResult> ProcessCrossCurrency(
        CrossCurrencyPaymentRequest request)
    {
        var paymentCurrency = request.CustomerCurrency;
        var chargeCurrency = request.SellerCurrency;
        var isCrossCurrency = paymentCurrency != chargeCurrency;

        long chargeAmount;
        long? fxRate = null;
        long? platformFxFee = null;

        if (isCrossCurrency)
        {
            var marketRate = await _fxRates.GetRate(
                paymentCurrency, chargeCurrency);
            var platformMarkup = GetPlatformFxMarkup(
                request.PlatformId);

            fxRate = (long)(marketRate * (1 + platformMarkup));
            platformFxFee = (long)(
                request.SellerAmount * platformMarkup);

            chargeAmount = _converter.Convert(
                request.SellerAmount,
                chargeCurrency,
                paymentCurrency,
                fxRate.Value);
        }
        else
        {
            chargeAmount = request.SellerAmount;
        }

        var intent = await _gateway.CreatePaymentIntent(
            new PaymentIntentParams
            {
                Amount = chargeAmount,
                Currency = paymentCurrency,
                ApplicationFeeAmount = request.PlatformFee,
                TransferData = new TransferDestination
                {
                    Destination = request.SellerAccountId,
                    Amount = request.SellerAmount
                },
                Metadata = new Dictionary<string, string>
                {
                    ["fx_rate"] = fxRate?.ToString() ?? "1.0",
                    ["fx_fee"] = platformFxFee?.ToString() ?? "0",
                    ["original_currency"] = chargeCurrency,
                    ["original_amount"] =
                        request.SellerAmount.ToString()
                }
            });

        return new CurrencyPaymentResult
        {
            PaymentIntentId = intent.Id,
            ChargeCurrency = paymentCurrency,
            ChargeAmount = chargeAmount,
            FxRate = fxRate,
            FxFee = platformFxFee,
            Status = intent.Status
        };
    }

    private decimal GetPlatformFxMarkup(string platformId)
    {
        return 0.01m; // Default 1% markup
    }
}

FX Rate Management

FX rate management is a critical component that directly impacts platform profitability and customer trust. The platform must source real-time exchange rates from multiple providers, apply a configurable markup, handle rate volatility during the transaction lifecycle, and provide rate lock periods that give customers a guaranteed rate for a limited time. Rates are typically locked for 15-30 minutes after a quote is presented to the customer, and if the customer does not complete payment within that window, they must request a fresh quote.

Currency Support Matrix
CurrencyCodeMinor UnitsPaymentsPayoutsSettlement
US DollarUSD2YesYesYes
EuroEUR2YesYesYes
British PoundGBP2YesYesYes
Japanese YenJPY0YesYesYes
Australian DollarAUD2YesYesYes
Indian RupeeINR2YesLimitedNo
Brazilian RealBRL2YesLimitedNo
Nigerian NairaNGN2YesNoNo
graph LR Buyer[US Buyer Pays $100] --> Gateway[Payment Gateway] Gateway --> FXEngine[FX Conversion Engine] FXEngine -->|Rate: 149.50| RateProvider[FX Rate Provider] FXEngine -->|Apply 1% Markup| Markup[Rate: 150.995] Markup --> SellerAmount[JPY 15099] SellerAmount --> Transfer[Transfer to JP Seller] Transfer --> SellerBank[JP Seller Bank Account] FXEngine -->|FX Fee| PlatformFX[Platform FX Revenue]

Financial reporting across multiple currencies adds another layer of complexity. The platform's accounting system must track revenue, expenses, and balances in multiple currencies, handle currency translation for consolidated reporting, manage unrealized FX gains and losses, and comply with accounting standards for foreign currency transactions.

9. Fraud Detection and Risk Management (Radar)

Fraud detection in a marketplace payment platform is exponentially more complex than in a single-merchant environment because the platform must protect against fraud from multiple angles: stolen card usage by buyers, seller fraud (listing fake products, not delivering goods), account takeover of connected accounts, synthetic identity fraud during onboarding, and coordinated fraud rings that exploit the marketplace's trust mechanisms. Stripe's Radar system, which we are modeling here, uses a combination of machine learning models, rules engines, and network analysis to detect and prevent fraud in real-time across millions of transactions.

Real-Time Risk Scoring Pipeline

Every payment must be risk-scored within the authorization window — typically under 50 milliseconds. The risk scoring pipeline collects signals from multiple sources: the transaction details (amount, currency, merchant category), the customer's behavioral history (previous purchases, chargeback rate), device fingerprinting data (browser characteristics, IP address, device ID), network-level signals (velocity checks, known fraud patterns), and third-party data (address verification, CVV verification). These signals are fed into an ensemble of machine learning models that produce a composite risk score.

C#// Real-time fraud risk assessment engine
public class FraudRiskEngine
{
    private readonly IMLModel _gradientBoostModel;
    private readonly IMLModel _neuralNetworkModel;
    private readonly IRulesEngine _rulesEngine;
    private readonly IDeviceFingerprint _deviceService;
    private readonly IVelocityChecker _velocityChecker;

    public async Task<RiskAssessment> AssessTransaction(
        TransactionContext context)
    {
        var signals = new RiskSignals();

        signals.TransactionFeatures =
            ExtractTransactionFeatures(context);
        signals.DeviceFeatures = await _deviceService
            .AnalyzeDeviceFingerprint(
                context.DeviceFingerprint);
        signals.HistoricalFeatures =
            await GetHistoricalFeatures(
                context.CustomerId,
                context.SellerAccountId);
        signals.VelocityFeatures = await _velocityChecker
            .CheckVelocity(new VelocityQuery
            {
                CustomerId = context.CustomerId,
                SellerAccountId = context.SellerAccountId,
                IPAddress = context.CustomerIPAddress,
                Amount = context.Amount,
                TimeWindow = TimeSpan.FromMinutes(15)
            });

        // Run through ML models
        var gbScore = await _gradientBoostModel.Predict(signals);
        var nnScore = await _neuralNetworkModel.Predict(signals);

        // Ensemble the scores
        var mlScore = (gbScore * 0.6m) + (nnScore * 0.4m);

        // Apply business rules
        var ruleResult = _rulesEngine.Evaluate(context, signals);

        // Combine ML and rules
        var finalScore = CombineScores(mlScore, ruleResult);

        var recommendation = finalScore switch
        {
            >= 0.85m => RiskRecommendation.Allow,
            >= 0.60m => RiskRecommendation.Challenge,
            >= 0.40m => RiskRecommendation.Review,
            _ => RiskRecommendation.Reject
        };

        return new RiskAssessment
        {
            Score = finalScore,
            Recommendation = recommendation,
            MLBreakdown = new MLBreakdown
            {
                GradientBoostScore = gbScore,
                NeuralNetworkScore = nnScore,
                EnsembleScore = mlScore
            },
            RuleFlags = ruleResult.TriggeredRules,
            Requires3DS = finalScore >= 0.60m &&
                context.Requires3DS,
            Reason = ruleResult.PrimaryReason
        };
    }
}

Rules Engine

The rules engine complements the ML models by encoding known fraud patterns and business policies that are easier to express as deterministic rules than to learn from data. For example, a rule might flag any transaction where the billing country differs from the shipping country AND the amount exceeds $500 AND the customer's account is less than 30 days old. The rules engine must support complex boolean logic, configurable thresholds, and real-time updates so that the risk team can deploy new rules without code deployments.

Fraud Detection Signals and Weights
Signal CategorySpecific SignalsWeightLatency Impact
Device IntelligenceDevice fingerprint, IP reputation, geolocationHigh~5ms
Behavioral AnalyticsPurchase history, session behavior, typing patternsHigh~8ms
Velocity ChecksTransaction frequency, amount velocityMedium~3ms
Network AnalysisIP graph, device graph, email graphMedium~10ms
Card VerificationCVV check, AVS check, BIN analysisHigh~15ms
3D Secure3DS2 challenge result, issuer riskHigh~50ms if challenged
Cross-PlatformFraud database lookups, consortium dataMedium~7ms

Seller Risk Monitoring

Beyond transaction-level fraud, the platform must continuously monitor connected accounts for seller-side fraud patterns. These include sudden changes in transaction volume, an increase in dispute rates, patterns consistent with triangulation fraud, or listings that generate an unusual number of complaints. The seller monitoring system operates asynchronously, running batch analyses on seller behavior patterns and triggering alerts when anomalies are detected.

graph TB Transaction[Incoming Transaction] --> PreFilter[Pre-Filter] PreFilter -->|Pass| FeatureEngine[Feature Engineering] PreFilter -->|Fail| Block[Block Transaction] FeatureEngine --> MLModels[ML Ensemble Models] FeatureEngine --> RulesEng[Rules Engine] MLModels --> ScoreCombine[Score Combination] RulesEng --> ScoreCombine ScoreCombine --> Decision{Risk Decision} Decision -->|Allow| Approve[Approve Payment] Decision -->|Challenge| ThreeDS[3D Secure Challenge] Decision -->|Review| ManualReview[Manual Review] Decision -->|Reject| Decline[Decline Payment] ThreeDS --> ThreeDSResult{3DS Result} ThreeDSResult -->|Authenticated| Approve ThreeDSResult -->|Failed| Decline Approve --> UpdateModel[Update ML Models] Decline --> UpdateModel ManualReview --> UpdateModel

The fraud system must also implement a feedback loop where the outcomes of reviewed transactions are fed back into the ML models to improve future predictions. This continuous learning cycle is essential because fraud patterns evolve constantly, and models that are not regularly retrained will degrade in accuracy over time. The system should implement A/B testing for model versions to measure the impact of new models on fraud loss rates and false positive rates before full deployment.

10. Dispute and Chargeback Handling

Disputes and chargebacks are an inevitable reality in any payment system, and they become significantly more complex in a marketplace environment where the platform, the seller, and the customer all have different perspectives and interests. A chargeback occurs when a cardholder disputes a transaction with their issuing bank, claiming that the charge was unauthorized, the product was not as described, or the transaction was otherwise illegitimate. The platform must handle the dispute lifecycle efficiently while minimizing losses and maintaining trust with all parties.

Dispute Lifecycle Management

When a chargeback is initiated, the issuing bank debits the funds from the merchant's account (in a Connect context, this means the funds are clawed back from whoever received them) and provides a reason code that categorizes the dispute. The platform must immediately notify the seller, gather evidence from both the seller and the customer, compile a compelling representment package, and submit it to the issuing bank before the deadline. The entire process is time-sensitive — most issuing banks give merchants only 7-21 days to respond.

C#// Dispute management and representment service
public class DisputeManagementService
{
    private readonly IDisputeRepository _disputeRepo;
    private readonly IArtifactCollector _artifacts;
    private readonly IWebhookDispatcher _webhooks;
    private readonly IBalanceLedger _ledger;

    public async Task HandleChargeback(
        ChargebackNotification notification)
    {
        var dispute = await _disputeRepo
            .GetByPaymentId(notification.PaymentId)
            ?? new Dispute
            {
                PaymentId = notification.PaymentId,
                PlatformId = notification.PlatformId,
                SellerAccountId = notification.ConnectedAccountId
            };

        dispute.Status = DisputeStatus.UnderReview;
        dispute.ChargebackAmount = notification.Amount;
        dispute.ReasonCode = notification.ReasonCode;
        dispute.ReceivedAt = DateTime.UtcNow;
        dispute.Deadline = CalculateDeadline(
            notification.ReasonCode);

        // Freeze funds in the connected account
        await _ledger.PlaceHold(
            notification.ConnectedAccountId,
            notification.Amount,
            $"Chargeback hold for dispute {dispute.Id}");

        // Collect evidence artifacts automatically
        var evidence = await _artifacts.CollectEvidence(
            new EvidenceRequest
            {
                PaymentId = notification.PaymentId,
                SellerAccountId = notification.ConnectedAccountId,
                RequiredDocuments = GetRequiredEvidence(
                    notification.ReasonCode)
            });

        dispute.Evidence = evidence;
        await _disputeRepo.Save(dispute);

        await _webhooks.Dispatch(new WebhookEvent
        {
            EventType = "dispute.created",
            ObjectId = dispute.Id,
            Data = new
            {
                dispute.Id,
                dispute.PaymentId,
                dispute.ChargebackAmount,
                dispute.ReasonCode,
                dispute.Deadline,
                dispute.Status
            }
        });
    }

    public async Task<RepresentmentResult> SubmitRepresentment(
        string disputeId, RepresentmentPackage package)
    {
        var dispute = await _disputeRepo.GetById(disputeId);

        if (DateTime.UtcNow > dispute.Deadline)
            throw new DeadlineExpiredException(disputeId);

        ValidateEvidence(package, dispute.ReasonCode);

        var result = await SubmitToNetwork(dispute, package);

        dispute.Status = DisputeStatus.UnderRepresentment;
        dispute.RepresentmentSubmittedAt = DateTime.UtcNow;
        await _disputeRepo.Save(dispute);

        return new RepresentmentResult
        {
            DisputeId = disputeId,
            SubmittedAt = DateTime.UtcNow,
            EstimatedResolution = DateTime.UtcNow.AddDays(60)
        };
    }

    private TimeSpan CalculateDeadline(string reasonCode)
    {
        return reasonCode switch
        {
            "fraudulent" => TimeSpan.FromDays(45),
            "product_not_received" => TimeSpan.FromDays(30),
            "product_defective" => TimeSpan.FromDays(30),
            "duplicate" => TimeSpan.FromDays(30),
            _ => TimeSpan.FromDays(21)
        };
    }
}
Chargeback Reason Codes and Required Evidence
Reason CodeDescriptionRequired EvidenceSuccess Rate
fraudulentUnauthorized transactionAVS match, CVV match, IP match, device history15-25%
product_not_receivedCustomer claims non-deliveryTracking number, delivery confirmation, signature40-60%
product_defectiveProduct not as describedProduct description, return policy, refund offered30-45%
duplicateCustomer charged twiceOriginal transaction proof, unique order IDs60-80%
subscription_canceledRecurring charge after cancellationTerms of service, cancellation policy, comms logs35-50%
credit_not_processedCustomer claims refund was not issuedRefund receipt, communication records50-70%
graph TB Cardholder[Cardholder] -->|Disputes| IssuingBank[Issuing Bank] IssuingBank -->|Chargeback| Network[Card Network] Network -->|Debit| Platform[Platform Account] Platform -->|Freeze| SellerBalance[Seller Balance] Platform -->|Notify| Seller[Seller Notification] Seller -->|Provides Evidence| Platform Platform -->|Compiles| Representment[Representment Package] Representment -->|Submit| Network Network -->|Decision| Outcome{Outcome} Outcome -->|Won| Release[Release Frozen Funds] Outcome -->|Lost| Deduct[Deduct from Seller] Outcome -->|Pre-arbitration| Arbitration[Arbitration] Release --> SellerBalance2[Seller Balance Updated] Deduct --> SellerBalance3[Seller Balance Reduced]

The dispute management system must also implement prevention strategies to reduce chargeback rates before they occur. High chargeback rates can result in fines from card networks and even loss of the ability to process payments. Prevention strategies include proactive customer communication (sending delivery confirmation emails), clear billing descriptors, easy refund processes, and machine learning models that predict which transactions are likely to result in chargebacks and flag them for additional verification.

11. Tax Reporting (1099, VAT, Global Compliance)

Tax reporting is one of the most complex and legally sensitive aspects of running a marketplace payment platform. In the United States, platforms that facilitate payments to sellers are generally required to issue 1099-K forms to sellers who exceed certain transaction thresholds. In Europe, platforms must comply with DAC7 reporting requirements. In the UK, the Off-Payroll Working rules (IR35) may apply. And in many other jurisdictions, similar reporting obligations exist. The tax reporting system must accurately track all reportable transactions, generate the required forms, file them with the appropriate tax authorities, and provide sellers with the information they need for their own tax filings.

1099-K Reporting in the United States

Under IRS regulations, payment platforms must file Form 1099-K for each connected account that meets two thresholds: more than 200 transactions AND more than $20,000 in gross payment volume during the calendar year. The platform must collect a valid Taxpayer Identification Number (TIN) — either a Social Security Number for individuals or an Employer Identification Number for businesses — from each connected account that might exceed these thresholds. The platform must also verify that the TIN matches the legal name on file using the IRS TIN Matching service.

C#// Tax reporting service for 1099-K generation
public class TaxReportingService
{
    private readonly ITransactionRepository _transactions;
    private readonly IConnectedAccountRepository _accounts;
    private readonly ITaxFormRepository _forms;
    private readonly IIRSClient _irsClient;

    public async Task<TaxReportSummary>
        GenerateAnnual1099KReports(int taxYear)
    {
        var summary = new TaxReportSummary
        {
            TaxYear = taxYear
        };

        var activeAccounts = await _accounts
            .GetAccountsWithActivity(taxYear);

        foreach (var account in activeAccounts)
        {
            var volume = await _transactions
                .CalculateGrossVolume(account.Id, taxYear);
            var transactionCount = await _transactions
                .CountTransactions(account.Id, taxYear);

            if (volume.GrossVolume >= 20000_00 &&
                transactionCount >= 200)
            {
                var taxInfo = await _accounts
                    .GetTaxInfo(account.Id);
                if (taxInfo == null ||
                    string.IsNullOrEmpty(taxInfo.TIN))
                {
                    await RequestTINFromSeller(account);
                    summary.AccountsPendingTIN.Add(account.Id);
                    continue;
                }

                var tinValidation = await _irsClient
                    .ValidateTIN(
                        taxInfo.TIN,
                        taxInfo.LegalName,
                        taxInfo.TINType);

                if (!tinValidation.IsMatch)
                {
                    await ApplyBackupWithholding(account);
                    summary.AccountsWithMismatchedTIN
                        .Add(account.Id);
                    continue;
                }

                var form = new Form1099K
                {
                    TaxYear = taxYear,
                    PayerTIN = taxInfo.TIN,
                    PayerName = taxInfo.LegalName,
                    GrossPaymentVolume = volume.GrossVolume,
                    TransactionCount = transactionCount,
                    CardNotPresentCount =
                        volume.CardNotPresentCount,
                    MerchantCategoryCode = account.MCC,
                    FilingDeadline =
                        new DateTime(taxYear + 1, 1, 31)
                };

                await _forms.Save(form);
                summary.Forms.Add(form);
            }
        }

        if (summary.Forms.Any())
        {
            await _irsClient.FileBatch(summary.Forms);
            summary.FiledAt = DateTime.UtcNow;
        }

        return summary;
    }
}

International Tax Compliance

Beyond the United States, marketplace platforms must navigate a patchwork of international tax regulations. The European Union's DAC7 directive requires digital platforms to report seller income to tax authorities across all EU member states. The UK's HMRC has similar reporting requirements. Many countries require VAT (Value Added Tax) collection and remittance on digital services. The platform must implement a jurisdiction-aware tax engine that determines which regulations apply based on the seller's location, the buyer's location, and the nature of the goods or services being sold.

Global Tax Reporting Requirements
JurisdictionReporting FormThresholdFiling DeadlinePenalty for Non-Compliance
United States1099-K$20K + 200 transactionsJanuary 31$280 per form (2026)
European UnionDAC7 ReportEUR 30 or 30 transactionsJanuary 31Varies by member state
United KingdomHMRC ReportingGBP 1,700 or 30 transactionsJanuary 31GBP 250 per form
CanadaT4A / T5018CAD 20K (T4A) / CAD 500 (T5018)March 1$100 per day
AustraliaTPARAUD 20,000August 28AUD 1,110 per form

The tax reporting system must also handle corrections and amendments. If a seller disputes their reported volume, or if the platform discovers an error in previously filed reports, it must generate corrected forms and file them with the appropriate authorities. The system must maintain a complete audit trail of all tax reporting activities, including when forms were generated, filed, and acknowledged by tax authorities. This audit trail is essential for responding to tax authority inquiries and defending the platform's reporting practices in the event of an audit.

12. Embedded Components and Custom UIs

One of Stripe Connect's most powerful features is its embedded components — pre-built, hosted UI elements that platforms can integrate directly into their applications. These components handle complex payment flows (onboarding, payout management, dispute resolution) while maintaining PCI compliance because the sensitive payment data never touches the platform's servers. For platforms building Custom account integrations, Stripe also provides the Stripe.js library and Elements components that allow fully customized payment forms while still offloading card data handling to Stripe's hosted iframe.

Embedded Onboarding Component

The embedded onboarding component allows platforms to present a complete KYC collection flow within their own application without redirecting users to an external page. The component handles document upload, identity verification, tax information collection, and bank account setup. For Express accounts, the component communicates with Stripe's hosted onboarding service. For Custom accounts, the platform must build its own onboarding components, often using Stripe's Elements as building blocks for specific pieces of the flow.

C#// Server-side configuration for embedded onboarding
public class OnboardingController : Controller
{
    private readonly IStripeClient _stripe;
    private readonly IPlatformConfig _config;

    [HttpPost]
    public async Task<IActionResult> CreateAccountLink(
        OnboardingRequest request)
    {
        var account = await _stripe.Accounts.GetAsync(
            request.ConnectedAccountId);

        var accountLinkOptions = new AccountLinkCreateOptions
        {
            Account = account.Id,
            RefreshUrl = Url.Action("OnboardingRefresh",
                "Onboarding",
                new { accountId = account.Id }),
            ReturnUrl = Url.Action("OnboardingComplete",
                "Onboarding",
                new { accountId = account.Id }),
            Type = "account_onboarding"
        };

        var accountLink = await _stripe.AccountLinks
            .CreateAsync(accountLinkOptions);

        return Json(new
        {
            url = accountLink.Url,
            expiresAt = accountLink.ExpiresAt
        });
    }

    [HttpPost]
    public async Task<IActionResult> CreateLoginLink(
        string accountId)
    {
        var loginLink = await _stripe.Accounts
            .CreateLoginLinkAsync(accountId);

        return Json(new { url = loginLink.Url });
    }
}

Embedded Payout Management

The embedded payout component gives connected accounts visibility into their balance, payout schedule, and transaction history. For Express accounts, this is provided through the Express Dashboard, which can be embedded as an iframe within the platform's application. For Custom accounts, the platform must build its own balance and payout management UI using Stripe's API to retrieve balance information, payout history, and upcoming payout details.

graph TB subgraph Platform App PlatformUI[Platform UI] PlatformUI --> EmbedHost[Embed Host Component] end subgraph Stripe Components EmbedHost --> OnboardComp[Onboarding Component] EmbedHost --> PaymentComp[Payment Element] EmbedHost --> PayoutComp[Payout Dashboard] EmbedHost --> DisputeComp[Dispute Manager] end subgraph Stripe API OnboardComp --> AccountAPI[Account API] PaymentComp --> PaymentIntentAPI[PaymentIntent API] PayoutComp --> BalanceAPI[Balance API] DisputeComp --> DisputeAPI[Dispute API] end AccountAPI --> StripeBackend[Stripe Backend] PaymentIntentAPI --> StripeBackend BalanceAPI --> StripeBackend DisputeAPI --> StripeBackend StripeBackend --> CardNetworks[Card Networks] StripeBackend --> Banks[Banking Network]
Embedded Components vs Custom UI
AspectStripe Embedded ComponentsCustom UI with Stripe.jsFully Custom UI
PCI ComplianceStripe-managed (SAQ-A)Stripe-managed (SAQ-A-EP)Platform-managed (SAQ-D)
Branding ControlLimitedHighFull
Development EffortLowMediumHigh
MaintenanceStripe-maintainedSharedPlatform-maintained
Account TypeStandard, ExpressAll typesCustom only
Feature ParityHighHighVaries

The key architectural consideration for embedded components is the trust boundary. When using Stripe's embedded components, the platform never handles raw card data — the component communicates directly with Stripe's servers from the customer's browser, and the platform only receives tokenized references. This dramatically simplifies PCI compliance because the platform never has access to card numbers, CVVs, or other sensitive payment credentials. The platform's server only needs to communicate with Stripe's API using its server-side keys, which are never exposed to the browser.

13. Webhooks and Event System

The webhook system is the nervous system of a Stripe Connect platform. Because payment processing is inherently asynchronous — the outcome of a payment authorization, the completion of a payout, the resolution of a dispute — the platform must be notified of state changes through an event-driven mechanism. Stripe's webhook system delivers HTTP POST requests to registered endpoints for every meaningful event in the system. The platform must implement a robust webhook receiver that handles delivery guarantees, idempotency, and failure recovery.

Event Types and Ordering

Stripe generates dozens of event types for Connect platforms. The most critical include payment_intent.succeeded, payment_intent.payment_failed, account.updated, account.external_account.created, payout.paid, payout.failed, charge.dispute.created, and transfer.paid. Each event contains a timestamp and a sequential ID that allows the platform to process events in order. However, the platform must be prepared to handle out-of-order delivery because webhook delivery is best-effort and retries may arrive after newer events.

C#// Webhook receiver with idempotent processing
[ApiController]
[Route("api/webhooks")]
public class WebhookController : ControllerBase
{
    private readonly IEventProcessor _processor;
    private readonly IEventStore _eventStore;
    private readonly ISignatureVerifier _signatureVerifier;
    private readonly ILogger<WebhookController> _logger;

    [HttpPost("stripe")]
    public async Task<IActionResult> HandleStripeWebhook()
    {
        string payload;
        using (var reader = new StreamReader(Request.Body))
            payload = await reader.ReadToEndAsync();

        var signature = Request.Headers["Stripe-Signature"]
            .FirstOrDefault();
        if (!_signatureVerifier.Verify(
            payload, signature, GetWebhookSecret()))
            return Unauthorized();

        var stripeEvent = JsonConvert
            .DeserializeObject<StripeEvent>(payload);

        if (await _eventStore.Exists(stripeEvent.Id))
        {
            _logger.LogInformation(
                "Duplicate event {EventId} skipped",
                stripeEvent.Id);
            return Ok();
        }

        try
        {
            switch (stripeEvent.Type)
            {
                case "payment_intent.succeeded":
                    await HandlePaymentSucceeded(
                        stripeEvent.Data.Object
                            as PaymentIntent);
                    break;
                case "payment_intent.payment_failed":
                    await HandlePaymentFailed(
                        stripeEvent.Data.Object
                            as PaymentIntent);
                    break;
                case "account.updated":
                    await HandleAccountUpdated(
                        stripeEvent.Data.Object
                            as ConnectedAccount);
                    break;
                case "payout.paid":
                    await HandlePayoutPaid(
                        stripeEvent.Data.Object as Payout);
                    break;
                case "payout.failed":
                    await HandlePayoutFailed(
                        stripeEvent.Data.Object as Payout);
                    break;
                case "charge.dispute.created":
                    await HandleDisputeCreated(
                        stripeEvent.Data.Object as Dispute);
                    break;
                case "transfer.paid":
                    await HandleTransferPaid(
                        stripeEvent.Data.Object as Transfer);
                    break;
                default:
                    _logger.LogDebug(
                        "Unhandled event: {EventType}",
                        stripeEvent.Type);
                    break;
            }

            await _eventStore.Save(new ProcessedEvent
            {
                EventId = stripeEvent.Id,
                EventType = stripeEvent.Type,
                ProcessedAt = DateTime.UtcNow
            });
            return Ok();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Error processing webhook {EventId}",
                stripeEvent.Id);
            return StatusCode(500);
        }
    }
}

Webhook Reliability Patterns

Stripe guarantees at-least-once delivery for webhooks, meaning the platform must implement idempotent processing to handle duplicate deliveries. The standard pattern is to record each processed event ID in a database and check for duplicates before processing. Events must be processed within the timeout window — if the platform takes too long to respond (more than 10-20 seconds), Stripe will consider the delivery failed and retry. For long-running operations, the webhook handler should record the event and return a 200 response immediately, then process the event asynchronously via a background queue.

Critical Webhook Events for Connect Platforms
Event TypeTriggerPlatform ActionPriority
payment_intent.succeededPayment captured successfullyUpdate order status, notify sellerCritical
payment_intent.payment_failedPayment declined or erroredNotify customer, retry or abandonCritical
account.updatedConnected account status changedUpdate seller capabilities in platformHigh
payout.paidPayout deposited in bank accountUpdate payout status, notify sellerHigh
payout.failedPayout rejected by bankRecredit balance, notify seller, flagCritical
charge.dispute.createdChargeback initiatedFreeze funds, request evidence, notifyCritical
transfer.paidTransfer completedUpdate balance, log settlementMedium
graph TB Stripe[Stripe Platform] -->|HTTP POST| WebhookReceiver[Webhook Receiver] WebhookReceiver --> SignatureCheck{Signature Valid?} SignatureCheck -->|No| Reject[Return 401] SignatureCheck -->|Yes| IdempotencyCheck{Already Processed?} IdempotencyCheck -->|Yes| Skip[Return 200 Skip] IdempotencyCheck -->|No| Queue[Event Queue] Queue --> PaymentWorker[Payment Worker] Queue --> AccountWorker[Account Worker] Queue --> PayoutWorker[Payout Worker] Queue --> DisputeWorker[Dispute Worker] PaymentWorker --> PlatformDB[(Platform Database)] AccountWorker --> PlatformDB PayoutWorker --> PlatformDB DisputeWorker --> PlatformDB PaymentWorker --> NotifySeller[Notify Seller] AccountWorker --> UpdateCapabilities[Update Capabilities] PayoutWorker --> UpdateBalance[Update Balance] DisputeWorker --> StartDispute[Start Dispute Process] PlatformDB --> EventStore[(Event Store)] EventStore -->|Record| IdempotencyCheck

The webhook system should also implement monitoring and alerting. Failed webhook deliveries, events that take too long to process, and events that trigger errors should all be tracked and alerted on. A dashboard showing webhook delivery health, processing latency, and error rates is essential for maintaining the reliability of the platform's synchronization with Stripe.

14. API Design and Idempotency

The API design of a Stripe Connect-like platform must balance developer experience, safety, and extensibility. Stripe's API is widely regarded as one of the best-designed APIs in the industry, and for good reason: it follows consistent conventions, provides predictable behavior, handles errors gracefully, and supports idempotent operations that prevent duplicate charges. Understanding these design principles is essential for building a platform that developers will trust with their payment infrastructure.

RESTful Resource Design

The API follows REST conventions with resources like /accounts, /payment_intents, /transfers, /payouts, and /disputes. Each resource supports standard CRUD operations: POST to create, GET to retrieve, PATCH to update, and DELETE to cancel (where applicable). List endpoints support pagination using cursor-based pagination with starting_after and ending_before parameters. All timestamps are returned as Unix timestamps in seconds, and all monetary amounts are returned in the smallest currency unit (cents for USD, yen for JPY) to avoid floating-point precision issues.

C#// Idempotent API middleware for safe retries
public class IdempotencyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IIdempotencyStore _store;

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Method != "POST")
        {
            await _next(context);
            return;
        }

        if (!context.Request.Headers
            .TryGetValue("Idempotency-Key",
                out var idempotencyKey))
        {
            await _next(context);
            return;
        }

        var existingResult = await _store.Get(idempotencyKey);
        if (existingResult != null)
        {
            context.Response.StatusCode =
                existingResult.StatusCode;
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(
                existingResult.ResponseBody);
            return;
        }

        var originalBodyStream = context.Response.Body;
        using var responseBody = new MemoryStream();
        context.Response.Body = responseBody;

        await _next(context);

        responseBody.Seek(0, SeekOrigin.Begin);
        var responseText =
            await new StreamReader(responseBody)
                .ReadToEndAsync();

        await _store.Save(new IdempotencyRecord
        {
            Key = idempotencyKey,
            StatusCode = context.Response.StatusCode,
            ResponseBody = responseText,
            CreatedAt = DateTime.UtcNow,
            ExpiresAt = DateTime.UtcNow.AddHours(24)
        });

        responseBody.Seek(0, SeekOrigin.Begin);
        await responseBody.CopyToAsync(originalBodyStream);
    }
}

Error Handling and Status Codes

The API must return meaningful error responses that help developers understand what went wrong and how to fix it. Every error response follows a consistent structure: an error object containing a type (e.g., card_error, invalid_request_error, api_error), a human-readable message, a machine-readable code (e.g., card_declined, insufficient_funds), and a param indicating which request parameter caused the error.

API Error Types and Handling
Error TypeHTTP StatusExampleDeveloper Action
invalid_request_error400Missing required parameterFix the request parameters
authentication_error401Invalid API keyCheck API credentials
card_error402Card declinedRetry with different payment method
idempotency_error409Key reused with different paramsUse a new idempotency key
rate_limit_error429Too many requestsImplement exponential backoff
api_error500Internal processing errorRetry with backoff
invalid_expand400Cannot expand nested resourceCheck expand parameter

Versioning and Backwards Compatibility

API versioning is critical for a payment platform because breaking changes can cause outages for dependent platforms. Stripe handles this by pinning API versions to dates — when a platform creates an account, it is pinned to the API version that was current at that time. New API versions are released periodically with backward-compatible additions. Breaking changes are introduced with long deprecation periods, and the version header is used to ensure that each platform receives the behavior it expects.

graph LR Client[Client] -->|API Request| Gateway[API Gateway] Gateway --> Auth{Authenticated?} Auth -->|No| Error401[401 Unauthorized] Auth -->|Yes| RateLimit{Rate Limited?} RateLimit -->|Yes| Error429[429 Too Many Requests] RateLimit -->|No| Idempotency{Idempotent Key?} Idempotency -->|Existing| Cached[Return Cached Response] Idempotency -->|New| Route[Route to Service] Idempotency -->|None| Route Route --> Validation{Valid Request?} Validation -->|No| Error400[400 Bad Request] Validation -->|Yes| Service[Microservice] Service --> Response[200 Success] Service --> BizError[402 Payment Error]

The API must also support expand parameters that allow developers to include related resources in a single API call, reducing the number of round trips needed. For example, a GET /payment_intents/pi_123?expand[]=customer&expand[]=transfer request would return the payment intent with the full customer and transfer objects embedded rather than just their IDs.

15. Connect for Platforms (Marketplaces, SaaS)

Stripe Connect is designed to serve two primary platform archetypes: marketplaces and SaaS platforms. While both use the same underlying Connect infrastructure, their requirements, architecture, and usage patterns differ significantly. Marketplaces typically have many sellers, many buyers, and the platform facilitates transactions between them. SaaS platforms have a subscription-based model where the platform provides software services and may facilitate payments on behalf of its customers.

Marketplace Platform Architecture

A marketplace platform built on Connect must handle several unique requirements: managing thousands or millions of connected seller accounts, supporting diverse product categories with different fee structures, handling complex search and discovery, providing buyer protection through escrow, and managing the lifecycle of multi-party transactions from listing through delivery and payment. The platform's architecture must support the full commerce lifecycle while leveraging Connect for the payment layer.

The marketplace platform typically implements a listing service where sellers create product or service listings, a search and discovery service that helps buyers find what they need, an order management service that coordinates the transaction between buyer and seller, a fulfillment service that tracks delivery status, and a payment service that orchestrates the Connect payment flow. Each of these services communicates with the others through an event-driven architecture, with the payment service serving as the final step that triggers fund movement.

SaaS Platform Architecture

A SaaS platform using Connect typically needs to process payments on behalf of its merchant users — for example, a point-of-sale system that processes card payments for retail stores. The SaaS platform creates connected accounts for each of its merchant users, processes payments through those accounts, and collects its subscription fee as a percentage of each transaction. This model is sometimes called payment facilitation or payfac and requires careful attention to regulatory requirements, as the platform is effectively acting as a payment facilitator for its merchants.

Marketplace vs SaaS Platform Requirements
RequirementMarketplaceSaaS Platform
Connected AccountsMillions (sellers, service providers)Hundreds to thousands (merchants)
Account TypeExpress or CustomCustom (white-label)
Payment FlowSplit payments, escrowDirect charges, application fees
Payout ModelPer-transaction or scheduledBatch settlement to merchant
BrandingMarketplace brandSaaS brand (white-label)
OnboardingSelf-service, at scaleAssisted, enterprise-grade
Revenue ModelTransaction feesSubscription + transaction fees
RegulatoryMarketplace regulationsPayment facilitator regulations
graph TB subgraph Marketplace Model Buyers1[Buyers] -->|Browse and Purchase| Marketplace[Marketplace Platform] Marketplace -->|Route Payment| Sellers1[Sellers] Marketplace -->|Platform Fee| Platform1[Platform Revenue] Marketplace -->|Escrow| Escrow[Escrow Account] end subgraph SaaS Payfac Model Customers2[Customers] -->|Pay at POS| SaaSApp[SaaS Application] SaaSApp -->|Process Payment| Merchant[Merchant Account] SaaSApp -->|Collect Subscription| Platform2[SaaS Revenue] SaaSApp -->|Settle| MerchantBank[Merchant Bank] end

Both marketplace and SaaS platforms must carefully design their onboarding flows to minimize friction while maintaining compliance. The onboarding experience is often the first interaction a seller or merchant has with the platform's payment system, and a confusing or overly burdensome onboarding flow can cause significant drop-off. The platform should implement analytics to track onboarding funnel metrics — how many accounts start onboarding, how many complete each step, and where users abandon the flow — to identify and address friction points.

The platform must also implement robust monitoring and alerting for all payment-related operations. Dashboards should display real-time metrics including payment success rates, average processing latency, payout completion rates, dispute rates, and revenue by connected account. Alerts should be configured for anomalies such as sudden drops in payment success rates, spikes in dispute rates, or payout failures exceeding normal thresholds.

16. Performance and Reliability at Scale

Operating a payment platform at Stripe Connect's scale requires obsessive attention to performance and reliability. Every millisecond of latency in payment authorization directly impacts conversion rates — studies show that each additional 100ms of page load time reduces conversion by 1-2%. At Stripe's scale, even small latency increases can translate to billions of dollars in lost revenue. The platform must be designed for five nines of availability (99.999%), meaning less than 5.26 minutes of downtime per year.

Performance Optimization Strategies

The performance optimization strategy begins at the edge. The API gateway must be deployed in multiple regions with intelligent routing that directs requests to the nearest datacenter. DNS-based load balancing directs users to the optimal endpoint based on geographic proximity. Connection pooling and HTTP/2 multiplexing reduce the overhead of establishing connections to backend services. The gateway must also implement request coalescing — when multiple identical requests arrive simultaneously (common during webhooks), they should be coalesced into a single backend request.

The payment processing pipeline must be optimized for the common case while handling edge cases gracefully. The happy path — a successful card payment with no fraud flags, no currency conversion, and no split payment complexity — should complete in under 200ms from the perspective of the payment gateway. This requires pre-computing frequently accessed data (exchange rates, risk thresholds, account capabilities), caching aggressively (account status, fee structures), and using efficient data structures for real-time calculations.

C#// High-performance payment processing pipeline
public class OptimizedPaymentPipeline
{
    private readonly ICacheService _cache;
    private readonly IAccountCapabilityCache _capabilityCache;
    private readonly IFxRateCache _fxCache;
    private readonly ICircuitBreaker _circuitBreaker;
    private readonly ICardNetwork _cardNetwork;

    public async Task<PaymentAuthorizationResult> Authorize(
        AuthorizationRequest request)
    {
        // Hot path: check cached account status
        var capabilities = await _capabilityCache
            .GetCapabilities(request.ConnectedAccountId);
        if (!capabilities.CanReceivePayments)
            return PaymentAuthorizationResult
                .Declined("Account restricted");

        // Check cached balance for split payments
        if (request.HasSplitPayment)
        {
            var availableBalance = await _cache
                .GetAsync<long>(
                    $"balance:{request.ConnectedAccountId}");
            if (availableBalance < request.TransferAmount)
                return PaymentAuthorizationResult
                    .Declined("Insufficient balance");
        }

        // Build authorization request with pre-computed values
        var authRequest = new NetworkAuthorizationRequest
        {
            Amount = request.Amount,
            Currency = request.Currency,
            CardToken = request.CardToken,
            MerchantId = GetMerchantId(
                request.ConnectedAccountId),
            Metadata = request.Metadata
        };

        // Submit to card network with circuit breaker
        var networkResponse = await _circuitBreaker
            .ExecuteAsync(
                () => _cardNetwork.Authorize(authRequest),
                new CircuitBreakerOptions
                {
                    FailureThreshold = 5,
                    RecoveryTimeout =
                        TimeSpan.FromSeconds(30),
                    Timeout = TimeSpan.FromSeconds(3)
                });

        var result = ProcessNetworkResponse(networkResponse);

        // Update balance asynchronously
        if (result.Approved && request.HasSplitPayment)
        {
            _ = Task.Run(async () =>
            {
                await _cache.DecrementAsync(
                    $"balance:{request.ConnectedAccountId}",
                    request.TransferAmount);
            });
        }

        return result;
    }
}

Reliability Engineering

Reliability in a payment platform requires defense in depth. The system must tolerate failures at every layer: individual servers can fail, network connections can be interrupted, databases can become unavailable, and external services (card networks, banking partners) can experience outages. The architecture must be designed to degrade gracefully — for example, if the fraud scoring service is unavailable, the system might route payments to a simplified rules-based risk engine rather than failing the entire payment.

Reliability Architecture Components
ComponentStrategyFailoverRecovery Time
API GatewayMulti-region active-activeAutomatic DNS failover< 30 seconds
Payment DatabasePrimary-replica with auto-failoverAutomatic promotion< 10 seconds
Card Network ConnectionMulti-path with circuit breakerSecondary network route< 5 seconds
Fraud EngineGraceful degradationFallback to rules-onlyImmediate
Balance CacheRedis cluster with replicasRead from replica< 1 second
Webhook DeliveryAsync queue with dead letterRetry with backoffMinutes to hours
Payout ProcessingBatch with checkpointingResume from last checkpointMinutes
graph TB subgraph Region US-East USGW[US Gateway] --> USService[US Payment Service] USService --> USDB[(US Database Primary)] end subgraph Region EU-West EUGW[EU Gateway] --> EUService[EU Payment Service] EUService --> EUDb[(EU Database Replica)] end subgraph Shared Infrastructure GlobalCache[(Global Cache Redis)] GlobalQueue[Global Event Queue Kafka] GlobalDB[(Global Ledger Cross-Region)] end USGW --> GlobalCache EUGW --> GlobalCache USService --> GlobalQueue EUService --> GlobalQueue USDB --> GlobalDB EUDb --> GlobalDB GlobalDB -->|Async Replication| USDB GlobalDB -->|Async Replication| EUDb

Chaos engineering must be a regular practice for a payment platform. Teams should regularly inject failures — killing random service instances, introducing network latency between services, corrupting cache entries, and simulating card network outages — to verify that the system degrades gracefully and recovers correctly. These chaos experiments should be run in a staging environment first, then carefully in production with blast radius controls. The results of chaos experiments feed directly into reliability improvements, closing the loop between testing and engineering.

17. Interview Q&A

The following questions are commonly asked in senior and staff-level system design interviews when the topic is payment platform design. Each question is designed to probe a specific aspect of the system, and the answers provided demonstrate the depth of understanding expected at the senior+ level.

Q1: How would you handle a situation where a payment succeeds at the card network but the transfer to the connected account fails?

Answer: This is a classic distributed transaction problem. The payment authorization and the transfer to the connected account are two separate operations that must both succeed for the transaction to be complete. If the authorization succeeds but the transfer fails, the funds are in limbo — they've been captured from the customer's card but haven't reached the seller. The system must implement a reconciliation process that identifies these orphaned funds and handles them appropriately. The standard approach is to credit the funds to a suspense account, trigger an alert for the operations team, and attempt the transfer again. If the transfer continues to fail (e.g., because the connected account is restricted), the funds must eventually be refunded to the customer. The key principle is that money should never be created or destroyed — every cent must be accounted for in the ledger at all times. The suspense account acts as a holding area where funds wait until their final destination is determined, and the reconciliation engine runs periodically to identify and resolve orphaned funds.

Q2: How would you design the balance ledger to support millions of concurrent transactions while maintaining strict consistency?

Answer: The balance ledger must be designed as an event-sourced system where every balance change is recorded as an immutable event. The current balance is derived from the event stream, and the event stream is the source of truth. To support concurrency, the ledger uses optimistic concurrency control — each balance update includes the version number of the balance it's modifying. If two transactions try to modify the same balance simultaneously, one will succeed and the other will be retried with the updated version. For extremely high-throughput accounts, the ledger can implement sharding where different aspects of the balance (available, pending, reserved) are stored on separate shards. The critical invariant is that the sum of all debits must equal the sum of all credits across the entire ledger at all times — this can be verified periodically through a ledger reconciliation process that runs continuously in the background.

Q3: How would you prevent double-spending when a customer has a limited balance and initiates two concurrent payments?

Answer: Double-spending prevention requires atomic operations at the database level. When processing a payment, the system must atomically check that the customer has sufficient available balance and deduct the payment amount in a single database transaction with appropriate isolation levels. In PostgreSQL, this can be achieved using SELECT ... FOR UPDATE which locks the balance row for the duration of the transaction. Alternatively, the system can use a compare-and-swap (CAS) operation where the balance update includes the expected current value, and the update fails if the balance has changed since the check. The API layer must also implement idempotency to prevent duplicate payments from being processed if the customer clicks the pay button twice. The combination of database-level locking and API-level idempotency provides defense in depth against double-spending.

Q4: How would you handle a platform that needs to split a single payment among 50 different recipients?

Answer: A split among 50 recipients introduces several challenges. First, the transfer API has practical limits on the number of transfers per payment — typically around 10-20 for destination charges. For larger splits, the platform should use separate charges and transfers: create the payment as a charge to the platform's account, then create individual transfers to each recipient. This approach requires the platform to maintain a sufficient balance to cover all transfers before they're created. The system must also handle partial failures — if 48 of 50 transfers succeed but 2 fail, the system must decide whether to roll back the successful transfers or to handle the failures independently. The recommended approach is to process transfers in a saga pattern where each transfer is independent and failures are handled individually, with automatic retries and manual escalation for persistent failures.

Q5: How would you design the webhook system to guarantee at-least-once delivery while preventing duplicate processing?

Answer: The webhook system must guarantee at-least-once delivery through a multi-layered approach. First, events are written to a durable event log (e.g., Kafka or an event-sourced database) before any delivery attempt is made. The delivery service reads from this log and attempts to POST the event to the registered endpoint. If the endpoint returns a non-2xx status code or doesn't respond within the timeout window (typically 10-20 seconds), the delivery is retried with exponential backoff. The platform receiving the webhook must implement idempotency by recording the event ID in a database before processing. When a duplicate event arrives, the platform checks the database and returns a 200 response without reprocessing. For events that trigger side effects (like sending emails or initiating transfers), the platform must ensure that the side effect is also idempotent — either by checking if the side effect has already been performed, or by designing the side effect to be naturally idempotent.

Q6: How would you handle a dispute where the seller claims they delivered the product but the customer claims they never received it?

Answer: This is one of the most common dispute types (reason code: product_not_received) and requires the platform to act as a neutral arbiter. The platform must collect evidence from both parties: the seller provides proof of delivery (tracking number, delivery confirmation, signature confirmation, GPS proof of delivery if available), and the customer provides evidence of non-receipt (correspondence with the carrier, photos of empty doorstep, witness statements). The platform should also independently verify the tracking information through the carrier's API to confirm the delivery status and location. If the evidence is inconclusive, the platform must make a judgment call based on its buyer protection policies and the credibility of each party. The outcome should be communicated clearly to both parties with a detailed explanation of the evidence considered.

Q7: How would you implement currency conversion for a payment where the buyer pays in EUR and the seller expects USD, and the FX rate changes during the transaction?

Answer: FX rate management during a transaction requires a clear policy on rate locking. The standard approach is to lock the FX rate at the moment the customer confirms the payment, not at the moment they initiate it. The flow is: when the customer views the checkout page, the platform displays an estimated conversion rate with a disclaimer; when the customer clicks pay, the platform requests a guaranteed rate quote from its FX provider with a validity window (e.g., 60 seconds); the guaranteed rate is locked into the PaymentIntent and cannot change; if the customer takes longer than 60 seconds to complete 3DS verification, the payment is declined and the customer must re-initiate with a fresh rate quote. This approach protects both the customer and the platform from adverse FX movements.

Q8: How would you design the system to handle a sudden 10x spike in payment volume, such as during a flash sale?

Answer: A 10x spike requires preparation across all layers of the stack. The API gateway must auto-scale based on request rate and have pre-provisioned capacity to handle sudden bursts. The payment processing pipeline should use a queue-based architecture where incoming payment requests are placed in a durable queue and processed by a fleet of workers that can scale horizontally. The key insight is that customers at a flash sale expect some queuing — a 2-5 second wait is acceptable if the customer sees their position in a queue. The system must implement rate limiting that distributes capacity fairly rather than rejecting excess requests outright. Database connections must be pooled and connection limits must be increased in anticipation of the spike. The balance cache must be warmed before the event to avoid cold-start cache misses.

Q9: How would you audit the financial integrity of the balance ledger to ensure no money has been created or destroyed?

Answer: Financial integrity verification requires a reconciliation process that runs continuously in the background. The process verifies three invariants: for every account, the current balance equals the sum of all credited amounts minus the sum of all debited amounts (account-level reconciliation); across all accounts, the sum of all credits equals the sum of all debits (system-level reconciliation); the ledger balance matches the actual funds held at banking partners (bank-level reconciliation). The reconciliation process runs as a background job that processes transactions in batches, maintaining running totals and comparing them to stored balance snapshots. Any discrepancy triggers an immediate alert and automatically pauses the affected account's operations until the discrepancy is investigated and resolved.

Q10: How would you design the Connect API to be backward-compatible when introducing a major change like switching from amount-based splits to percentage-based splits?

Answer: Backward compatibility in a payment API is critical because breaking changes can cause outages for platforms that depend on the API. The recommended approach is to introduce the new functionality alongside the old, rather than replacing it. Specifically: create a new API version that supports both amount-based and percentage-based splits in the same request, with the split type determined by which fields are provided; document the new version clearly and provide a migration guide; give platforms at least 6 months to migrate; implement a webhook event that notifies platforms when they are approaching the deprecation deadline; provide a simulation mode that allows platforms to test their integration against the new version without affecting live data; and maintain a compatibility layer that translates between old and new formats during the transition period.

Ayodhyya - System Design Blog Series | Stripe Connect Marketplace Payment Platform - Senior+ Guide

Article #193 | Published April 29, 2024