Split Payments System Design: Charging One Purchase Across Multiple Payment Methods

Split Payments System Design Charging One Purchase Across Multiple Payment Methods

Split Payments — Charging One Purchase Across Multiple Payment Methods

A complete system-design guide to a checkout that can divide a single purchase amount across a gift card, store credit, and a credit card in one atomic transaction — with correctness guaranteed even at a million requests per minute.

01

Introduction & History

Split payment support sounds like a small checkout feature, but it is actually a distributed transaction problem wearing a friendly UI. A customer buying a $120 pair of shoes might want to apply a $30 gift card, $20 of store credit, and charge the remaining $70 to their credit card — all in one purchase, with one receipt, and with the guarantee that either the whole purchase succeeds or none of it does. Getting this wrong either overcharges a customer, leaves inventory reserved for a purchase that never fully completed, or — worst of all — captures money on one payment method while silently failing on another.

The idea of paying with more than one instrument at a single point of sale is not new. Physical retail cashiers have manually combined a gift card and a card swipe for decades, with the cashier acting as the “orchestrator,” manually re-running the second charge if the first partially covered the total. What changed in e-commerce is that this manual, human-mediated process had to become a fully automated, auditable, and instantaneous digital workflow, coordinated across payment processors that were never designed to know about each other, all while the checkout page needs to feel like a single, seamless “Pay Now” button to the customer.

Split payments became a mainstream e-commerce feature as gift cards, store credit, loyalty points, and buy-now-pay-later (BNPL) products proliferated alongside traditional cards. Large retailers (Amazon, Walmart, Target) and payment platforms (Stripe, Adyen, Braintree) all publish guidance today on “split tender” or “mixed tender” transactions, because customers increasingly hold value across several instruments and expect to combine them freely rather than being forced to pick just one.

🍴
Everyday analogy

Think of paying a restaurant bill by splitting it between two friends’ cards. The waiter doesn’t hand you the check twice — they run each card for its share, and only once both go through do they tear off the receipt and consider the table’s bill “paid.” If one card gets declined halfway through, the waiter doesn’t just shrug and let you leave; they either ask for a different card for that portion or void what was already charged. A split payment system automates exactly this behavior — for potentially two, three, or more “friends” (payment methods), correctly, every single time, for millions of transactions a day.

From Wire-and-Cash Registers to Programmable Checkout

Early electronic point-of-sale systems in the 1980s and 1990s could barely process one card at a time reliably; splitting a bill across tenders was a manual, error-prone cashier workflow, often requiring a supervisor override just to run a second charge against the same sale. The rise of programmable payment gateways and orchestration platforms in the 2010s — building on card network APIs, gift card processor APIs, and internal wallet ledgers — turned “split tender” from a physical-register trick into a well-defined backend workflow that any online checkout could offer. Today it is considered a baseline expectation for serious e-commerce platforms rather than a niche feature, and the underlying engineering challenge — coordinating money movement across systems that were never designed to know about each other — has become a well-studied pattern in payments architecture, closely related to the broader family of distributed saga problems that also show up in travel booking, ride-hailing fare splitting, and marketplace payouts.

Why This System Matters for an Architect

Split payments force an architect to reason precisely about several classically hard distributed systems problems at once:

  • Atomicity across independent systems — a gift card processor and a card network are two completely separate systems with no shared transaction context; “all or nothing” must be built by the application, not inherited for free from a database.
  • Partial failure handling — what happens when method 1 of 3 succeeds and method 2 fails must be a first-class, well-tested code path, not an afterthought.
  • Idempotency and exactly-once charging — retries must never double-charge any one of the multiple methods involved.
  • High-scale checkout traffic — checkout is often the single highest-value, highest-traffic path in any e-commerce platform, and must stay fast and available under extreme load (flash sales, big shopping days).
  • Financial auditability — every cent charged to every method must be traceable and reconcilable independently.
02

The Problem & Motivation

📌
Problem statement

Design a system that allows a customer to pay for a single purchase using two or more different payment methods simultaneously (for example, a gift card covering part of the amount and a credit card covering the rest), guaranteeing that the full purchase amount is charged exactly once across all methods combined — never overcharged, never undercharged, and never left in a half-charged state — while supporting on the order of a million checkout-related requests per minute across the platform.

Why “Just Charge Each Method One After Another” Doesn’t Work

A naive implementation charges method 1, and if it succeeds, charges method 2, and so on, sequentially, with no coordination logic beyond a simple loop. This breaks down the moment method 2 fails after method 1 has already succeeded: the customer has now been charged $30 on their gift card for a purchase that isn’t going to complete, inventory may already be reserved, and there is no clear, automatic path back to a clean state unless the system was explicitly designed with compensation logic from day one.

The Three Real Problems Hiding Inside One Problem

Sub-problemWhat it really isPrimary technique
AllocationDeciding exactly how much of the total each chosen payment method should cover, including rounding rulesA deterministic Allocation Engine with clear, testable rules
Coordinated Authorization & CaptureGetting every method to approve its share, and only then committing all of them together (or none)Saga pattern with a two-phase authorize-then-capture flow per method
CompensationCleanly reversing any methods that already succeeded if another method in the same purchase failsAutomated compensating transactions (voids/refunds) per payment method

Business Constraints That Shape the Design

  • Method ordering rules: Many businesses require certain methods to be charged first — for example, gift cards and store credit are typically applied before a credit card, since they represent liabilities already on the business’s books that customers expect to be depleted first.
  • Partial-amount instruments: Gift cards and store credit can only ever cover up to their remaining balance; the Allocation Engine must never allocate more to a method than it can actually cover.
  • Minimum charge amounts: Card networks often reject authorizations below a minimum (e.g., $0.50); the allocation logic must avoid leaving an unpayable sliver on any one method.
  • Authorization holds vs. captures: Card payments are typically authorized (a hold placed) before being captured (the hold converted to an actual charge) at order fulfillment time, while gift card debits are often immediate — the orchestrator must handle methods with different authorization semantics uniformly.
  • Refund complexity: A return or refund on a split payment must correctly reverse the right amount on the right combination of original methods, not simply refund the full amount to one method.
  • Scale: Checkout is typically the most latency- and availability-sensitive path in the entire platform, since it directly gates revenue, and traffic spikes dramatically during sales events.
💬
What an interviewer may ask

“If a customer has $30 of gift card balance and the order total changes mid-checkout because of a shipping fee update, what happens to the allocation?” A strong answer: allocation must be recomputed and re-confirmed with the customer whenever the chargeable total changes after methods were first selected — the system should never silently re-allocate a changed total across previously-selected methods without the customer seeing and approving the new breakdown.

03

Architecture & Components

Here is the complete system, end to end. Every box states both what it does and what kind of component it is (gateway, load balancer, orchestrator, processor adapter, and so on), since an interviewer expects you to be able to name the role of every box you draw, not just its business label.

CLIENT LAYER Mobile App Client Web Checkout Client Point-of-Sale Client EDGE & GATEWAY LAYER CDN Edge Cache Load Balancer (L4/L7) API Gateway (Auth/RL/Route) APPLICATION SERVICE LAYER Auth Service Split PaymentOrchestrator Payment Allocation Engine Payment Method Router Gift Card Service Credit Card Service Wallet / Store Credit Service Ledger Service Refund & Reversal Service Notification Service EXTERNAL PAYMENT PROVIDERS Gift Card Processor Card Network Acquirer OBSERVABILITY LAYER Prometheus Jaeger Tracing Central Logs DATA & MESSAGING LAYER Kafka Message Queue Idempotency Store (Redis) Payment State Store (Redis) Audit Log Store Primary DB (Postgres, sharded) Read Replica DB Async workers (refund/notify)
Fig. 1 — End-to-end split payment architecture. Every box names its component role — gateway, load balancer, orchestrator, processor adapter, cache, queue — doubling as a component inventory.

Component-by-Component Breakdown

3.1 CDN (Content Delivery Network)

Serves the static checkout page assets (JS bundles, styling, product images) so the actual payment API traffic hitting the backend is limited to real transactional calls, not asset requests.

3.2 Load Balancer

Distributes incoming checkout traffic across regions and availability zones at Layer 4 (raw TCP distribution) and Layer 7 (HTTP-aware routing, health checks, connection draining during deploys). Checkout traffic is spiky by nature (flash sales, restock drops), so the load balancer tier must itself be elastically scaled ahead of anticipated peaks.

3.3 API Gateway

The single entry point for all client requests: authentication, coarse rate limiting per customer, request validation, and routing to the Split Payment Orchestrator. No payment-method-specific logic lives here — the gateway stays payment-agnostic.

3.4 Auth Service

Validates the customer’s session and, importantly, confirms ownership of the payment instruments being used (a gift card code or stored card token must belong to the authenticated customer or be a valid redeemable code) before any allocation or authorization begins.

3.5 Split Payment Orchestrator

The brain of the system. Receives the order total and the customer’s chosen combination of payment methods, drives the Allocation Engine, coordinates the Payment Method Router through authorization and capture, and owns the saga’s compensation logic if anything fails partway through. This is the one component that must see the entire transaction from start to finish.

3.6 Payment Allocation Engine

A pure, deterministic function that takes the order total and the list of selected methods (with their available balances and priority order) and computes exactly how much each method should be charged, respecting balance caps, minimum-charge rules, and rounding. Being a pure function (no side effects, no external calls) makes it trivially fast and easy to unit test exhaustively.

3.7 Payment Method Router

Fans out the allocated amounts to the correct downstream service per method type, and fans back in the results. It’s the component that lets the Orchestrator treat “authorize $30 on a gift card” and “authorize $70 on a credit card” as symmetrical operations even though the two are handled by completely different downstream services and providers.

3.8 Gift Card Payment Service, Credit Card Payment Service, Wallet/Store Credit Service

Each is a dedicated adapter service wrapping one category of payment method, translating the Orchestrator’s generic “authorize” and “capture” calls into the specific API calls, error codes, and retry semantics of its underlying processor. New payment method types (like BNPL) are added by writing one new adapter service, not by modifying the Orchestrator.

3.9 External Payment Providers

The actual card network acquirer (e.g., a processor like Stripe or Adyen sitting in front of Visa/Mastercard rails) and the gift card processor (often an internal service if gift cards are issued by the business itself, or a third-party gift card platform). These are outside the company’s direct control and must be treated as unreliable, rate-limited external dependencies.

3.10 Payment State Store

A fast key-value store (Redis) holding the live, in-progress state of each split payment transaction — which methods have been authorized, which captured, which failed — keyed by a transaction ID. This is what lets the Orchestrator (and any retry or recovery process) know exactly where a given multi-method transaction currently stands.

3.11 Idempotency Store

Holds client-supplied idempotency keys mapped to their final result for a bounded window, preventing a retried “Pay Now” click (common on flaky mobile networks) from re-running the entire split payment saga a second time.

3.12 Ledger Service

Records a double-entry accounting trail for every method involved in a split transaction — a $120 purchase split three ways produces separate, individually traceable ledger entries for the gift card debit, the store credit debit, and the credit card charge, all tied back to one order ID.

3.13 Refund and Reversal Service

Handles both the compensation path (undoing an already-succeeded method when a sibling method in the same transaction fails) and customer-initiated returns after the fact, in both cases needing to know exactly which method covered which portion of the original purchase.

3.14 Message Queue (Kafka)

Decouples the Orchestrator from slower or optional downstream work: sending confirmation notifications, feeding the fraud/risk analytics pipeline, and triggering the reconciliation job all happen asynchronously off the customer’s critical checkout path.

3.15 Primary Database, Read Replicas, Audit Log

The Primary Database (sharded PostgreSQL) holds durable order and transaction records once a split payment completes; read replicas serve order-history and reporting queries; the Audit Log is an immutable record of every allocation decision and every authorization/capture/reversal event per method, satisfying financial audit requirements.

3.16 Observability Stack

Metrics, distributed tracing, and centralized logging, covered in depth in Section 10 — with split payments, tracing a single order across multiple payment-method adapters is especially valuable for debugging partial failures.

💬
What an interviewer may ask

“Why is the Allocation Engine a separate component from the Orchestrator instead of just inline logic?” Good answer: allocation is pure business logic with no I/O, while the Orchestrator’s job is coordinating I/O-heavy calls to external systems and managing saga state. Separating them means the allocation rules (which change often — new promotions, new rounding policies) can be tested and deployed independently of the much more sensitive orchestration and compensation code.

04

Internal Working

4.1 How Allocation Is Computed

1

Customer selects total & methods

Customer chooses a purchase total (say $120) and payment methods: gift card (balance $30), store credit (balance $20), credit card (default fallback for the remainder).

2

Priority-ordered allocation

The Allocation Engine applies methods in business-defined priority order — typically gift card, then store credit, then card — capping each at the lesser of its available balance or the remaining unallocated total.

3

Gift card cap

Allocated min(30, 120) = 30. Remaining total: 90.

4

Store credit cap

Allocated min(20, 90) = 20. Remaining total: 70.

5

Credit card remainder

Allocated the full remainder, 70, since it has no balance cap.

6

Minimum-charge check

The engine checks each allocated amount against minimum-charge rules per method type, and if a method’s allocation would fall below its minimum, it’s dropped from this transaction and its amount rolled into the next method in priority order.

7

Plan persisted

The final allocation {giftCard: 30, storeCredit: 20, creditCard: 70} is returned to the Orchestrator and written into the Payment State Store as the transaction’s “plan,” before any real money moves.

Java — pure, deterministic allocation logic (no I/O, fully unit-testable)
public List<MethodAllocation> allocate(BigDecimal total, List<PaymentMethod> methodsInPriorityOrder) {
    List<MethodAllocation> allocations = new ArrayList<>();
    BigDecimal remaining = total;

    for (PaymentMethod method : methodsInPriorityOrder) {
        if (remaining.compareTo(BigDecimal.ZERO) <= 0) break;

        BigDecimal cap = method.hasBalanceCap() ? method.getAvailableBalance() : remaining;
        BigDecimal allocated = remaining.min(cap);

        if (allocated.compareTo(method.getMinimumChargeAmount()) < 0) {
            continue; // too small to charge this method meaningfully, skip it
        }

        allocations.add(new MethodAllocation(method.getId(), allocated));
        remaining = remaining.subtract(allocated);
    }

    if (remaining.compareTo(BigDecimal.ZERO) > 0) {
        throw new InsufficientCoverageException(remaining); // no method covers the rest
    }
    return allocations;
}

4.2 The Authorize-Then-Capture Saga

Rather than immediately charging every method, the Orchestrator uses a two-phase flow per method, mirroring how card networks already work (authorize places a hold; capture finalizes it), and applies the same two-phase discipline even to instant-debit methods like gift cards by treating their “authorize” step as a soft reservation:

  1. Phase 1 — Authorize all methods in parallel. The Orchestrator calls the Payment Method Router, which fans out authorization requests to every selected method’s adapter service simultaneously (not sequentially, to minimize total checkout latency). Each adapter returns success or failure independently.
  2. Decision point. Only if every single method authorizes successfully does the Orchestrator proceed to capture. If even one method fails to authorize, none are captured — any methods that did authorize are immediately voided (a cancelled hold, not a real reversal, since nothing was captured yet).
  3. Phase 2 — Capture all methods. Once all authorizations are confirmed, the Orchestrator instructs every adapter to capture its held amount. This step is intentionally kept as fast and reliable as possible, since by this point the business has already committed (inventory reserved, order created) based on the authorization success.
  4. Capture-phase failure handling. If a capture fails after authorization succeeded (rare, but possible — e.g., a card issuer system hiccup at the exact capture moment), the Orchestrator triggers compensation (Section 4.3) for any methods that did successfully capture.
Java — orchestrator driving the two-phase authorize-then-capture saga
public SplitPaymentResult executeSplitPayment(SplitPaymentRequest request, String idempotencyKey) {

    Optional<SplitPaymentResult> existing = idempotencyStore.find(idempotencyKey);
    if (existing.isPresent()) return existing.get();

    List<MethodAllocation> plan = allocationEngine.allocate(request.getTotal(), request.getMethods());
    stateStore.savePlan(request.getTransactionId(), plan);

    List<AuthResult> authResults = methodRouter.authorizeAllInParallel(plan);

    if (authResults.stream().anyMatch(AuthResult::isFailure)) {
        methodRouter.voidSuccessfulAuthorizations(authResults);
        SplitPaymentResult failed = SplitPaymentResult.failed(authResults);
        idempotencyStore.save(idempotencyKey, failed, Duration.ofHours(24));
        return failed;
    }

    List<CaptureResult> captureResults = methodRouter.captureAllInParallel(plan);

    if (captureResults.stream().anyMatch(CaptureResult::isFailure)) {
        compensationService.reverseSuccessfulCaptures(captureResults);
        SplitPaymentResult failed = SplitPaymentResult.failedAtCapture(captureResults);
        idempotencyStore.save(idempotencyKey, failed, Duration.ofHours(24));
        return failed;
    }

    ledgerService.recordAllCaptures(request.getTransactionId(), plan, captureResults);
    SplitPaymentResult success = SplitPaymentResult.success(captureResults);
    idempotencyStore.save(idempotencyKey, success, Duration.ofHours(24));
    return success;
}

4.3 Compensation Logic

Compensation is the piece that separates a production-grade split payment system from a fragile prototype. When one method fails, every method that already succeeded must be automatically reversed — a void if it was only authorized, a refund/reversal if it was already captured.

Java — compensation triggered when a sibling method fails mid-saga
public void reverseSuccessfulCaptures(List<CaptureResult> results) {
    for (CaptureResult result : results) {
        if (result.isSuccess()) {
            PaymentMethodAdapter adapter = adapterRegistry.get(result.getMethodType());
            adapter.reverseCapture(result.getMethodId(), result.getAmount());
            ledgerService.recordReversal(result.getTransactionId(), result.getMethodId(), result.getAmount());
        }
    }
}
💬
What an interviewer may ask

“Why authorize-then-capture instead of just capturing directly on the first pass?” Strong answer: the two-phase approach lets the system confirm every method can actually be charged before any money commits anywhere, dramatically shrinking the compensation surface — voiding an authorization is cheap and instant on most networks, while reversing an already-captured charge can be slower, sometimes not same-day, and occasionally incurs processor fees.

4.4 Idempotency Across Multiple Methods

A single idempotency key covers the entire split transaction, not one key per method — this is important, because a client retry must replay the whole saga’s cached outcome atomically, rather than allowing a scenario where a retry re-runs allocation and re-authorizes only some methods while treating others as already done.

4.5 CAP Theorem and Where Each Store Sits

Every distributed store in this design makes a deliberate, explicit trade-off between consistency and availability during a network partition (partition tolerance itself being non-negotiable in any real distributed deployment):

ComponentCAP posture during a partitionWhy this is the right choice here
Payment State StoreConsistency-favoring (CP) for writesTwo nodes disagreeing about whether a method has already been captured is unacceptable — it could directly cause a double-charge or a missed reversal
Idempotency StoreConsistency-favoring (CP)A stale or lost idempotency record defeats the entire purpose of the mechanism, risking a duplicate saga execution
Ledger DatabaseStrongly consistent (CP), full ACIDMoney must never be double-counted or lost; availability is knowingly sacrificed during a partition rather than risk an inconsistent balance
Gift card / store credit balance cache (checkout preview only)Availability-favoring (AP)A briefly stale balance shown during preview is acceptable since it is never used to authorize the actual charge — only the live, re-validated balance at authorization time is authoritative

4.6 Concurrency Control

The Payment State Store’s per-transaction record uses optimistic concurrency control: every write includes a version number, and the Orchestrator’s update is rejected (and retried after re-reading) if the version has changed since it was last read. This prevents a rare but real race — for example, the live saga process and the recovery sweep from Section 8.3 both attempting to act on the same stuck transaction at once — without needing a heavyweight distributed lock on the hot path.

Java — optimistic concurrency check when updating saga state
public void markMethodCaptured(String txnId, String methodId, int expectedVersion) {
    boolean updated = stateStore.compareAndSwap(
        txnId,
        expectedVersion,
        state -> state.withMethodCaptured(methodId)
    );
    if (!updated) {
        throw new ConcurrentSagaModificationException(txnId);
        // caller re-reads the latest state and decides whether to retry or abort
    }
}

4.7 Consensus and Replication in the Underlying Infrastructure

Where the system needs strict multi-node agreement — Redis Cluster deciding which node currently owns a given shard, or Kafka electing a partition leader — this relies on the underlying infrastructure’s own established consensus protocols (Raft-based consensus in modern Redis Cluster and in Kafka’s KRaft mode) rather than the application layer attempting to build its own distributed consensus, which is a common and costly mistake at this level of system complexity. Replication factor for the Payment State Store and Ledger is set higher than for the purely informational balance-preview cache, directly reflecting the differing consistency requirements from Section 4.5.

05

Data Flow & Lifecycle

Customer API Gateway Orchestrator Allocation Eng. Method Router Gift Card Svc Card Svc Checkout 100 (gift+card) Create split payment req Compute allocation gift 40, card 60 Authorize in parallel Auth gift 40 Auth card 60 gift OK card OK All authorized Capture gift 40 Capture card 60 gift captured card captured Split payment done Show receipt (ledger writes happen in parallel with the receipt render)
Fig. 2 — Sequence diagram of a two-method split payment moving through allocation, parallel authorization, and parallel capture.
Initiated Allocated Authorizing Methods All Authorized Partial Auth Failure Capturing Partial Capture Failure Compensating Captured Completed Failed
Fig. 3 — State lifecycle of a split payment transaction. Both authorization-phase and capture-phase failures route through the same Compensating state, keeping reversal logic centralized.

Compensation Flow in Detail

One PaymentMethod Fails Which methodsalready succeeded? Reverse Gift Card Capture Reverse Credit Card Capture Update Ledger — Reversal Entry Publish “payment failed” Notify customer Release reserved inventory Gift Card captured Credit Card captured
Fig. 4 — Compensation flow. Every method that already succeeded gets an independent, explicit reversal — nothing is left half-charged.
06

Advantages, Disadvantages & Trade-offs

Advantages of the Authorize-then-Capture Saga

  • Minimizes the compensation surface — most failures are caught at the cheap “authorization” stage, before any real capture happens.
  • Each payment method type is fully decoupled behind its own adapter, so adding a new method (BNPL, crypto, regional wallets) never touches the Orchestrator’s core logic.
  • Parallel authorization across methods keeps checkout latency close to that of the slowest single method, rather than the sum of all methods.

Disadvantages / Costs

  • Genuine architectural complexity versus a single-method checkout — more moving parts, more failure modes to test.
  • Some payment processors don’t support a clean “authorize without capture” mode for certain method types, forcing a fallback to slower reversal-based compensation for those methods specifically.
  • Held authorizations on multiple methods simultaneously can temporarily reduce a customer’s available balance/credit across several instruments at once, even before the purchase finalizes — a real UX and support consideration.

Key Trade-off: Sequential vs. Parallel Authorization

ApproachBenefitCost
Sequential (one method at a time)Simpler to reason about; can short-circuit early on first failure without touching other methods at allTotal latency is the sum of every method’s latency — unacceptable for a fast checkout experience with 2-3+ methods
Parallel (all methods at once)Total latency is close to the slowest single method, not the sum — essential for checkout UXRequires the void/compensation logic to be robust from day one, since a failure can occur after other methods have already succeeded in parallel

Key Trade-off: Where to Draw the Line on Method Priority

Always charging liability-backed instruments (gift cards, store credit) before a credit card is standard because it reduces the business’s outstanding liability fastest, but it does mean a gift card is committed (authorized) even if the credit card authorization later fails — which is exactly why the compensation path must be equally well-built for the “cheap” methods, not just the card rails.

💬
What an interviewer may ask

“Would you ever authorize methods sequentially instead of in parallel?” A nuanced answer: yes, in specific cases — for instance, if a business policy requires the customer to explicitly confirm the card charge amount only after gift card/store credit balances are deducted first (some regulatory environments require this transparency), sequential authorization for just that pairing might be intentional, trading a small amount of latency for a clearer customer-facing breakdown.

07

Performance & Scalability — Designing for a Million Requests a Minute

A million requests a minute is roughly 16,700 requests/second sustained, with realistic 3-5x bursts during flash sales or high-traffic shopping events pushing peak load toward 50,000-85,000 requests/second. Split payment checkout traffic is a smaller, higher-value subset of total platform traffic (most of a million requests/minute on an e-commerce platform is browsing and search, not checkout), but checkout is also the path where failure is most costly, so it deserves dedicated, carefully-provisioned capacity even at a smaller absolute volume.

7.1 Read/Write Split at the Platform Level

PathTypical share of total platform trafficConsistency needScaling strategy
Browsing / product / cart (read)~90%+Eventually consistent, cache-heavyCDN and read-replica-backed, entirely separate from the payment path
Checkout / split payment (write)Small % of total, but highest valueStrongly consistent, exactly-once per methodDedicated, isolated service pool with its own scaling and alerting, never sharing capacity with browsing traffic

7.2 Capacity Math Walkthrough

  • Platform-wide sustained load: 1,000,000 requests / 60 seconds ≈ 16,667 requests/second average across the whole platform.
  • Checkout share: if checkout-related calls (cart finalize, allocation preview, pay-now) represent roughly 5-8% of total platform traffic even during a sale, that is still 800-1,300+ requests/second sustained hitting the Split Payment Orchestrator, with bursts pushing well beyond that during a flash sale’s opening minutes.
  • Per-method fan-out multiplier: a single “Pay Now” click for a 2-method split generates two parallel downstream authorize calls and two parallel capture calls — meaning the Payment Method Router and adapter services see roughly double the request volume of the Orchestrator itself, and this multiplier grows with the number of methods a business chooses to support per order.
  • External provider rate limits: unlike the currency-cache pattern in a pure-read system, split payments cannot avoid calling out to real external processors for every authorization and capture — so provider-side rate limits (not just internal capacity) become a hard ceiling that must be planned for explicitly, often requiring negotiated higher rate limits with major processors ahead of known high-traffic events.

7.3 Horizontal Scaling of the Orchestrator and Adapters

The Split Payment Orchestrator is designed to be stateless per request — all in-flight transaction state lives in the shared Payment State Store, not in server memory — so the Orchestrator pool scales horizontally behind the load balancer with no coordination overhead, identically to the Method Router and each payment-method adapter service.

Java — orchestrator entry point kept stateless and safe to run on any instance
public SplitPaymentResult handleCheckout(SplitPaymentRequest request) {
    // All state reads/writes go through the shared Payment State Store and
    // Idempotency Store - this instance holds nothing in local memory
    // that another instance couldn't pick up if this request were retried
    // against a different pod entirely.
    return executeSplitPayment(request, request.getIdempotencyKey());
}

7.4 Protecting External Providers with Bulkheads and Queuing

Since every authorize/capture call ultimately hits a real external processor with its own rate limits, the adapter services implement per-provider bulkheads (separate thread/connection pools per provider so a slowdown in the card network never starves gift card processing capacity) and, for non-latency-critical work like reconciliation, queue-backed batching via Kafka to smooth bursts rather than hammering providers with every request synchronously.

7.5 Caching What Can Safely Be Cached

Not everything in this system benefits from caching the way a pure rate-lookup system does, since almost every operation here is a real state-changing call to an external provider. What can be cached: gift card balance lookups for the checkout preview screen (short TTL, a few seconds, purely informational — never used to authorize the actual charge, which always re-validates the real balance at authorization time) and customer payment-method metadata (stored card brand/last 4 digits, gift card nicknames) which changes rarely.

7.6 Capacity Planning Table

LayerTarget capacity at peakScaling lever
Load Balancer / Gateway80,000+ req/s platform-wideMulti-region LB, auto-scaled gateway pool
Split Payment Orchestrator3,000-5,000 req/s at peakStateless horizontal auto-scaling
Payment Method Adapters (per method)Sized to match Orchestrator fan-out, capped by provider rate limitsPer-provider bulkheads, negotiated rate limit increases for major sale events
Payment State Store (Redis)10,000+ ops/sCluster sharded by transaction ID
Primary DatabaseWrite-optimized, sharded by customer regionSharding + read replicas for reporting queries
💬
What an interviewer may ask

“A million requests a minute — where’s your bottleneck going to be first in this system specifically?” Good candidates recognize that, unlike a pure-cache-read system, the bottleneck here is very likely the external payment processors’ own rate limits, not internal infrastructure — and that this must be handled with pre-negotiated limits, graceful queuing, and bulkheading, since no amount of internal auto-scaling can make a third party’s API accept more traffic than it’s willing to.

7.7 Networking Considerations at This Volume

Internal service-to-service calls (Orchestrator to each adapter) benefit from HTTP/2 or gRPC with persistent connection pooling to avoid handshake overhead at high fan-out volume. Calls to external payment providers, by contrast, are typically REST/HTTPS per the provider’s own API, so connection reuse (keep-alive) to each provider’s endpoint is the main lever available — a well-tuned HTTP client with a properly sized connection pool per provider avoids becoming an accidental bottleneck of the system’s own making.

08

High Availability & Reliability

8.1 No Single Point of Failure Per Payment Method

Where a business’s card processing relationship allows it, the Card Payment Service adapter can support more than one acquirer/processor and fail over between them if the primary is degraded — protecting the whole split transaction from being blocked by a single provider’s outage, in the same spirit as the multi-provider approach used for external data feeds in other high-stakes systems.

8.2 Payment State Store Reliability

The Payment State Store is the single most important piece of infrastructure for correctness during partial failures — it’s what lets a recovery process determine, even after an Orchestrator instance crashes mid-saga, exactly which methods were authorized and which were captured, so an automated recovery job can safely resume or compensate rather than guessing. It therefore runs with cross-AZ replication and a higher durability configuration than a typical cache.

8.3 Saga Recovery After a Crash

If the Orchestrator instance handling a transaction crashes mid-saga (say, after authorizing the gift card but before authorizing the credit card), a background recovery process periodically scans the Payment State Store for transactions stuck in an intermediate state past a reasonable timeout, and either resumes the saga from where it left off or triggers compensation — ensuring no transaction is silently abandoned in a half-authorized state.

Java — background recovery sweep for stuck split payment transactions
@Scheduled(fixedDelay = 30000)
public void recoverStuckTransactions() {
    List<SplitPaymentState> stuck = stateStore.findStuckBeyond(Duration.ofMinutes(2));
    for (SplitPaymentState state : stuck) {
        if (state.allMethodsAuthorized()) {
            orchestrator.resumeAtCapture(state);
        } else {
            compensationService.reverseSuccessfulAuthorizations(state);
        }
    }
}

8.4 Multi-Region Deployment

The Orchestrator, Method Router, and adapter services run across multiple regions behind global traffic routing. Because payment provider relationships are sometimes region-specific (a card acquirer contract might be scoped to one geography), the routing layer must be aware of which region “owns” a given customer’s payment methods, rather than treating every region as fully interchangeable the way a pure cache-read system could.

8.5 Circuit Breakers Around Every External Provider

Java — circuit breaker isolating one provider outage from sibling methods
public AuthResult authorizeCard(CardAuthRequest request) {
    if (circuitBreaker.isOpen("cardAcquirerX")) {
        throw new ProviderUnavailableException("cardAcquirerX");
    }
    try {
        AuthResult result = cardAcquirerXClient.authorize(request);
        circuitBreaker.recordSuccess("cardAcquirerX");
        return result;
    } catch (ProviderTimeoutException e) {
        circuitBreaker.recordFailure("cardAcquirerX");
        throw new ProviderUnavailableException("cardAcquirerX", e);
    }
}

Note this failure is surfaced back to the Orchestrator as an ordinary authorization failure for that one method — it does not need special-case handling, because the saga’s existing compensation logic already knows how to cleanly unwind any sibling methods that succeeded before this one failed.

8.6 Disaster Recovery

The Ledger and Audit stores are backed up continuously with near-zero Recovery Point Objective, since they are the systems of record proving exactly what was charged to which method for every order — a requirement shared with most regulated financial systems.

💬
What an interviewer may ask

“The Orchestrator crashes right after capturing the gift card but before capturing the credit card. What happens?” Strong answer: this is exactly the scenario the Payment State Store and the recovery sweep exist for — the recovery process finds the stuck transaction, sees the gift card capture succeeded and the credit card authorization step was already confirmed, and either resumes the capture on the credit card or, if the authorization has since expired, compensates the gift card capture. The key insight: recovery logic reads the same state store the live saga uses, so there’s no separate, divergent “recovery path” to keep in sync.

09

Security

9.1 Authentication & Authorization

Every checkout request is authenticated via OAuth 2.0/JWT at the API Gateway. The Auth Service additionally verifies that every payment method referenced in the request — gift card code, stored card token, store credit balance — actually belongs to (or is legitimately redeemable by) the authenticated customer, closing off a common attack where a customer tries to apply someone else’s gift card code to their own order.

9.2 PCI-DSS Scope Minimization

Raw card numbers are never handled or stored by the Split Payment Orchestrator or any internal service — the client tokenizes card details directly with the card processor (via their hosted fields or SDK), and only an opaque, single-use token ever reaches the backend. This keeps the vast majority of the system entirely out of PCI-DSS scope, dramatically reducing both risk and compliance burden, and is standard practice across the payments industry.

9.3 Preventing Allocation Manipulation

  • Server-side allocation only: the client can request which methods to use and in what preference, but the actual charged amount per method is always computed and enforced server-side by the Allocation Engine — a tampered client request claiming a different split is simply recalculated, not trusted.
  • Balance re-validation at authorization time: even though a balance may have been shown during checkout preview, the Gift Card and Wallet services always re-check the live balance at the moment of authorization, closing a time-of-check-to-time-of-use gap where a balance could have been spent elsewhere between preview and confirm.

9.4 Fraud Controls

Split payments introduce a specific fraud pattern worth calling out: using multiple small, low-scrutiny methods (several small gift cards, for instance) to launder value or test stolen payment credentials without triggering a single large-transaction fraud alert. The system runs velocity checks across the full order (not per individual method) and flags orders using an unusual number of distinct payment methods for manual or automated review.

9.5 Data Protection

Customer PII and any payment tokens are encrypted at rest (AES-256) and in transit (TLS 1.3). Logs are scrubbed of sensitive fields (card tokens, gift card codes) before reaching centralized logging, since logs are a common accidental leak surface.

9.6 Secrets Management

Credentials for every payment processor integration are stored in a dedicated secrets manager with short-lived, automatically rotated credentials, never embedded directly in service configuration.

💬
What an interviewer may ask

“Could a customer manipulate the split to get an unfair discount, like allocating more to a smaller gift card than it’s worth?” The answer: no, because the Allocation Engine’s output is always recomputed and capped server-side against the live balance of each method at authorization time — the client’s requested allocation is treated as a preference/input, never as an authoritative instruction the server blindly trusts.

10

Monitoring, Logging & Metrics

10.1 The Metrics That Matter Most for This System

MetricWhy it matters
Authorization success rate, per method typeA drop for one method (say, credit cards specifically) surfaces a provider-side issue before it snowballs into a checkout-wide problem
Compensation trigger rateShould stay low and stable; a sudden spike signals a systemic problem with a specific provider or a bug in the allocation logic
End-to-end checkout latency (p50/p95/p99), split by number of methods usedDirectly ties to conversion rate; a 3-method split should not meaningfully exceed a 1-method checkout’s latency thanks to parallel authorization
Stuck-transaction count (from the recovery sweep)Should be near zero at steady state; any sustained non-zero value means recovery isn’t keeping up with crash/timeout frequency
Per-provider circuit breaker open eventsEarly warning of a degrading external payment processor
Ledger imbalance alertsShould never fire; any occurrence is treated as a P1 incident given the direct financial correctness implication

10.2 Distributed Tracing Across Methods

Every checkout request carries a single trace ID propagated through the Orchestrator, Allocation Engine, Method Router, and every individual method adapter it fans out to. This is especially valuable here: when a customer reports “my gift card was charged but my order shows as failed,” an engineer can pull up the full trace and see exactly which method succeeded, which failed, and whether compensation ran correctly, in one view instead of correlating logs across several independent adapter services manually.

10.3 Alerting Philosophy

  • P1 (page immediately): ledger imbalance detected; compensation failure (a reversal itself failed, leaving a customer genuinely overcharged); Payment State Store cluster degraded.
  • P2 (urgent, business hours): a single payment provider’s authorization success rate drops below threshold; stuck-transaction count rising.
  • P3 (informational): auto-scaling events, minor latency degradation within SLA.

10.4 Dashboards for Different Audiences

Engineering dashboards track latency, error rates, and circuit breaker state per provider. A separate finance/risk dashboard tracks aggregate outstanding authorizations (money held but not yet captured, across all in-flight split transactions) and compensation volume, since both represent real, live financial exposure that finance teams need visibility into independent of engineering’s operational view.

🚨
What an interviewer may ask

“What’s the single most important alert in this whole system?” Strongest answer: a failed compensation — that is, a case where the system tried to reverse an already-captured charge (because a sibling method failed) and that reversal itself failed. This is the one failure mode that can leave a real customer genuinely, unrecoverably overcharged, so it deserves the tightest alerting and, ideally, an automated retry-with-escalation path rather than relying purely on human response.

11

Deployment & Cloud

11.1 Containerized Microservices on Kubernetes

Each component — Orchestrator, Allocation Engine, Method Router, each payment-method adapter, Ledger Service, Refund Service — is packaged as an independent container and deployed on Kubernetes, allowing each to scale according to its own traffic profile; the Orchestrator and adapters typically need far more replicas than the lower-volume Refund Service.

11.2 Canary Deployments for Payment-Critical Code

Given the direct financial stakes, changes to the Allocation Engine or Orchestrator are always rolled out via canary deployment, routing a small percentage of real checkout traffic to the new version while automated checks confirm the ledger-imbalance and compensation-failure metrics from Section 10.1 stay at zero before the rollout proceeds — any anomaly triggers automatic rollback, treating payment-path deploys with more caution than typical feature rollouts elsewhere in the platform.

11.3 Infrastructure as Code

Kubernetes clusters, Redis clusters, Kafka topics, database instances, and per-provider network/firewall rules are all defined declaratively (Terraform or similar), letting a new region or a new payment-method adapter’s supporting infrastructure be stood up reproducibly.

11.4 Multi-Region Cloud Topology

Regional active-active deployment (e.g., US, EU, APAC) with the Payment State Store and Idempotency Store globally replicated for the life of a single in-flight transaction, while the Ledger and customer order data are sharded regionally to satisfy data residency requirements common in payments regulation.

11.5 Cost Optimization

The Orchestrator and adapter pools, being stateless, mix spot/preemptible instances with a reserved baseline for predictable steady-state traffic, while the Payment State Store and Ledger infrastructure run on stable, reserved capacity given their correctness sensitivity — the same cost-vs-criticality split applied consistently across every tier of the system.

💬
What an interviewer may ask

“Would you deploy the Allocation Engine and the Orchestrator together, or separately?” A balanced answer: separately, even though they’re tightly coupled logically — the Allocation Engine is pure logic with a much simpler blast radius if something goes wrong, so it can iterate and deploy faster and more frequently, while the Orchestrator (which manages live saga state and external provider calls) benefits from a slower, more heavily gated release cadence.

12

Databases, Caching & Load Balancing

12.1 Choosing the Store for Each Piece of Data

StoreTechnology choiceReasoning
Payment State StoreRedis Cluster, high replication factorFast reads/writes for live saga state; correctness-critical, so replicated more heavily than a typical cache
Idempotency StoreRedis with native TTLAutomatic expiry of old idempotency keys after the retry window closes, no manual cleanup job needed
LedgerRelational (PostgreSQL), append-only tables, strict ACID transactionsDouble-entry accounting demands strong consistency and multi-row atomic commits
Order / Transaction HistoryPostgreSQL, sharded by customer regionRelational integrity for order-customer-payment relationships; sharding purely for write/storage scale
Audit LogAppend-only object storage (S3 with object lock) or an immutable log tableRegulatory requirement for tamper-evidence and multi-year retention

12.2 Caching Strategy Deep Dive

Unlike a pure rate-lookup system, most operations here are not cacheable, since they represent real state-changing calls to external providers. The narrow, safe caching opportunity is read-only, informational data shown during checkout preview (gift card balance display, saved card metadata) with a short TTL and an explicit rule that this cached value is never used to actually authorize a charge — authorization always re-reads live balance data.

12.3 Load Balancing Algorithms

  • Layer 4 load balancing at the outer edge for raw connection distribution across regions.
  • Layer 7 load balancing at the API Gateway, routing checkout-specific traffic to the Orchestrator pool separately from browsing/search traffic, and performing active health checks on both.
  • Client-side load balancing via a service mesh between the Orchestrator and each adapter service, providing mTLS between internal services and fine-grained traffic shaping for canary releases described in Section 11.2.

12.4 Sharding the Primary Database

Order and transaction history is sharded by a hash of customer ID combined with home region, keeping a given customer’s order history queries local to one shard while distributing write load evenly as the customer base and order volume grow.

💬
What an interviewer may ask

“Why not just use one database for everything here, given how tightly coupled allocation, authorization, and ledger data all are?” Good answer: coupling the data doesn’t require coupling the storage technology — each store is chosen for the specific consistency and performance profile of its data, and keeping them separate (with the Orchestrator as the coordinating layer) is exactly what lets the read-heavy checkout-preview path, the write-heavy live-saga-state path, and the durability-critical ledger path each scale and fail independently of one another.

13

APIs & Microservices

13.1 Core API Contract

HTTP — allocation preview + split payment execution APIs
// Allocation Preview API (read-only, for showing the customer a breakdown before confirming)
POST /v1/checkout/allocation-preview
Request:  {
  "orderTotal": 120.00,
  "methods": [
    { "type": "GIFT_CARD", "id": "gc_123" },
    { "type": "STORE_CREDIT", "id": "sc_456" },
    { "type": "CREDIT_CARD", "id": "card_token_789" }
  ]
}
Response: {
  "allocations": [
    { "methodId": "gc_123", "type": "GIFT_CARD", "amount": 30.00 },
    { "methodId": "sc_456", "type": "STORE_CREDIT", "amount": 20.00 },
    { "methodId": "card_token_789", "type": "CREDIT_CARD", "amount": 70.00 }
  ]
}

// Split Payment Execution API
POST /v1/checkout/pay
Headers:  Idempotency-Key: <client-generated-uuid>
Request:  { "orderId": "ord_9f21", "allocations": [ ...same shape as above... ] }
Response: {
  "transactionId": "txn_a83e",
  "status": "COMPLETED",
  "methodResults": [
    { "methodId": "gc_123", "status": "CAPTURED", "amount": 30.00 },
    { "methodId": "sc_456", "status": "CAPTURED", "amount": 20.00 },
    { "methodId": "card_token_789", "status": "CAPTURED", "amount": 70.00 }
  ]
}

The response always echoes back a per-method result, not just an overall status — this is deliberate, since customer support, refund logic, and the customer-facing receipt all need to know exactly how much was charged to which specific method, not just whether the order as a whole succeeded.

13.2 Why Microservices (and Where the Boundaries Are)

Service boundaries here follow the same principle used throughout this tutorial series: split along genuinely different scaling profiles and genuinely different change-cadences. The Allocation Engine changes often (new promotions, new method priority rules) and is pure logic; the Orchestrator changes rarely and carries high-stakes coordination responsibility; each payment-method adapter evolves independently as its underlying provider’s API evolves. Splitting any other way — say, by “everything payments” as one monolith — would force unrelated changes (a new gift card provider integration, an allocation rounding tweak) to deploy and be tested together, increasing risk for no benefit.

13.3 Synchronous vs Asynchronous Communication

InteractionStyleReasoning
Client → Orchestrator (pay)Synchronous (REST/HTTP)Customer is actively waiting on the checkout screen for a definitive success/failure result
Orchestrator → Method Router → AdaptersSynchronous, but fanned out in parallelEach method’s result is needed before the saga can proceed to its next phase
Orchestrator → Notification ServiceAsynchronous (Kafka event)The customer’s receipt/confirmation screen doesn’t need to wait for an email or SMS to be dispatched
Orchestrator → Reconciliation / AnalyticsAsynchronous (Kafka event stream)Entirely decoupled from the customer’s live checkout request

13.4 API Versioning and Backward Compatibility

Because mobile app clients in particular can lag behind the latest API version for weeks after a release (app store review delays, users on old app versions), all breaking changes go through explicit versioning with a long deprecation window, and new payment method types are added as purely additive enum values with graceful client-side fallback behavior for older clients that don’t yet recognize a new method type.

💬
What an interviewer may ask

“Would you combine the allocation-preview and pay-now calls into a single API?” Good answer: no — keeping them separate mirrors the real UX need (the customer should see and implicitly approve the breakdown before committing), and it also gives the Allocation Engine a safe, side-effect-free way to be called repeatedly (say, every time the customer toggles a method on or off) without any risk of accidentally triggering a real charge.

14

Design Patterns & Anti-Patterns

14.1 Patterns Used in This System

  • Saga Pattern: the Split Payment Orchestrator drives a multi-step, multi-method transaction with well-defined compensating actions per method, since no single database transaction can span independent external payment processors.
  • Adapter Pattern: each payment-method service wraps its provider’s specific API behind a common authorize/capture/reverse interface the Orchestrator can call uniformly.
  • Circuit Breaker: isolates a struggling external provider so its degradation doesn’t cascade into failures for sibling methods in the same transaction (Section 8.5).
  • Idempotent Receiver: a single idempotency key covers the entire multi-method saga (Section 4.4), preventing partial or duplicate replays.
  • Bulkhead: per-provider connection/thread pools ensure one slow provider can’t exhaust resources needed by another method’s adapter (Section 7.4).
  • Strategy Pattern (within the Allocation Engine): different allocation rule sets (method priority order, rounding policy) can be swapped per business or per promotion without touching the core saga logic.

14.2 Anti-Patterns to Avoid

Anti-patternWhy it’s dangerous here
Sequential, uncoordinated charging with no compensation logicDirectly causes the half-charged-order problem this entire system exists to prevent
Trusting a client-supplied allocation breakdown without server-side recomputationOpens a direct financial manipulation vector (Section 9.3)
One idempotency key per method instead of one per whole transactionAllows a retry to replay only part of a saga, leaving state inconsistent (Section 4.4)
Treating compensation as a rare edge case tested only manuallyCompensation is a first-class, frequently-exercised code path at scale (any provider blip triggers it) and needs the same automated test rigor as the happy path
Storing card numbers or full gift card codes in the Orchestrator’s own databaseMassively expands PCI-DSS compliance scope unnecessarily; tokenization exists precisely to avoid this (Section 9.2)
Blocking capture on one slow method before even starting authorization on anotherReintroduces the sequential-latency problem parallel authorization is designed to solve
💬
What an interviewer may ask

“What’s the worst anti-pattern a junior engineer might introduce here without realizing it?” A strong answer to lead with: writing the “happy path” saga carefully but leaving compensation as a rough afterthought, reasoning that failures are rare. In a system processing enough volume to see a million requests a minute, a failure rate of even a fraction of a percent on any one provider translates into a steady, real stream of compensation events every single day — under-investing in that path is one of the most common causes of real customer-facing billing incidents in payments systems.

15

Best Practices & Common Mistakes

Best Practices

Practice 1

Always recompute server-side

Recompute and validate the allocation server-side at both authorization and capture time — never trust a previously-computed value blindly across saga steps without re-verifying live balances.

Practice 2

Idempotent adapters, not just the orchestrator

Make every authorize and capture call idempotent at the adapter level too, since a network timeout between the Orchestrator and an adapter could otherwise cause a duplicate call to the same external provider.

Practice 3

Log per-method breakdowns

Log the exact per-method breakdown, provider response codes, and timestamps for every transaction, so any dispute can be traced back to precisely what happened on each individual method.

Practice 4

Test compensation like the happy path

Treat the compensation path with the same test rigor as the happy path — write automated tests that simulate a failure on each method position (first, middle, last) in a multi-method split.

Practice 5

Pure, versioned allocation

Design the Allocation Engine as a pure, versioned function so a past order’s exact allocation can always be recomputed identically for audit or dispute purposes.

Practice 6

Centralize priority & rounding

Keep method-priority and rounding rules centrally configured and testable, rather than duplicated logic scattered across adapters.

Practice 7

Version every decision

Version every allocation and saga decision explicitly, so that when the Allocation Engine’s rules change historical orders remain fully explainable using the rule version that was in effect when they were placed.

Common Mistakes

Mistakes to watch for

Under-provisioning for external provider rate limits — teams often size internal infrastructure generously but forget that a real ceiling exists at the provider side, which internal auto-scaling cannot solve. Treating gift card debits as “safe” instant operations that don’t need the same two-phase authorize/capture discipline as cards — this asymmetry is exactly what causes compensation gaps when a card fails after a gift card was already debited outright. Ignoring minimum-charge thresholds when computing allocation, leading to rejected authorizations on tiny remainder amounts that then require awkward, unplanned re-allocation logic mid-saga. Rounding inconsistently between the Allocation Engine and the Ledger Service — even a one-cent rounding mismatch across many transactions becomes a real reconciliation and audit headache at scale, so rounding rules must be defined once, centrally, and applied identically everywhere.

A Simple Rule of Thumb

Whenever a design decision in this system is unclear, the tie-breaker question is always the same: “if this specific step fails right now, do I know exactly what to reverse, and can I reverse it automatically?” If the answer is no, the design isn’t done yet — every state a transaction can be in must have a known, automated, tested path back to a clean, fully-reconciled state.

16

Real-World Industry Examples

Retail

Amazon

Amazon’s checkout has long supported combining gift card balance, promotional credit, and a card for a single order, publicly documenting that gift card and promotional balances are applied before the remaining amount is charged to the selected card — a direct real-world instance of the priority-ordered allocation approach described in this tutorial.

Payments

Stripe

Stripe’s payments platform documents patterns for combining multiple payment methods in a single checkout and emphasizes using their PaymentIntents API’s authorize-then-capture flow specifically to reduce the risk of partial failures in multi-step or multi-method payment flows, echoing the two-phase saga design in this tutorial.

Marketplaces

Adyen

As a payments platform serving large global retailers, Adyen has written about split and partial payments in the context of marketplace and multi-party payment scenarios, where a single checkout must correctly divide funds not just across payment methods but across multiple receiving parties — a related, even more complex variant of the same core allocation-and-coordination problem.

Travel & Loyalty

Airline & Travel Booking Platforms

Many airline and travel booking sites support combining loyalty points/miles with a card for a single booking, and publicly describe holding the card authorization until the points redemption is confirmed — the same “cheapest/most-committed instrument first, hold the flexible instrument until confirmed” sequencing logic discussed in Section 6.

💬
What an interviewer may ask

“How is this different from a marketplace split payment, where money needs to go to multiple sellers instead of coming from multiple payment methods?” Good answer: structurally similar in that both require atomic, multi-party coordination with compensation on partial failure, but a marketplace split additionally has to handle payout timing, seller-side fee deduction, and often regulatory requirements around holding funds in escrow — this tutorial’s problem is entirely about the money coming in from multiple sources, not going out to multiple destinations, which is a meaningfully different (and generally simpler) problem.

17

FAQ, Summary & Key Takeaways

What happens if the customer’s gift card balance changes between the allocation preview and the actual pay-now click?

The preview is purely informational; the real allocation is recomputed and the real balance re-validated at authorization time, so a changed balance simply produces a fresh allocation (or a clear error if the total can no longer be covered) rather than proceeding on stale numbers.

Can a customer split a payment across more than two methods?

Yes — the Allocation Engine and the parallel authorize/capture saga are both designed to handle an arbitrary list of methods, not just two; the priority-ordered allocation loop and the fan-out/fan-in pattern in the Method Router scale naturally to three, four, or more methods in a single order.

How are refunds handled for an order that was originally split across multiple methods?

The Refund and Reversal Service looks up the original per-method capture breakdown from the Ledger and, by default, reverses proportionally back to the same methods that were originally charged — a $60 refund on a $120 order split 30/20/70 across gift card/store credit/card would refund 15/10/35 back to each respective method, unless the business’s refund policy specifies a different rule (some businesses refund to the card first, for instance, to simplify customer support).

What data structure choices matter here, beyond “use Redis”?

The Payment State Store’s per-transaction record is effectively a small state machine object (matching Figure 3’s states) keyed by transaction ID — modeling it explicitly as an enum-driven state object, rather than a loose set of boolean flags, is what makes the recovery sweep in Section 8.3 reliable, since “what state is this transaction in” becomes a single, unambiguous field to query rather than an inferred combination of several flags that could theoretically be inconsistent with each other.

Does every payment method need to support the same authorize-then-capture semantics for this design to work?

No, and this is an important nuance: methods that only support an immediate, all-in-one debit (some gift card processors work this way) are handled by having their adapter simulate the two-phase contract at the application level — the adapter debits immediately but tracks the operation internally as “provisionally authorized” until the whole saga’s other methods also succeed, and if compensation is triggered, the adapter issues a real reversal/refund call to undo that immediate debit. The Orchestrator itself never needs to know which methods are “truly” two-phase underneath and which are simulated — the adapter interface hides that difference completely.

How do you test a system like this thoroughly before launch?

Beyond standard unit and integration tests, the highest-value testing technique here is deliberately injecting failures at every possible position in the saga — failing the first method, the last method, a method in the middle, a capture after successful authorization, and a compensation call itself — using contract-tested fake adapters that simulate each provider’s real failure modes, since production traffic will eventually exercise every one of these paths regardless of how rare they seem in testing.

Key Takeaways

  • Split payments are fundamentally a distributed saga problem: allocation, coordinated authorization, coordinated capture, and automated compensation are the four pieces that must all be designed together.
  • The authorize-then-capture two-phase flow, run in parallel across methods, minimizes both checkout latency and the surface area that compensation logic needs to cover.
  • The Payment State Store is what makes crash recovery and compensation reliable — it must be treated as correctness-critical infrastructure, not a disposable cache.
  • At a million-requests-a-minute platform scale, checkout traffic is a smaller but far higher-stakes slice, and external payment provider rate limits, not internal infrastructure, are usually the real ceiling.
  • Server-side allocation recomputation and live balance re-validation at every saga step close off the main financial manipulation vectors a client-trusting design would otherwise open up.
  • Compensation logic deserves the same engineering rigor and automated test coverage as the happy path — at scale, it is a frequently-exercised path, not a rare edge case.
  • The single anti-pattern to never fall into: charging methods sequentially with no automated, tested reversal plan for the methods that already succeeded.
💭
Final thought

If you take one architectural lesson from this tutorial into your next system design interview or your own production system, let it be this: money moving across multiple independent systems in a single logical transaction always needs an explicit coordinator that treats “undo” as seriously as “do.” A split payment is only as trustworthy as its worst-tested compensation path — everything else in this design, from the two-phase saga, to the state store, to the per-provider circuit breakers, exists in service of making sure that when one piece of a multi-method purchase fails, nothing is ever left half-done. That single discipline, applied consistently across every component described here, is what turns “combine a gift card and a credit card at checkout” from a fragile convenience feature into a piece of core financial infrastructure customers can rely on without a second thought.