How to Design Stripe Connect — Marketplace Payment Platform
A Senior+ Guide to Building a Multi-Sided Payment Infrastructure for Marketplaces, SaaS, and Platforms
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.
| Feature | Standard | Express | Custom |
|---|---|---|---|
| Onboarding | Stripe Dashboard | Stripe Express hosted page | Platform-built UI |
| Account Management | Stripe Dashboard | Express Dashboard | Platform-built UI |
| KYC/AML Responsibility | Stripe | Stripe | Platform + Stripe |
| Branding | Stripe branding | Minimal Stripe branding | Full platform branding |
| Setup Complexity | Low | Medium | High |
| Platform Responsibility | Minimal | Moderate | Maximum |
| Best For | Simple marketplaces | Gig platforms | Enterprise SaaS |
| Payout Management | Stripe-managed | Stripe-managed | Platform-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.
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.
| Service | Responsibility | Latency Target | Availability |
|---|---|---|---|
| API Gateway | Authentication, rate limiting, routing | < 10ms | 99.999% |
| Account Service | Onboarding, KYC orchestration, capabilities | < 200ms | 99.99% |
| Payment Service | Intent creation, confirmation, capture | < 1500ms | 99.999% |
| Transfer Service | Fund splitting, destination routing | < 500ms | 99.99% |
| Payout Service | Bank file generation, payout scheduling | Batch (hours) | 99.99% |
| Fraud Engine | Real-time risk scoring, rules evaluation | < 50ms | 99.99% |
| Balance Ledger | Double-entry bookkeeping, balance queries | < 20ms | 99.9999% |
| Webhook Service | Event dispatch, retry delivery, monitoring | < 500ms | 99.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.
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.
| Data Field | Standard | Express | Custom |
|---|---|---|---|
| Legal Name | Collected by Stripe | Collected by Stripe | Collected by Platform |
| Date of Birth | Collected by Stripe | Collected by Stripe | Collected by Platform |
| Address | Collected by Stripe | Collected by Stripe | Collected by Platform |
| Tax ID | Collected by Stripe | Collected by Stripe | Collected by Platform |
| Bank Account | Managed by seller | Collected by Stripe | Collected by Platform |
| ID Document | Collected by Stripe | Collected by Stripe | Collected by Platform |
| Business Registration | N/A | If applicable | Collected by Platform |
| Beneficial Ownership | N/A | If applicable | Collected 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.
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.
| Aspect | Destination Charge | Direct Charge | Separate Charges and Transfers |
|---|---|---|---|
| Merchant of Record | Platform | Connected Account | Platform (charges) / Account (transfers) |
| Refund Handling | From platform balance | From connected account balance | From platform balance |
| Chargeback Liability | Platform | Connected Account | Platform |
| Split Timing | At payment time | At payment time | After payment, separate transfer |
| Complexity | Low | Low | High |
| Use Case | Most marketplaces | Simple platforms | Complex 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.
| Fee Model | Calculation | Example | Best For |
|---|---|---|---|
| Flat Percentage | Total x Rate | $100 x 15% = $15 | Simple marketplaces |
| Percentage + Fixed | (Total x Rate) + Fixed | ($100 x 2.9%) + $0.30 = $3.20 | SaaS platforms |
| Tiered | Rate changes at thresholds | First $10K: 15%, Next: 12% | Growing marketplaces |
| Flat Fee | Fixed amount per transaction | $2.00 per transaction | High-value goods |
| Dynamic | Based on category, risk, geo | Varies per transaction | Enterprise 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.
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.
| Region | Primary Method | Settlement Time | Instant Available | Fee |
|---|---|---|---|---|
| United States | ACH | 2-3 business days | Yes (same-day ACH) | $0.25 per payout |
| European Union | SEPA | 1-2 business days | Yes (SEPA Instant) | EUR 0.25 per payout |
| United Kingdom | BACS | 3 business days | Yes (Faster Payments) | GBP 0.20 per payout |
| Australia | BECS | 2-3 business days | Yes (NPP) | AUD 0.30 per payout |
| Canada | EFT | 2-3 business days | Yes (Interac e-Transfer) | CAD 0.25 per payout |
| Japan | Bank Transfer | 1-2 business days | No | JPY 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.
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 | Code | Minor Units | Payments | Payouts | Settlement |
|---|---|---|---|---|---|
| US Dollar | USD | 2 | Yes | Yes | Yes |
| Euro | EUR | 2 | Yes | Yes | Yes |
| British Pound | GBP | 2 | Yes | Yes | Yes |
| Japanese Yen | JPY | 0 | Yes | Yes | Yes |
| Australian Dollar | AUD | 2 | Yes | Yes | Yes |
| Indian Rupee | INR | 2 | Yes | Limited | No |
| Brazilian Real | BRL | 2 | Yes | Limited | No |
| Nigerian Naira | NGN | 2 | Yes | No | No |
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.
| Signal Category | Specific Signals | Weight | Latency Impact |
|---|---|---|---|
| Device Intelligence | Device fingerprint, IP reputation, geolocation | High | ~5ms |
| Behavioral Analytics | Purchase history, session behavior, typing patterns | High | ~8ms |
| Velocity Checks | Transaction frequency, amount velocity | Medium | ~3ms |
| Network Analysis | IP graph, device graph, email graph | Medium | ~10ms |
| Card Verification | CVV check, AVS check, BIN analysis | High | ~15ms |
| 3D Secure | 3DS2 challenge result, issuer risk | High | ~50ms if challenged |
| Cross-Platform | Fraud database lookups, consortium data | Medium | ~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.
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)
};
}
}
| Reason Code | Description | Required Evidence | Success Rate |
|---|---|---|---|
| fraudulent | Unauthorized transaction | AVS match, CVV match, IP match, device history | 15-25% |
| product_not_received | Customer claims non-delivery | Tracking number, delivery confirmation, signature | 40-60% |
| product_defective | Product not as described | Product description, return policy, refund offered | 30-45% |
| duplicate | Customer charged twice | Original transaction proof, unique order IDs | 60-80% |
| subscription_canceled | Recurring charge after cancellation | Terms of service, cancellation policy, comms logs | 35-50% |
| credit_not_processed | Customer claims refund was not issued | Refund receipt, communication records | 50-70% |
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.
| Jurisdiction | Reporting Form | Threshold | Filing Deadline | Penalty for Non-Compliance |
|---|---|---|---|---|
| United States | 1099-K | $20K + 200 transactions | January 31 | $280 per form (2026) |
| European Union | DAC7 Report | EUR 30 or 30 transactions | January 31 | Varies by member state |
| United Kingdom | HMRC Reporting | GBP 1,700 or 30 transactions | January 31 | GBP 250 per form |
| Canada | T4A / T5018 | CAD 20K (T4A) / CAD 500 (T5018) | March 1 | $100 per day |
| Australia | TPAR | AUD 20,000 | August 28 | AUD 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.
| Aspect | Stripe Embedded Components | Custom UI with Stripe.js | Fully Custom UI |
|---|---|---|---|
| PCI Compliance | Stripe-managed (SAQ-A) | Stripe-managed (SAQ-A-EP) | Platform-managed (SAQ-D) |
| Branding Control | Limited | High | Full |
| Development Effort | Low | Medium | High |
| Maintenance | Stripe-maintained | Shared | Platform-maintained |
| Account Type | Standard, Express | All types | Custom only |
| Feature Parity | High | High | Varies |
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.
| Event Type | Trigger | Platform Action | Priority |
|---|---|---|---|
| payment_intent.succeeded | Payment captured successfully | Update order status, notify seller | Critical |
| payment_intent.payment_failed | Payment declined or errored | Notify customer, retry or abandon | Critical |
| account.updated | Connected account status changed | Update seller capabilities in platform | High |
| payout.paid | Payout deposited in bank account | Update payout status, notify seller | High |
| payout.failed | Payout rejected by bank | Recredit balance, notify seller, flag | Critical |
| charge.dispute.created | Chargeback initiated | Freeze funds, request evidence, notify | Critical |
| transfer.paid | Transfer completed | Update balance, log settlement | Medium |
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.
| Error Type | HTTP Status | Example | Developer Action |
|---|---|---|---|
| invalid_request_error | 400 | Missing required parameter | Fix the request parameters |
| authentication_error | 401 | Invalid API key | Check API credentials |
| card_error | 402 | Card declined | Retry with different payment method |
| idempotency_error | 409 | Key reused with different params | Use a new idempotency key |
| rate_limit_error | 429 | Too many requests | Implement exponential backoff |
| api_error | 500 | Internal processing error | Retry with backoff |
| invalid_expand | 400 | Cannot expand nested resource | Check 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.
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.
| Requirement | Marketplace | SaaS Platform |
|---|---|---|
| Connected Accounts | Millions (sellers, service providers) | Hundreds to thousands (merchants) |
| Account Type | Express or Custom | Custom (white-label) |
| Payment Flow | Split payments, escrow | Direct charges, application fees |
| Payout Model | Per-transaction or scheduled | Batch settlement to merchant |
| Branding | Marketplace brand | SaaS brand (white-label) |
| Onboarding | Self-service, at scale | Assisted, enterprise-grade |
| Revenue Model | Transaction fees | Subscription + transaction fees |
| Regulatory | Marketplace regulations | Payment facilitator regulations |
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.
| Component | Strategy | Failover | Recovery Time |
|---|---|---|---|
| API Gateway | Multi-region active-active | Automatic DNS failover | < 30 seconds |
| Payment Database | Primary-replica with auto-failover | Automatic promotion | < 10 seconds |
| Card Network Connection | Multi-path with circuit breaker | Secondary network route | < 5 seconds |
| Fraud Engine | Graceful degradation | Fallback to rules-only | Immediate |
| Balance Cache | Redis cluster with replicas | Read from replica | < 1 second |
| Webhook Delivery | Async queue with dead letter | Retry with backoff | Minutes to hours |
| Payout Processing | Batch with checkpointing | Resume from last checkpoint | Minutes |
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.