system-design62 min read

How to Design an E-Commerce Checkout & Cart System — A Senior+ Guide | Ayodhyya

How to Design an E-Commerce Checkout & Cart System

Building a Production-Grade Shopping Cart & Checkout Pipeline — Cart Management, Payment Processing, Inventory, and Order Fulfillment

Senior+ System Design Guide 10,000+ Words 24 Deep-Dive Sections C# · Mermaid · Real-World Case Studies

1. Introduction & Why Checkout is the Hardest Part of E-Commerce

The e-commerce checkout and cart system is arguably the most complex and business-critical component of any online retail platform. It sits at the exact intersection where browsing turns into revenue — every millisecond of latency, every confusing UI element, every failed payment directly impacts the bottom line. According to Baymard Institute research, the average documented online shopping cart abandonment rate is approximately 70%. This means for every 10 users who add items to their cart, only 3 complete a purchase. While not all of this abandonment is due to technical issues (some users are just browsing or comparing prices), a significant portion is caused by slow checkout pages, unexpected shipping costs, lack of payment options, and system errors during the checkout process.

Building a checkout system is deceptively simple at the surface level: a user adds items, enters shipping information, pays, and receives an order confirmation. But beneath this simple surface lies an extraordinarily complex distributed system. Consider the sheer number of systems that must coordinate for a single checkout: the cart service managing item state, the inventory service reserving stock, the pricing engine calculating discounts and taxes, the shipping service fetching carrier rates, the payment service processing transactions, the order service creating the order record, the notification service sending confirmations, and the analytics service tracking conversion events. Each of these systems must be consistent, performant, and fault-tolerant. A failure in any single system — a payment gateway timeout, an inventory service outage, a tax calculation error — can lose the sale entirely.

Key Insight: The checkout flow is not a simple linear pipeline. It is a complex orchestration of multiple distributed services with strict consistency requirements, compensation logic for partial failures, and real-time external dependencies (payment gateways, tax APIs, shipping calculators). Designing this system requires understanding saga patterns, idempotency, distributed transactions, and graceful degradation — all under the pressure of every second of downtime costing real revenue.

The challenge intensifies at scale. Amazon processes approximately 66,000 orders per hour (roughly 18 orders per second). During peak events like Prime Day or Black Friday, this can spike to 10x or more. At these volumes, the cart and checkout system must handle millions of concurrent sessions, process thousands of payments per second, maintain real-time inventory accuracy across multiple warehouses, and still deliver sub-second response times. The system must also handle edge cases that are unique to e-commerce: flash sales that deplete inventory in seconds, coupon codes that must be validated atomically, cart items that become unavailable mid-checkout, price changes that occur while items are in the cart, and split shipments where items ship from different warehouses.

Real-World Case Studies

Understanding how major e-commerce platforms approach the cart and checkout problem provides practical insights for our design. Each platform has unique constraints and innovations shaped by their specific business requirements and scale:

CompanyScaleKey InnovationTechnical Challenge
Amazon66K orders/hour1-Click ordering, anticipatory shippingSub-100ms checkout for Prime members, real-time inventory across 200+ fulfillment centers
Shopify4.4M merchantsMulti-tenant checkout, Shop PayTenant isolation, per-store pricing rules, 1M+ concurrent checkouts
StripeBillions of transactions/yearIdempotency keys, Radar ML fraud detectionExactly-once payment processing, multi-currency, 135+ currencies
Alibaba122K orders/second (peak Singles Day)Inventory pre-warming, elastic scalingHandling 100x normal traffic in minutes, distributed inventory locks
WalmartBillions in annual e-commerce revenueIn-store + online cart unificationReal-time cross-channel inventory, curbside pickup scheduling

Amazon's 1-Click ordering is a masterclass in checkout optimization. By pre-storing payment and shipping information and using a cryptographic token to authorize purchases, Amazon reduced checkout to a single button press. This seemingly simple feature requires an enormous backend infrastructure: the payment token vault must be PCI-DSS compliant, the inventory check must complete within 50ms, and the fraud detection must run synchronously before the order is confirmed. The result is a checkout experience so frictionless that it reportedly accounts for a significant portion of Amazon's impulse purchases.

Shopify's multi-tenant checkout presents a different but equally challenging problem. Each of Shopify's 4.4 million merchants has their own checkout configuration: custom discount rules, tax settings, shipping zones, payment gateways, and branding. The checkout system must efficiently serve millions of different stores while ensuring that one merchant's configuration changes never affect another's. This requires careful namespace isolation, per-tenant configuration caching, and a checkout rendering engine that can dynamically apply merchant-specific customizations without sacrificing performance.

The core lesson from these case studies is that checkout optimization is not about any single technique — it is about systematically eliminating friction at every step of the funnel while maintaining reliability and security. The system we design in this guide draws from all of these approaches, combining the best practices of industry leaders into a cohesive, production-ready architecture.

2. Functional & Non-Functional Requirements

Functional Requirements

  1. Cart Management: Users can add items (with quantity and variant selection), remove items, update quantities, and view their cart. The cart must support product variants (size, color), gift wrapping options, and custom attributes.
  2. Session Continuity: Anonymous users maintain cart state across sessions using device fingerprinting and cookies. Authenticated users have their cart persisted to their account and synced across devices.
  3. Pricing & Discounts: Real-time price calculation including base price, volume discounts, coupon/promo code application, tax computation, and shipping cost estimation.
  4. Inventory Reservation: When a user enters checkout, inventory is soft-locked with a TTL (typically 10 minutes). If the checkout is abandoned, the lock expires and inventory returns to the available pool.
  5. Checkout Flow: Multi-step checkout supporting address entry with validation, shipping method selection, payment method entry, order review, and order placement.
  6. Payment Processing: Integration with Stripe (and other payment gateways) supporting credit/debit cards, digital wallets (Apple Pay, Google Pay), and buy-now-pay-later (BNPL) options. Must handle 3D Secure authentication.
  7. Order Creation: On successful payment, create the order with a deterministic order ID, capture line items, apply all pricing, and trigger fulfillment workflows.
  8. Cart Abandonment Recovery: Track cart abandonment events and trigger recovery emails with deep links back to the user's cart.
  9. A/B Testing: Support experiment variants for checkout flow modifications (single-page vs multi-step, different payment UIs, etc.).

Non-Functional Requirements

RequirementTargetRationale
Cart API latency (p99)< 100msCart operations are on the critical browsing path
Checkout API latency (p99)< 500msCheckout includes external service calls (payment, tax, shipping)
Availability99.99% (52 min/year downtime)Every minute of downtime directly loses revenue
Concurrent users10M+ simultaneous cartsMust handle peak traffic events
Orders per second10,000+ at peakFlash sales and holiday peaks
Data durability99.999999999% (11 nines)Cart and order data must never be lost
Cart TTL30 days for anonymous, indefinite for authenticatedBalance storage cost with user experience
Inventory reservation TTL10 minutesPrevent hoarding while allowing time for checkout
Availability Target: 99.99% availability means the system can be down for at most 52.6 minutes per year, or about 4.38 minutes per month. For an e-commerce platform processing $1,000/second in revenue, each minute of downtime costs approximately $60,000. The cart and checkout service must be designed with redundancy at every layer — from load balancers to database replicas to payment gateway failover.

3. Capacity Estimation & Scale

Traffic Estimation

Let us estimate the system capacity for a mid-to-large e-commerce platform handling substantial traffic volumes:

MetricDailyPer Second (avg)Per Second (peak)
Page views500M~5,800~17,400 (3x)
Cart operations (add/update/remove)100M~1,157~3,470
Checkout initiations30M~347~1,041
Orders completed15M~174~522
Payment transactions16M (includes retries)~185~555

Storage Estimation

Data TypeSize per RecordDaily VolumeDaily StorageMonthly
Cart documents (Redis)~2KB10M active carts20 GB600 GB
Cart snapshots (DB)~1KB30M (one per checkout)30 GB900 GB
Orders~4KB15M60 GB1.8 TB
Payment records~2KB16M32 GB960 GB
Inventory reservations~200B30M (short-lived)6 GB~6 GB (TTL-managed)

Bandwidth Estimation

The cart API serving 3,470 peak requests per second with average response sizes of 5KB requires approximately 17 MB/s of outbound bandwidth for cart responses alone. The checkout API at 1,041 peak requests per second with 20KB average response sizes (including shipping options and pricing breakdowns) requires approximately 21 MB/s. Total system bandwidth including internal service-to-service communication is estimated at 200-500 MB/s during peak. This is well within the capacity of modern cloud networking infrastructure and does not represent a bottleneck.

Key Insight: Notice that the ratio of cart operations to completed orders is roughly 7:1. This means for every order placed, there are approximately 6 cart operations that do not result in a purchase. Understanding this ratio is critical for capacity planning — the system must be optimized for the much larger volume of cart operations, not just order creation.

4. High-Level Architecture Overview

The e-commerce checkout and cart system follows a microservices architecture with clear domain boundaries. Each service owns its data and communicates with other services through well-defined APIs and asynchronous events. This architecture allows independent scaling (the cart service can scale separately from the payment service), independent deployment, and fault isolation (a failure in the recommendation service should never prevent checkout).

graph TB Client[Client App
Web/Mobile] --> CDN[CDN / Edge] CDN --> Gateway[API Gateway
Rate Limiting + Auth] Gateway --> CartService[Cart Service] Gateway --> CheckoutService[Checkout Service] Gateway --> OrderService[Order Service] CartService --> Redis[(Redis Cluster
Cart Cache)] CartService --> CartDB[(PostgreSQL
Cart Persistence)] CheckoutService --> InventorySvc[Inventory Service] CheckoutService --> PricingSvc[Pricing Engine] CheckoutService --> ShippingSvc[Shipping Service] CheckoutService --> PaymentSvc[Payment Service] CheckoutService --> TaxSvc[Tax Calculation Service] PaymentSvc --> Stripe[Stripe API] PaymentSvc --> PaymentDB[(PostgreSQL
Payment Ledger)] OrderService --> OrderDB[(PostgreSQL
Orders)] OrderService --> EventBus[Event Bus
Kafka] EventBus --> NotificationSvc[Notification Service] EventBus --> FulfillmentSvc[Fulfillment Service] EventBus --> AnalyticsSvc[Analytics Service] EventBus --> AbandonmentSvc[Cart Abandonment Tracker] InventorySvc --> InventoryDB[(PostgreSQL
Inventory)] InventorySvc --> InventoryCache[(Redis
Inventory Cache)]

Service Responsibilities

ServiceResponsibilityStorageScale Factor
Cart ServiceCRUD operations on shopping carts, session managementRedis (hot) + PostgreSQL (cold)Cart count
Checkout ServiceOrchestrates the checkout flow across all servicesStateless (uses Redis for checkout state)Checkout initiations
Inventory ServiceStock levels, reservation management, warehouse allocationPostgreSQL + Redis cacheSKU count x warehouse count
Pricing EnginePrice calculation, discounts, coupons, promotionsRedis (rules cache) + PostgreSQLPricing rule complexity
Tax ServiceTax calculation for jurisdictions, tax-exempt handlingTax rules cache + external API (Avalara/TaxJar)Transaction count
Shipping ServiceCarrier rate shopping, delivery estimationRate cache + external carrier APIsCheckout initiations
Payment ServicePayment authorization, capture, refunds, vaultingPostgreSQL (encrypted)Payment count
Order ServiceOrder lifecycle management, status trackingPostgreSQL + event storeOrder count
Notification ServiceEmail, SMS, push notifications for order eventsMessage queue + template storeOrder events
Abandonment ServiceCart abandonment detection and recovery email triggersClickHouse (analytics) + PostgreSQLAbandoned cart count

Communication Patterns

The architecture uses two primary communication patterns. Synchronous communication (gRPC or REST) is used for the checkout flow where the checkout orchestrator needs immediate responses from each service (inventory check, price calculation, shipping rates, tax calculation). These calls are bounded by strict timeouts and have circuit breaker protection. Asynchronous communication (Kafka events) is used for side effects that do not need to block the checkout response: sending confirmation emails, updating analytics, triggering fulfillment, and tracking cart abandonment. This hybrid approach ensures the checkout response is returned to the user as quickly as possible while all downstream processing happens reliably in the background.

Architecture Principle: The checkout orchestrator performs all synchronous service calls in parallel (inventory check + price calculation + shipping rates + tax calculation) and aggregates results before returning. This parallel approach reduces checkout latency from the sum of all service latencies to the maximum of all service latencies — typically reducing from 800ms to 200ms.

5. Data Model & Storage Schema

The data model must support fast cart reads and writes, efficient inventory reservations, and durable order records. We use a polyglot persistence approach: Redis for hot cart data, PostgreSQL for durable records, and Kafka for event sourcing.

Cart Schema (PostgreSQL)

SQL
CREATE TABLE carts (
    cart_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID REFERENCES users(user_id),
    session_id      VARCHAR(128),
    status          VARCHAR(20) NOT NULL DEFAULT 'active',
    currency        CHAR(3) NOT NULL DEFAULT 'USD',
    subtotal        DECIMAL(12,2) DEFAULT 0,
    discount_total  DECIMAL(12,2) DEFAULT 0,
    tax_total       DECIMAL(12,2) DEFAULT 0,
    shipping_total  DECIMAL(12,2) DEFAULT 0,
    grand_total     DECIMAL(12,2) DEFAULT 0,
    coupon_code     VARCHAR(50),
    metadata        JSONB DEFAULT '{}',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at      TIMESTAMPTZ,
    CONSTRAINT chk_status CHECK (status IN ('active','merged','converted','abandoned','expired'))
);

CREATE TABLE cart_items (
    item_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    cart_id         UUID NOT NULL REFERENCES carts(cart_id) ON DELETE CASCADE,
    product_id      UUID NOT NULL,
    variant_id      UUID,
    sku             VARCHAR(100) NOT NULL,
    name            VARCHAR(500) NOT NULL,
    quantity        INT NOT NULL CHECK (quantity > 0),
    unit_price      DECIMAL(12,2) NOT NULL,
    line_total      DECIMAL(12,2) NOT NULL,
    weight_grams    INT,
    image_url       VARCHAR(1000),
    metadata        JSONB DEFAULT '{}',
    added_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(cart_id, product_id, variant_id)
);

CREATE INDEX idx_carts_user ON carts(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_carts_session ON carts(session_id);
CREATE INDEX idx_carts_expires ON carts(expires_at) WHERE expires_at IS NOT NULL;
CREATE INDEX idx_cart_items_cart ON cart_items(cart_id);

Order Schema (PostgreSQL)

SQL
CREATE TABLE orders (
    order_id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_number        VARCHAR(20) UNIQUE NOT NULL,
    user_id             UUID REFERENCES users(user_id),
    status              VARCHAR(30) NOT NULL DEFAULT 'pending',
    currency            CHAR(3) NOT NULL,
    subtotal            DECIMAL(12,2) NOT NULL,
    discount_total      DECIMAL(12,2) DEFAULT 0,
    tax_total           DECIMAL(12,2) NOT NULL,
    shipping_total      DECIMAL(12,2) NOT NULL,
    grand_total         DECIMAL(12,2) NOT NULL,
    shipping_address    JSONB NOT NULL,
    billing_address     JSONB NOT NULL,
    shipping_method     VARCHAR(50) NOT NULL,
    payment_method      VARCHAR(50) NOT NULL,
    payment_intent_id   VARCHAR(100),
    idempotency_key     VARCHAR(100) UNIQUE,
    notes               TEXT,
    placed_at           TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    paid_at             TIMESTAMPTZ,
    shipped_at          TIMESTAMPTZ,
    delivered_at        TIMESTAMPTZ,
    cancelled_at        TIMESTAMPTZ,
    metadata            JSONB DEFAULT '{}'
);

CREATE TABLE order_items (
    order_item_id   UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id        UUID NOT NULL REFERENCES orders(order_id),
    product_id      UUID NOT NULL,
    variant_id      UUID,
    sku             VARCHAR(100) NOT NULL,
    name            VARCHAR(500) NOT NULL,
    quantity        INT NOT NULL,
    unit_price      DECIMAL(12,2) NOT NULL,
    discount_amount DECIMAL(12,2) DEFAULT 0,
    tax_amount      DECIMAL(12,2) NOT NULL,
    line_total      DECIMAL(12,2) NOT NULL
);

CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_placed ON orders(placed_at DESC);
CREATE INDEX idx_orders_idempotency ON orders(idempotency_key);

Inventory Reservation Schema

SQL
CREATE TABLE inventory_reservations (
    reservation_id  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id        UUID REFERENCES orders(order_id),
    sku             VARCHAR(100) NOT NULL,
    warehouse_id    UUID NOT NULL,
    quantity        INT NOT NULL,
    status          VARCHAR(20) NOT NULL DEFAULT 'reserved',
    reserved_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at      TIMESTAMPTZ NOT NULL,
    released_at     TIMESTAMPTZ,
    UNIQUE(sku, warehouse_id, order_id)
);

CREATE INDEX idx_reservations_expiry ON inventory_reservations(expires_at)
    WHERE status = 'reserved';
CREATE INDEX idx_reservations_sku ON inventory_reservations(sku)
    WHERE status = 'reserved';
Schema Design Note: We store denormalized product data (name, price, image_url) in cart_items and order_items. This is intentional — product details can change after a user adds an item to their cart, but the cart must preserve what the user saw when they added it. This "snapshot at time of add" pattern prevents confusing price changes in the cart and provides an audit trail for order disputes.

6. API Design

The API layer follows REST conventions with consistent resource naming, proper HTTP semantics, and comprehensive error responses. All endpoints require authentication (except cart operations for anonymous users which use session tokens). The API is versioned (v1) and follows a resource-oriented design pattern.

Cart APIs

MethodEndpointDescriptionAuth
GET/api/v1/carts/{cartId}Get cart with items and totalsSession or User
POST/api/v1/carts/{cartId}/itemsAdd item to cartSession or User
PATCH/api/v1/carts/{cartId}/items/{itemId}Update item quantitySession or User
DELETE/api/v1/carts/{cartId}/items/{itemId}Remove item from cartSession or User
POST/api/v1/carts/{cartId}/couponApply coupon codeSession or User
DELETE/api/v1/carts/{cartId}/couponRemove coupon codeSession or User
POST/api/v1/carts/mergeMerge anonymous cart into user cartUser

Checkout APIs

MethodEndpointDescription
POST/api/v1/checkout/initializeStart checkout, reserve inventory, get pricing
PUT/api/v1/checkout/{checkoutId}/addressSet shipping address, get shipping rates
PUT/api/v1/checkout/{checkoutId}/shippingSelect shipping method
POST/api/v1/checkout/{checkoutId}/paymentProcess payment and place order
GET/api/v1/checkout/{checkoutId}/summaryGet full checkout summary

Order APIs

MethodEndpointDescription
GET/api/v1/orders/{orderId}Get order details
GET/api/v1/ordersList user's orders (paginated)
POST/api/v1/orders/{orderId}/cancelCancel an order
POST/api/v1/orders/{orderId}/refundRequest a refund

Cart Add Item — Request and Response

JSON
// POST /api/v1/carts/{cartId}/items
{
    "product_id": "prod_8xk2m9",
    "variant_id": "var_blue_large",
    "sku": "TSHIRT-BLU-L-001",
    "quantity": 2,
    "metadata": {
        "gift_wrap": true,
        "gift_message": "Happy Birthday!"
    }
}

// Response 201 Created
{
    "cart_id": "cart_3f8a2b",
    "item": {
        "item_id": "item_9z7c1d",
        "product_id": "prod_8xk2m9",
        "variant_id": "var_blue_large",
        "sku": "TSHIRT-BLU-L-001",
        "name": "Premium Cotton T-Shirt — Blue, Large",
        "quantity": 2,
        "unit_price": 29.99,
        "line_total": 59.98,
        "image_url": "https://cdn.example.com/products/tshirt-blue.jpg"
    },
    "totals": {
        "item_count": 3,
        "subtotal": 89.97,
        "estimated_tax": 7.20,
        "estimated_total": 97.17
    }
}
API Design Principle: Every cart mutation response includes the updated totals. This avoids the client needing a separate call to get the updated cart state after each operation. The server recalculates totals on every mutation and returns them inline, keeping the client always in sync without additional round trips.

7. Cart Service Design

The Cart Service is the highest-traffic service in the checkout ecosystem. It must handle millions of concurrent carts with sub-100ms latency for all CRUD operations. The service is designed as a stateless compute layer with a two-tier storage backend: Redis for hot active cart data and PostgreSQL for durable persistence.

Cart Operations Flow

sequenceDiagram participant Client participant CartAPI as Cart Service API participant Redis participant PostgreSQL participant Kafka Client->>CartAPI: POST /carts/{id}/items {product, qty} CartAPI->>Redis: GET cart:{cartId} alt Cart found in Redis Redis-->>CartAPI: Cart data else Cache miss CartAPI->>PostgreSQL: SELECT cart + items PostgreSQL-->>CartAPI: Cart data CartAPI->>Redis: SET cart:{cartId} (TTL 30min) end CartAPI->>CartAPI: Validate product exists and in stock CartAPI->>CartAPI: Add item, recalculate totals CartAPI->>Redis: SET cart:{cartId} (updated, TTL 30min) CartAPI->>PostgreSQL: UPSERT cart + items (async) CartAPI->>Kafka: Emit CartItemAdded event CartAPI-->>Client: 201 Created {item, totals}

Cart Service Implementation

C#
public class CartService : ICartService
{
    private readonly IRedisCache _cache;
    private readonly ICartRepository _repository;
    private readonly IEventPublisher _events;
    private readonly IProductClient _products;
    private readonly ITotalsCalculator _totals;

    public async Task<CartResponse> AddItemAsync(
        Guid cartId, AddItemRequest request)
    {
        // 1. Load cart (Redis-first strategy)
        var cart = await LoadCartAsync(cartId);
        if (cart == null)
        {
            cart = await _repository.CreateCartAsync(
                new Cart { CartId = cartId });
        }

        // 2. Validate product and get current pricing
        var product = await _products
            .GetProductAsync(request.ProductId);
        if (product == null)
            throw new NotFoundException(
                $"Product {request.ProductId} not found");
        if (!product.IsAvailable)
            throw new ConflictException(
                "Product is no longer available");

        // 3. Check if item already exists in cart
        var existing = cart.Items.FirstOrDefault(i =>
            i.ProductId == request.ProductId &&
            i.VariantId == request.VariantId);

        if (existing != null)
        {
            existing.Quantity += request.Quantity;
            existing.LineTotal =
                existing.Quantity * existing.UnitPrice;
        }
        else
        {
            cart.Items.Add(new CartItem
            {
                ItemId = Guid.NewGuid(),
                CartId = cartId,
                ProductId = request.ProductId,
                VariantId = request.VariantId,
                Sku = product.Sku,
                Name = product.Name,
                Quantity = request.Quantity,
                UnitPrice = product.Price,
                LineTotal = request.Quantity * product.Price,
                WeightGrams = product.WeightGrams,
                ImageUrl = product.ImageUrl,
                Metadata = request.Metadata ?? new(),
                AddedAt = DateTime.UtcNow
            });
        }

        // 4. Recalculate all totals
        cart.Totals = await _totals
            .CalculateCartTotalsAsync(cart);

        // 5. Persist and cache
        await _cache.SetCartAsync(cart,
            TimeSpan.FromMinutes(30));
        _ = _repository.UpsertCartAsync(cart); // fire-and-forget

        // 6. Publish event
        await _events.PublishAsync(new CartItemAdded
        {
            CartId = cartId,
            ProductId = request.ProductId,
            Quantity = request.Quantity,
            Timestamp = DateTime.UtcNow
        });

        return MapToResponse(cart);
    }

    private async Task<Cart?> LoadCartAsync(Guid cartId)
    {
        var cached = await _cache.GetCartAsync(cartId);
        if (cached != null) return cached;

        var cart = await _repository
            .GetCartWithItemsAsync(cartId);
        if (cart != null)
        {
            await _cache.SetCartAsync(cart,
                TimeSpan.FromMinutes(30));
        }
        return cart;
    }
}

Cart Totals Calculation

The totals calculator is a pure function that takes the cart state and produces a complete pricing breakdown. It handles the cascading nature of e-commerce pricing: line totals feed into the subtotal, discounts reduce the subtotal, taxes are calculated on the discounted amount, and shipping is added last. This calculation must be deterministic — the same cart state must always produce the same totals, regardless of when or where it is calculated.

C#
public class TotalsCalculator : ITotalsCalculator
{
    private readonly IPricingEngine _pricing;
    private readonly ITaxService _tax;
    private readonly IShippingService _shipping;

    public async Task<CartTotals> CalculateCartTotalsAsync(
        Cart cart)
    {
        // 1. Calculate line totals (quantity x unit_price)
        foreach (var item in cart.Items)
        {
            item.LineTotal = item.Quantity * item.UnitPrice;
        }

        // 2. Subtotal = sum of all line totals
        var subtotal = cart.Items.Sum(i => i.LineTotal);

        // 3. Apply discounts and promotions
        var discountResult = await _pricing
            .CalculateDiscountsAsync(cart);
        var discountTotal = discountResult.TotalDiscount;
        var discountedSubtotal = subtotal - discountTotal;

        // 4. Calculate tax on discounted amount
        var taxResult = await _tax.CalculateTaxAsync(
            discountedSubtotal, cart.ShippingAddress,
            cart.Items);
        var taxTotal = taxResult.TotalTax;

        // 5. Shipping is calculated separately
        var shippingTotal = cart.ShippingMethod != null
            ? await _shipping.GetRateAsync(
                cart.ShippingMethod, cart)
            : 0m;

        // 6. Grand total
        var grandTotal =
            discountedSubtotal + taxTotal + shippingTotal;

        return new CartTotals
        {
            Subtotal = subtotal,
            DiscountTotal = discountTotal,
            DiscountDetails = discountResult.AppliedDiscounts,
            TaxTotal = taxTotal,
            TaxDetails = taxResult.TaxBreakdown,
            ShippingTotal = shippingTotal,
            GrandTotal = grandTotal,
            Currency = cart.Currency
        };
    }
}

8. Session Management & Anonymous vs Authenticated Cart

One of the most nuanced challenges in e-commerce cart design is managing the transition between anonymous and authenticated states. A user browsing without logging in has an anonymous cart identified by a session cookie. Once they log in, that anonymous cart must be seamlessly merged with any existing authenticated cart. This transition must be lossless (no items disappear), conflict-free (duplicates are handled gracefully), and fast (the user should not notice the merge happening).

Session Architecture

stateDiagram-v2 [*] --> AnonymousCart: User visits site AnonymousCart --> AnonymousCart: Add/Remove items AnonymousCart --> Merging: User logs in Merging --> AuthenticatedCart: Merge successful state Merging { [*] --> CheckExisting CheckExisting --> MergeItems: User has existing cart CheckExisting --> AdoptCart: No existing cart MergeItems --> ResolveConflicts: Duplicates found MergeItems --> AdoptCart: No conflicts ResolveConflicts --> AdoptCart: Conflicts resolved } AuthenticatedCart --> AuthenticatedCart: Add/Remove items AuthenticatedCart --> ConvertedOrder: Checkout completed AuthenticatedCart --> AbandonedCart: No activity 30min AbandonedCart --> RecoveryEmail: Recovery trigger RecoveredCart --> AuthenticatedCart: User returns via link

Cart Merge Strategy

C#
public class CartMergeService : ICartMergeService
{
    public async Task<Cart> MergeCartsAsync(
        Guid anonymousCartId, Guid userId)
    {
        var userCart = await _cartRepo
            .GetActiveCartForUserAsync(userId);
        var anonCart = await _cartRepo
            .GetCartWithItemsAsync(anonymousCartId);

        if (anonCart == null || !anonCart.Items.Any())
            return userCart ?? await CreateCartForUser(userId);

        if (userCart == null)
        {
            // No existing user cart — simply adopt the anon cart
            anonCart.UserId = userId;
            anonCart.SessionId = null;
            await _cartRepo.UpdateCartAsync(anonCart);
            return anonCart;
        }

        // Both carts have items — merge them
        foreach (var anonItem in anonCart.Items)
        {
            var existingItem = userCart.Items.FirstOrDefault(i =>
                i.ProductId == anonItem.ProductId &&
                i.VariantId == anonItem.VariantId);

            if (existingItem != null)
            {
                // Sum quantities, respecting max per customer
                var maxQty = await GetMaxQuantityAsync(
                    anonItem.ProductId);
                existingItem.Quantity = Math.Min(
                    existingItem.Quantity + anonItem.Quantity,
                    maxQty);
                existingItem.LineTotal =
                    existingItem.Quantity * existingItem.UnitPrice;
            }
            else
            {
                // Item only in anonymous cart — add to user cart
                anonItem.CartId = userCart.CartId;
                anonItem.ItemId = Guid.NewGuid();
                userCart.Items.Add(anonItem);
            }
        }

        // Preserve the most recent coupon from either cart
        if (!string.IsNullOrEmpty(anonCart.CouponCode) &&
            string.IsNullOrEmpty(userCart.CouponCode))
        {
            userCart.CouponCode = anonCart.CouponCode;
        }

        // Recalculate totals and persist
        userCart.Totals = await _totals
            .CalculateCartTotalsAsync(userCart);
        await _cartRepo.UpdateCartAsync(userCart);

        // Mark anonymous cart as merged
        anonCart.Status = CartStatus.Merged;
        await _cartRepo.UpdateCartAsync(anonCart);

        // Update cache
        await _cache.SetCartAsync(userCart,
            TimeSpan.FromMinutes(30));
        await _cache.RemoveCartAsync(anonymousCartId);

        return userCart;
    }
}

Session Token Design

The session token for anonymous users is a cryptographically signed JWT containing the cart_id, a creation timestamp, and an HMAC signature. The token is stored in an HttpOnly, Secure, SameSite=Lax cookie with a 30-day expiry. The signature prevents tampering — a user cannot fabricate a cart_id to access another user's cart. The session token maps to a cart_id via a server-side lookup (Redis hash), providing a layer of indirection that allows cart reassignment during merge.

Security Warning: Never store cart contents directly in the client-side cookie. Even encrypted cookies can be decrypted if the encryption key is compromised. Instead, store only the session identifier and look up cart contents server-side. This limits exposure — a compromised session token grants access to one cart, but does not reveal product details, pricing, or other sensitive data to the client.

9. Cart Persistence — Redis + Database Strategy

The cart persistence layer uses a write-through caching strategy with Redis as the primary read store and PostgreSQL as the durable write store. This approach ensures that cart reads (which happen far more frequently than writes) are served from Redis with sub-millisecond latency, while PostgreSQL guarantees durability in case of Redis failure.

Cache Strategy

OperationRedisPostgreSQLConsistency
Read cartPrimary (TTL 30 min)Fallback on cache missEventual (acceptable for cart)
Add/remove itemUpdate + extend TTLAsync upsert (fire-and-forget)Eventual (Redis is source of truth)
Checkout initializeRead from cacheSync read for durabilityStrong (transactional)
Cart abandoned/expiredDelete from cacheUpdate status to expiredEventual

Redis Data Structure

Carts are stored in Redis as serialized JSON blobs under the key pattern cart:{cartId}. This is simpler than using Redis Hash structures and allows atomic read-modify-write operations with a distributed lock. The lock uses the Redis SET NX EX pattern with a 5-second TTL to prevent race conditions when multiple requests modify the same cart simultaneously.

C#
public class RedisCartCache : IRedisCache
{
    private readonly IConnectionMultiplexer _redis;
    private const string CartKeyPrefix = "cart:";
    private const string LockKeyPrefix = "lock:cart:";
    private static readonly TimeSpan CartTTL =
        TimeSpan.FromMinutes(30);
    private static readonly TimeSpan LockTTL =
        TimeSpan.FromSeconds(5);

    public async Task<Cart?> GetCartAsync(Guid cartId)
    {
        var db = _redis.GetDatabase();
        var key = $"{CartKeyPrefix}{cartId}";
        var json = await db.StringGetAsync(key);
        if (json.IsNullOrEmpty) return null;
        return JsonSerializer.Deserialize<Cart>(json!);
    }

    public async Task SetCartAsync(Cart cart,
        TimeSpan? ttl = null)
    {
        var db = _redis.GetDatabase();
        var key = $"{CartKeyPrefix}{cart.CartId}";
        var json = JsonSerializer.Serialize(cart);
        await db.StringSetAsync(key, json, ttl ?? CartTTL);
    }

    public async Task<bool> AcquireCartLockAsync(
        Guid cartId)
    {
        var db = _redis.GetDatabase();
        var lockKey = $"{LockKeyPrefix}{cartId}";
        return await db.StringSetAsync(
            lockKey, "1", LockTTL,
            When.NotExists);
    }

    public async Task ReleaseCartLockAsync(Guid cartId)
    {
        var db = _redis.GetDatabase();
        var lockKey = $"{LockKeyPrefix}{cartId}";
        await db.KeyDeleteAsync(lockKey);
    }
}

Redis Cluster Sharding

At scale (10M+ active carts), a single Redis instance cannot hold all cart data. We use Redis Cluster with hash slot-based sharding. The cart_id is used as the hash tag key, ensuring all operations for a given cart hit the same shard. With 16,384 hash slots distributed across 6 Redis masters (with replicas), each master handles approximately 1.67M slots. The average cart (2KB) means each master stores approximately 3.3GB of cart data — well within Redis in-memory capacity.

Cache Eviction Strategy: Anonymous carts use a 30-minute Redis TTL, with the TTL extended on every read or write. This means actively-shopped carts stay in Redis indefinitely (until the user stops interacting), while truly abandoned carts naturally expire. Authenticated carts have no TTL and are always persisted to PostgreSQL. This hybrid TTL approach reduces Redis memory usage by automatically cleaning up abandoned anonymous carts while preserving authenticated user data.

10. Inventory Reservation & Soft Lock with TTL

Inventory reservation is one of the most critical components of the checkout system. When a user begins checkout, the system must ensure the items in their cart are actually available and prevent other users from purchasing the same items simultaneously. This is achieved through a "soft lock" mechanism: inventory is temporarily reserved with a time-to-live (TTL), and if the checkout is not completed within the TTL window, the reservation expires and the inventory becomes available again.

Reservation Lifecycle

stateDiagram-v2 [*] --> Available: Initial state Available --> Reserved: User starts checkout Reserved --> Committed: Payment successful Reserved --> Available: TTL expires (10 min) Reserved --> Available: User cancels checkout Committed --> Shipped: Fulfillment starts Shipped --> Delivered: Delivery confirmed Committed --> Refunded: Full refund processed Committed --> PartialRefund: Partial refund processed Refunded --> Available: Inventory restocked

Inventory Service Implementation

C#
public class InventoryService : IInventoryService
{
    private readonly IInventoryRepository _repo;
    private readonly IDistributedLock _locks;
    private readonly IRedisCache _cache;
    private readonly TimeSpan ReservationTTL =
        TimeSpan.FromMinutes(10);

    public async Task<ReservationResult> ReserveInventoryAsync(
        Guid orderId, List<ReservationRequest> items)
    {
        var results = new List<ItemReservationResult>();

        // Sort items by SKU for deterministic lock ordering
        // (prevents deadlocks)
        items = items.OrderBy(i => i.Sku).ToList();

        foreach (var item in items)
        {
            // Acquire per-SKU distributed lock
            var lockKey = $"inventory:{item.Sku}";
            var acquired = await _locks.AcquireAsync(
                lockKey, TimeSpan.FromSeconds(5));

            if (!acquired)
            {
                results.Add(new ItemReservationResult
                {
                    Sku = item.Sku,
                    Success = false,
                    Reason = "System busy, please retry"
                });
                continue;
            }

            try
            {
                // Check available quantity
                var available = await GetAvailableQuantityAsync(
                    item.Sku, item.WarehouseId);

                if (available < item.Quantity)
                {
                    results.Add(new ItemReservationResult
                    {
                        Sku = item.Sku,
                        Success = false,
                        Reason = available == 0
                            ? "Out of stock"
                            : $"Only {available} available"
                    });
                    continue;
                }

                // Create reservation with TTL
                var reservation = new InventoryReservation
                {
                    ReservationId = Guid.NewGuid(),
                    OrderId = orderId,
                    Sku = item.Sku,
                    WarehouseId = item.WarehouseId,
                    Quantity = item.Quantity,
                    Status = "reserved",
                    ReservedAt = DateTime.UtcNow,
                    ExpiresAt = DateTime.UtcNow.Add(ReservationTTL)
                };

                await _repo.CreateReservationAsync(reservation);

                // Update available quantity in cache
                await _cache.IncrementInventoryAsync(
                    item.Sku, -item.Quantity);

                results.Add(new ItemReservationResult
                {
                    Sku = item.Sku,
                    Success = true,
                    ReservationId = reservation.ReservationId,
                    ExpiresAt = reservation.ExpiresAt
                });
            }
            finally
            {
                await _locks.ReleaseAsync(lockKey);
            }
        }

        var allSucceeded = results.All(r => r.Success);
        return new ReservationResult
        {
            OrderId = orderId,
            AllReserved = allSucceeded,
            Items = results,
            ExpiresAt = DateTime.UtcNow.Add(ReservationTTL)
        };
    }

    public async Task<int> GetAvailableQuantityAsync(
        string sku, Guid warehouseId)
    {
        // Try cache first
        var cached = await _cache
            .GetInventoryCountAsync(sku, warehouseId);
        if (cached.HasValue) return cached.Value;

        var reserved = await _repo
            .GetActiveReservationCountAsync(sku, warehouseId);
        var totalStock = await _repo
            .GetTotalStockAsync(sku, warehouseId);
        var available = totalStock - reserved;

        await _cache.SetInventoryCountAsync(
            sku, warehouseId, available,
            TimeSpan.FromSeconds(30));

        return available;
    }
}

Reservation Expiration Worker

A background worker periodically scans for expired reservations and releases them back into the available inventory pool. This worker runs every 30 seconds and uses a batch query to find all reservations where expires_at < NOW() AND status = 'reserved'. Expired reservations are updated to status = 'released', and the corresponding inventory counts are decremented in the cache.

C#
public class ReservationExpirationWorker : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<ReservationExpirationWorker> _log;

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                using var scope = _scopeFactory.CreateScope();
                var repo = scope.ServiceProvider
                    .GetRequiredService<IInventoryRepository>();
                var cache = scope.ServiceProvider
                    .GetRequiredService<IRedisCache>();

                var expired = await repo
                    .GetExpiredReservationsAsync(
                        batchSize: 500);

                foreach (var reservation in expired)
                {
                    reservation.Status = "released";
                    reservation.ReleasedAt = DateTime.UtcNow;
                    await repo.UpdateReservationAsync(
                        reservation);

                    // Return inventory to available pool
                    await cache.IncrementInventoryAsync(
                        reservation.Sku,
                        reservation.Quantity);

                    _log.LogInformation(
                        "Released expired reservation {Id} " +
                        "for {Sku} qty {Qty}",
                        reservation.ReservationId,
                        reservation.Sku,
                        reservation.Quantity);
                }

                if (expired.Any())
                {
                    _log.LogInformation(
                        "Released {Count} expired reservations",
                        expired.Count);
                }
            }
            catch (Exception ex)
            {
                _log.LogError(ex,
                    "Error processing expired reservations");
            }

            await Task.Delay(
                TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}
Inventory Race Condition: Without proper locking, two concurrent checkout requests could both read the same available quantity (e.g., 1 item left), both pass the availability check, and both create reservations — resulting in an oversell. The per-SKU distributed lock prevents this by serializing all inventory operations for the same SKU. However, this creates a bottleneck during flash sales. For extreme scale, consider optimistic concurrency with a version column on the inventory table and retry logic for conflicting writes.

11. Pricing Engine — Discounts, Coupons & Tax Calculation

The pricing engine is responsible for the complete price calculation pipeline: base prices, volume discounts, promotional pricing, coupon/promo codes, gift cards, and tax computation. This pipeline must be deterministic (same inputs always produce same outputs), auditable (every price change must have a reason), and performant (pricing calculation should add less than 50ms to checkout latency).

Pricing Pipeline

graph LR A[Base Price] --> B[Product Discounts] B --> C[Cart-Level Promotions] C --> D[Coupon/Promo Code] D --> E[Gift Card Balance] E --> F[Tax Calculation] F --> G[Final Price] H[Volume Rules] --> B I[Flash Sale] --> B J[Buy X Get Y] --> C K[Percentage Off] --> D L[Fixed Amount] --> D

Price Calculation Implementation

C#
public class PricingEngine : IPricingEngine
{
    private readonly IDiscountRepository _discounts;
    private readonly ICouponRepository _coupons;
    private readonly IRuleEngine _rules;

    public async Task<PricingResult> CalculatePriceAsync(
        Cart cart)
    {
        var result = new PricingResult();

        // Step 1: Base prices (already set on cart items)
        result.BaseTotal = cart.Items
            .Sum(i => i.Quantity * i.UnitPrice);

        // Step 2: Product-level discounts
        foreach (var item in cart.Items)
        {
            var productDiscounts = await _discounts
                .GetActiveDiscountsForProductAsync(
                    item.ProductId);

            foreach (var discount in productDiscounts)
            {
                if (await _rules.EvaluateAsync(
                    discount.Rules, cart))
                {
                    var discountAmount = discount.Type
                        == DiscountType.Percentage
                        ? item.LineTotal * discount.Value / 100
                        : Math.Min(discount.Value,
                            item.LineTotal);

                    result.AppliedDiscounts.Add(
                        new AppliedDiscount
                    {
                        Code = discount.Code,
                        Description = discount.Description,
                        Amount = discountAmount,
                        AppliesTo = DiscountScope.Product,
                        ProductId = item.ProductId
                    });
                }
            }
        }

        // Step 3: Cart-level promotions
        var cartPromos = await _discounts
            .GetActiveCartPromotionsAsync();
        foreach (var promo in cartPromos)
        {
            if (await _rules.EvaluateAsync(
                promo.Rules, cart))
            {
                var discountAmount = CalculatePromoDiscount(
                    promo, cart);
                result.AppliedDiscounts.Add(
                    new AppliedDiscount
                {
                    Code = promo.Code,
                    Description = promo.Description,
                    Amount = discountAmount,
                    AppliesTo = DiscountScope.Cart
                });
            }
        }

        // Step 4: Coupon code
        if (!string.IsNullOrEmpty(cart.CouponCode))
        {
            var coupon = await _coupons
                .ValidateCouponAsync(
                    cart.CouponCode, cart.UserId);

            if (coupon != null)
            {
                var couponDiscount = coupon.Type
                    == DiscountType.Percentage
                    ? (result.BaseTotal -
                        result.ProductDiscountTotal) *
                        coupon.Value / 100
                    : coupon.Value;

                result.AppliedDiscounts.Add(
                    new AppliedDiscount
                {
                    Code = coupon.Code,
                    Description =
                        $"{coupon.Value}% off",
                    Amount = couponDiscount,
                    AppliesTo = DiscountScope.Order
                });

                await _coupons
                    .IncrementUsageAsync(coupon.CouponId);
            }
        }

        result.TotalDiscount = result.AppliedDiscounts
            .Sum(d => d.Amount);
        result.DiscountedSubtotal =
            result.BaseTotal - result.TotalDiscount;

        return result;
    }
}

public class TaxService : ITaxService
{
    private readonly ITaxProvider _provider;

    public async Task<TaxResult> CalculateTaxAsync(
        decimal amount, Address address,
        List<CartItem> items)
    {
        var taxRequest = new TaxRequest
        {
            TransactionDate = DateTime.UtcNow,
            Addresses = new[] { MapAddress(address) },
            Lines = items.Select(i => new TaxLine
            {
                ItemCode = i.Sku,
                Quantity = i.Quantity,
                UnitPrice = i.UnitPrice,
                LineTotal = i.LineTotal,
                TaxCode = GetTaxCode(i.ProductId)
            }).ToList()
        };

        var response = await _provider
            .CalculateTaxAsync(taxRequest);

        return new TaxResult
        {
            TotalTax = response.TotalTax,
            TaxBreakdown = response.TaxLines
                .Select(l => new TaxBreakdownItem
            {
                Jurisdiction = l.JurisdictionName,
                Rate = l.Rate,
                TaxableAmount = l.TaxableAmount,
                TaxAmount = l.TaxAmount
            }).ToList()
        };
    }
}

Coupon Validation Rules

RuleDescriptionError Response
ExistenceCoupon code must exist and be active"Invalid coupon code"
ExpirationCoupon must not be expired"This coupon has expired"
Usage limitTotal uses must not exceed max_redemptions"This coupon has reached its usage limit"
Per-user limitUser uses must not exceed per_user_limit"You have already used this coupon"
Minimum orderCart subtotal must meet minimum_order_amount"Minimum order of $X required"
Product restrictionAt least one eligible product in cart"This coupon is not applicable to your cart"
StackabilityCoupon must not conflict with existing discounts"This coupon cannot be combined with other offers"
Pricing Consistency: Tax calculation must use the discounted subtotal, not the original subtotal. Applying tax to the pre-discount amount would overcharge customers and violate tax regulations in many jurisdictions. The pricing pipeline always calculates discounts first, then computes tax on the post-discount amount.

12. Checkout Flow & Address Validation

The checkout flow is an orchestrated sequence of steps that must guide the user from cart to confirmed order. Each step may involve external service calls, user input validation, and state management. The checkout service acts as an orchestrator, maintaining checkout state and coordinating calls to the inventory, pricing, shipping, tax, and payment services.

Checkout Step Sequence

graph TD A[Initialize Checkout] --> B{Items Available?} B -->|Yes| C[Reserve Inventory - 10 min TTL] B -->|No| Z[Show Unavailable Items] C --> D[Enter Shipping Address] D --> E[Validate Address] E -->|Valid| F[Show Shipping Options] E -->|Invalid| D F --> G[Select Shipping Method] G --> H[Review Order Summary] H --> I[Enter Payment Details] I --> J{3D Secure Required?} J -->|Yes| K[3D Secure Challenge] J -->|No| L[Process Payment] K --> L L -->|Success| M[Create Order] L -->|Failed| N[Show Payment Error] N --> I M --> O[Send Confirmation Email] M --> P[Release Remaining Inventory] M --> Q[Track Analytics Event] Z --> A

Address Validation

Address validation is critical for reducing shipping errors and delivery failures. The system validates addresses using a multi-layer approach: format validation (USPS/Country-specific format rules), database lookup (existing known addresses), and external API validation (Google Places API or SmartyStreets for USPS address standardization). Validated addresses are cached to reduce API calls for repeat customers.

C#
public class AddressValidationService
    : IAddressValidationService
{
    private readonly IAddressValidator _validator;
    private readonly IAddressCache _cache;

    public async Task<AddressValidationResult> ValidateAsync(
        Address address)
    {
        // 1. Check cache for previously validated address
        var cacheKey = ComputeAddressHash(address);
        var cached = await _cache.GetAsync(cacheKey);
        if (cached != null)
            return cached;

        // 2. Format validation
        var formatResult = ValidateFormat(address);
        if (!formatResult.IsValid)
            return formatResult;

        // 3. External API validation (SmartyStreets / USPS)
        var apiResult = await _validator
            .StandardizeAsync(address);

        var result = new AddressValidationResult
        {
            IsValid = apiResult.Deliverable,
            StandardizedAddress = apiResult.Standardized,
            Suggestions = apiResult.Candidates
                .Take(3)
                .Select(c => c.FormattedAddress)
                .ToList(),
            Resolution = apiResult.Deliverable
                ? AddressResolution.Verified
                : AddressResolution.Undeliverable
        };

        if (result.IsValid)
        {
            await _cache.SetAsync(cacheKey, result,
                TimeSpan.FromDays(30));
        }

        return result;
    }
}

Checkout Orchestrator

C#
public class CheckoutOrchestrator : ICheckoutService
{
    private readonly IInventoryService _inventory;
    private readonly IPricingEngine _pricing;
    private readonly IShippingService _shipping;
    private readonly ITaxService _tax;
    private readonly IPaymentService _payment;
    private readonly IOrderService _orders;
    private readonly IEventPublisher _events;

    public async Task<CheckoutResult> InitializeCheckoutAsync(
        Guid cartId, Guid? userId)
    {
        var cart = await LoadCartAsync(cartId);
        if (!cart.Items.Any())
            throw new ConflictException("Cart is empty");

        // Parallel calls: inventory check + price calculation
        var inventoryTask = _inventory
            .CheckAvailabilityAsync(cart.Items);
        var pricingTask = _pricing.CalculatePriceAsync(cart);

        await Task.WhenAll(inventoryTask, pricingTask);

        var inventory = await inventoryTask;
        var pricing = await pricingTask;

        if (!inventory.AllAvailable)
        {
            return new CheckoutResult
            {
                Success = false,
                UnavailableItems = inventory.UnavailableItems
            };
        }

        // Create checkout session
        var checkout = new CheckoutSession
        {
            CheckoutId = Guid.NewGuid(),
            CartId = cartId,
            UserId = userId,
            PricingResult = pricing,
            Status = CheckoutStatus.AddressRequired,
            ExpiresAt = DateTime.UtcNow.AddMinutes(15),
            CreatedAt = DateTime.UtcNow
        };

        await _cache.SetCheckoutAsync(checkout,
            TimeSpan.FromMinutes(15));

        return new CheckoutResult
        {
            Success = true,
            CheckoutId = checkout.CheckoutId,
            Pricing = pricing,
            Status = checkout.Status
        };
    }

    public async Task<OrderResult> PlaceOrderAsync(
        Guid checkoutId, PlaceOrderRequest request)
    {
        var checkout = await _cache
            .GetCheckoutAsync(checkoutId);
        if (checkout == null)
            throw new NotFoundException(
                "Checkout session expired");

        // 1. Reserve inventory (soft lock, 10 min TTL)
        var reservation = await _inventory
            .ReserveInventoryAsync(
                checkout.OrderId, checkout.Cart.Items);

        if (!reservation.AllReserved)
        {
            return new OrderResult
            {
                Success = false,
                Error = "Some items are no longer available",
                UnavailableItems = reservation.Items
                    .Where(i => !i.Success).ToList()
            };
        }

        // 2. Re-validate pricing (prices may have changed)
        var currentPricing = await _pricing
            .CalculatePriceAsync(checkout.Cart);
        if (currentPricing.GrandTotal !=
            checkout.PricingResult.GrandTotal)
        {
            return new OrderResult
            {
                Success = false,
                Error = "Prices have changed",
                UpdatedPricing = currentPricing
            };
        }

        // 3. Process payment
        var paymentResult = await _payment
            .ChargeAsync(new PaymentRequest
        {
            Amount = checkout.PricingResult.GrandTotal,
            Currency = checkout.Cart.Currency,
            PaymentMethodId = request.PaymentMethodId,
            IdempotencyKey = checkout.CheckoutId.ToString(),
            Metadata = new()
            {
                ["checkout_id"] =
                    checkout.CheckoutId.ToString(),
                ["cart_id"] =
                    checkout.CartId.ToString()
            }
        });

        if (!paymentResult.Success)
        {
            await _inventory.ReleaseReservationAsync(
                reservation.OrderId);
            return new OrderResult
            {
                Success = false,
                Error = paymentResult.ErrorMessage
            };
        }

        // 4. Create order
        var order = await _orders.CreateOrderAsync(
            checkout, paymentResult, request);

        // 5. Publish events (async, fire-and-forget)
        await _events.PublishAsync(new OrderPlaced
        {
            OrderId = order.OrderId,
            UserId = checkout.UserId,
            Total = checkout.PricingResult.GrandTotal,
            Timestamp = DateTime.UtcNow
        });

        // 6. Clear cart and checkout session
        await ClearCartAsync(checkout.CartId);
        await _cache.RemoveCheckoutAsync(checkoutId);

        return new OrderResult
        {
            Success = true,
            OrderId = order.OrderId,
            OrderNumber = order.OrderNumber
        };
    }
}

13. Shipping Calculation & Carrier Integration

Shipping calculation involves integrating with multiple carrier APIs (UPS, FedEx, USPS, DHL) to provide accurate rates and delivery estimates. The shipping service must handle rate shopping (comparing prices across carriers), address validation (ensuring the address is deliverable), package dimension calculation, and delivery date estimation. This is one of the most latency-sensitive parts of checkout because it involves multiple external API calls.

Shipping Rate Response

JSON
{
    "shipping_options": [
        {
            "carrier": "UPS",
            "service": "Ground",
            "rate_id": "ups_ground_001",
            "rate": 8.99,
            "currency": "USD",
            "estimated_days": { "min": 5, "max": 7 },
            "estimated_delivery": "2026-07-18",
            "weight_kg": 0.45
        },
        {
            "carrier": "FedEx",
            "service": "Express 2Day",
            "rate_id": "fedex_express_2day",
            "rate": 14.99,
            "currency": "USD",
            "estimated_days": { "min": 2, "max": 2 },
            "estimated_delivery": "2026-07-14",
            "weight_kg": 0.45
        },
        {
            "carrier": "USPS",
            "service": "Priority Mail",
            "rate_id": "usps_priority",
            "rate": 7.35,
            "currency": "USD",
            "estimated_days": { "min": 2, "max": 3 },
            "estimated_delivery": "2026-07-15",
            "weight_kg": 0.45
        }
    ],
    "free_shipping_eligible": false,
    "free_shipping_remaining": 15.02
}

Free Shipping Threshold Logic

Free shipping is a powerful conversion incentive. The system tracks each cart's proximity to the free shipping threshold and displays a dynamic banner (e.g., "Add $15.02 more for free shipping!"). This is calculated by comparing the cart subtotal against the store's free shipping threshold (typically $50-$100). When the threshold is met, the shipping options are filtered to show free shipping as the default option.

C#
public class ShippingService : IShippingService
{
    private readonly ICarrierClient _carriers;
    private readonly IFreeShippingConfig _config;

    public async Task<ShippingQuote> GetRatesAsync(
        Address destination, List<CartItem> items)
    {
        // 1. Calculate package dimensions and weight
        var packages = PackageCalculator
            .OptimizePacking(items);

        // 2. Fetch rates from all carriers in parallel
        var carrierTasks = new[]
        {
            _carriers.GetUpsRatesAsync(destination, packages),
            _carriers.GetFedExRatesAsync(destination, packages),
            _carriers.GetUspsRatesAsync(destination, packages)
        };

        var carrierResults = await Task
            .WhenAll(carrierTasks);

        // 3. Aggregate and deduplicate rates
        var rates = carrierResults
            .SelectMany(r => r.Rates)
            .OrderBy(r => r.Rate)
            .ToList();

        // 4. Check free shipping eligibility
        var subtotal = items.Sum(i => i.LineTotal);
        var freeShippingThreshold =
            _config.FreeShippingThreshold;
        var qualifiesForFreeShipping =
            subtotal >= freeShippingThreshold;

        if (qualifiesForFreeShipping)
        {
            rates.Insert(0, new ShippingRate
            {
                Carrier = "FREE",
                Service = "Standard Shipping",
                Rate = 0m,
                EstimatedDays = new() { Min = 5, Max = 7 }
            });
        }

        return new ShippingQuote
        {
            Options = rates,
            FreeShippingEligible =
                qualifiesForFreeShipping,
            FreeShippingRemaining =
                qualifiesForFreeShipping
                    ? 0m
                    : freeShippingThreshold - subtotal
        };
    }
}

14. Payment Processing & Stripe Integration

Payment processing is the highest-risk component of the checkout system. A bug in payment processing can result in double charges, lost payments, or financial data exposure. The payment service must handle authorization, capture, 3D Secure authentication, payment method vaulting, refunds, and disputes — all while maintaining PCI-DSS compliance. We use Stripe as our primary payment gateway, which provides a robust API, built-in fraud detection (Radar), and support for 135+ currencies.

Payment Flow

sequenceDiagram participant Client participant Checkout as Checkout Service participant Payment as Payment Service participant Stripe participant DB as Payment Ledger Client->>Checkout: Place Order Checkout->>Payment: Charge(amount, method) Payment->>DB: Create payment record (status: pending) Payment->>Stripe: PaymentIntent.create(amount) alt 3D Secure Required Stripe-->>Payment: requires_action Payment-->>Checkout: requires_3ds Checkout-->>Client: Return 3DS redirect URL Client->>Stripe: 3DS verification Stripe-->>Client: 3DS complete Client->>Checkout: Confirm 3DS Checkout->>Payment: Confirm payment Payment->>Stripe: PaymentIntent.confirm() else No 3DS Required Stripe-->>Payment: succeeded end Stripe-->>Payment: Webhook: payment_intent.succeeded Payment->>DB: Update status: captured Payment-->>Checkout: Payment successful Checkout->>DB: Create order (status: confirmed) Checkout-->>Client: Order confirmed

Stripe Payment Integration

C#
public class StripePaymentService : IPaymentService
{
    private readonly StripeClient _stripe;
    private readonly IPaymentRepository _repo;

    public async Task<PaymentResult> ChargeAsync(
        PaymentRequest request)
    {
        // 1. Create payment record for audit trail
        var payment = new PaymentRecord
        {
            PaymentId = Guid.NewGuid(),
            IdempotencyKey = request.IdempotencyKey,
            Amount = request.Amount,
            Currency = request.Currency,
            Status = PaymentStatus.Pending,
            CreatedAt = DateTime.UtcNow
        };
        await _repo.CreateAsync(payment);

        try
        {
            // 2. Create Stripe PaymentIntent
            var intentOptions = new PaymentIntentCreateOptions
            {
                Amount = StripeAmount(request.Amount,
                    request.Currency),
                Currency = request.Currency.ToLower(),
                PaymentMethod = request.PaymentMethodId,
                ConfirmationMethod = "manual",
                Confirm = true,
                CaptureMethod = "manual",
                IdempotencyKey = request.IdempotencyKey,
                Metadata = request.Metadata
                    .ToDictionary(k => k.Key, k => k.Value)
            };

            var intent = await _stripe.PaymentIntents
                .CreateAsync(intentOptions);

            payment.StripePaymentIntentId = intent.Id;
            payment.StripeStatus = intent.Status;

            // 3. Handle response
            switch (intent.Status)
            {
                case "succeeded":
                    payment.Status = PaymentStatus.Authorized;
                    await _repo.UpdateAsync(payment);
                    return new PaymentResult
                    {
                        Success = true,
                        PaymentId = payment.PaymentId,
                        AuthorizationId = intent.Id
                    };

                case "requires_action":
                    payment.Status =
                        PaymentStatus.RequiresAction;
                    await _repo.UpdateAsync(payment);
                    return new PaymentResult
                    {
                        Success = false,
                        RequiresAction = true,
                        ClientSecret = intent.ClientSecret,
                        PaymentId = payment.PaymentId
                    };

                case "requires_payment_method":
                    payment.Status = PaymentStatus.Failed;
                    payment.FailureReason =
                        "Invalid payment method";
                    await _repo.UpdateAsync(payment);
                    return new PaymentResult
                    {
                        Success = false,
                        ErrorMessage =
                            "Invalid payment method"
                    };

                default:
                    payment.Status = PaymentStatus.Failed;
                    payment.FailureReason = intent.Status;
                    await _repo.UpdateAsync(payment);
                    return new PaymentResult
                    {
                        Success = false,
                        ErrorMessage =
                            "Payment could not be processed"
                    };
            }
        }
        catch (StripeException ex)
        {
            payment.Status = PaymentStatus.Failed;
            payment.FailureReason = ex.Message;
            await _repo.UpdateAsync(payment);
            throw new PaymentException(
                "Payment processing failed", ex);
        }
    }

    public async Task<CaptureResult> CaptureAsync(
        Guid paymentId)
    {
        var payment = await _repo.GetAsync(paymentId);
        if (payment == null)
            throw new NotFoundException("Payment not found");

        var captureOptions = new PaymentIntentCaptureOptions
        {
            AmountToCapture = StripeAmount(
                payment.Amount, payment.Currency)
        };

        var intent = await _stripe.PaymentIntents
            .CaptureAsync(
                payment.StripePaymentIntentId,
                captureOptions);

        payment.Status = PaymentStatus.Captured;
        payment.CapturedAt = DateTime.UtcNow;
        await _repo.UpdateAsync(payment);

        return new CaptureResult
        {
            Success = true,
            CapturedAmount = payment.Amount
        };
    }

    private long StripeAmount(decimal amount, string currency)
    {
        var zeroDecimalCurrencies = new HashSet<string>
            { "JPY", "KRW", "VND" };
        var divisor = zeroDecimalCurrencies
            .Contains(currency.ToUpper()) ? 1 : 100;
        return (long)(amount * divisor);
    }
}
Capture vs Authorization: We use manual capture (authorize now, capture later) rather than immediate capture. This is important because authorization places a hold on the customer's card, but the actual money transfer happens at capture. By deferring capture until the order is confirmed and inventory is committed, we can safely void the authorization if any post-payment step fails (e.g., inventory reservation expired, shipping address is undeliverable). This prevents situations where a customer is charged for an order that cannot be fulfilled.

15. Idempotency & Exactly-Once Payments

In a distributed system, network failures can cause requests to be retried. Without idempotency, a retry could result in a double charge — the most financially damaging type of system failure. The payment service must guarantee exactly-once processing of every payment request, even if the client retries due to a timeout or network error.

Idempotency Strategy

Every payment request includes an idempotency_key — a unique identifier generated by the client (typically the checkout session ID). The payment service stores a mapping of idempotency_key to payment_result in PostgreSQL with a unique constraint. Before processing a new payment, the service checks if the idempotency key already exists. If it does, it returns the stored result instead of processing a new payment.

C#
public class IdempotentPaymentService : IPaymentService
{
    private readonly IPaymentService _inner;
    private readonly IPaymentRepository _repo;
    private readonly IDistributedLock _locks;

    public async Task<PaymentResult> ChargeAsync(
        PaymentRequest request)
    {
        if (string.IsNullOrEmpty(request.IdempotencyKey))
            throw new ArgumentException(
                "Idempotency key is required");

        // Acquire lock for this idempotency key
        var lockKey =
            $"idempotency:{request.IdempotencyKey}";
        await _locks.AcquireAsync(lockKey,
            TimeSpan.FromSeconds(30));

        try
        {
            // Check if this key was already processed
            var existing = await _repo
                .GetByIdeMPotencyKeyAsync(
                    request.IdempotencyKey);

            if (existing != null)
            {
                // Return cached result — do NOT process again
                return new PaymentResult
                {
                    Success = existing.Status ==
                        PaymentStatus.Authorized,
                    PaymentId = existing.PaymentId,
                    AuthorizationId =
                        existing.StripePaymentIntentId,
                    IdempotentReplay = true
                };
            }

            // Process the payment
            var result = await _inner.ChargeAsync(request);
            return result;
        }
        finally
        {
            await _locks.ReleaseAsync(lockKey);
        }
    }
}

Timeout Handling and Reconciliation

The most dangerous scenario in payment processing is the "uncertain state" — where the client sends a payment request, the payment gateway processes it, but the response is lost due to a network timeout. The client does not know if the payment succeeded, and the server may have already charged the customer. Our system handles this through a reconciliation process that runs in the background.

C#
public class PaymentReconciliationWorker : BackgroundService
{
    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var pendingPayments = await _repo
                .GetPendingPaymentsAsync(
                    olderThan: TimeSpan.FromSeconds(60));

            foreach (var payment in pendingPayments)
            {
                try
                {
                    var status = await _stripe
                        .PaymentIntents.GetAsync(
                            payment.StripePaymentIntentId);

                    payment.StripeStatus = status.Status;
                    payment.Status = status.Status == "succeeded"
                        ? PaymentStatus.Authorized
                        : PaymentStatus.Failed;
                    payment.ReconciledAt = DateTime.UtcNow;
                    await _repo.UpdateAsync(payment);

                    _log.LogInformation(
                        "Reconciled payment {Id}: {Status}",
                        payment.PaymentId, status.Status);
                }
                catch (Exception ex)
                {
                    _log.LogError(ex,
                        "Failed to reconcile payment {Id}",
                        payment.PaymentId);
                }
            }

            await Task.Delay(
                TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}
Double Charge Prevention: The idempotency key must be generated by the client before sending the payment request, and must be deterministic for a given checkout attempt (e.g., the checkout session ID). If the client generates a new idempotency key for each retry, the idempotency mechanism is defeated. Ensure the client SDK handles this correctly by persisting the idempotency key in localStorage or session storage.

16. Order State Machine

Every order progresses through a well-defined lifecycle of states. The order state machine ensures that orders transition correctly and that invalid state transitions are rejected. This state machine is the single source of truth for order status and drives all downstream processes (payment capture, fulfillment, notifications). A clean state machine makes the system predictable, debuggable, and auditable.

Order States and Transitions

stateDiagram-v2 [*] --> Pending: Order placed Pending --> Confirmed: Payment authorized Pending --> Cancelled: Payment failed Confirmed --> Processing: Capture initiated Confirmed --> Cancelled: Void authorization Processing --> Shipped: Package shipped Processing --> Cancelled: Cannot fulfill Shipped --> Delivered: Delivery confirmed Shipped --> Returned: Return initiated Delivered --> Completed: Return window closed Delivered --> Refunded: Full refund Returned --> Refunded: Return processed Returned --> PartialRefund: Partial return Completed --> [*] Cancelled --> [*] Refunded --> [*] PartialRefund --> [*]

Order State Machine Implementation

C#
public class OrderStateMachine
{
    private static readonly Dictionary<
        OrderStatus, HashSet<OrderStatus>> Transitions = new()
    {
        [OrderStatus.Pending] = new()
            { OrderStatus.Confirmed, OrderStatus.Cancelled },
        [OrderStatus.Confirmed] = new()
            { OrderStatus.Processing, OrderStatus.Cancelled },
        [OrderStatus.Processing] = new()
            { OrderStatus.Shipped, OrderStatus.Cancelled },
        [OrderStatus.Shipped] = new()
            { OrderStatus.Delivered, OrderStatus.Returned },
        [OrderStatus.Delivered] = new()
            { OrderStatus.Completed, OrderStatus.Refunded },
        [OrderStatus.Returned] = new()
            { OrderStatus.Refunded, OrderStatus.PartialRefund }
    };

    public bool CanTransition(
        OrderStatus from, OrderStatus to)
    {
        return Transitions.TryGetValue(from, out var allowed)
            && allowed.Contains(to);
    }

    public OrderStatus Transition(
        OrderStatus current, OrderStatus target)
    {
        if (!CanTransition(current, target))
        {
            throw new InvalidOperationException(
                $"Invalid order transition: " +
                $"{current} -> {target}");
        }
        return target;
    }
}

public class OrderService : IOrderService
{
    private readonly OrderStateMachine _stateMachine;
    private readonly IOrderRepository _repo;
    private readonly IEventPublisher _events;

    public async Task<Order> CreateOrderAsync(
        CheckoutSession checkout,
        PaymentResult payment,
        PlaceOrderRequest request)
    {
        var order = new Order
        {
            OrderId = Guid.NewGuid(),
            OrderNumber = GenerateOrderNumber(),
            UserId = checkout.UserId,
            Status = OrderStatus.Pending,
            Subtotal = checkout.PricingResult.Subtotal,
            DiscountTotal =
                checkout.PricingResult.TotalDiscount,
            TaxTotal = checkout.PricingResult.TaxTotal,
            ShippingTotal =
                checkout.PricingResult.ShippingTotal,
            GrandTotal =
                checkout.PricingResult.GrandTotal,
            Currency = checkout.Cart.Currency,
            ShippingAddress = request.ShippingAddress,
            BillingAddress = request.BillingAddress,
            ShippingMethod = request.ShippingMethod,
            PaymentMethod = request.PaymentMethodId,
            PaymentIntentId =
                payment.AuthorizationId,
            IdempotencyKey =
                checkout.CheckoutId.ToString(),
            PlacedAt = DateTime.UtcNow,
            Items = checkout.Cart.Items.Select(i =>
                new OrderItem
            {
                OrderItemId = Guid.NewGuid(),
                ProductId = i.ProductId,
                VariantId = i.VariantId,
                Sku = i.Sku,
                Name = i.Name,
                Quantity = i.Quantity,
                UnitPrice = i.UnitPrice,
                LineTotal = i.LineTotal
            }).ToList()
        };

        // Transition: Pending -> Confirmed
        order.Status = _stateMachine.Transition(
            OrderStatus.Pending, OrderStatus.Confirmed);

        await _repo.CreateOrderAsync(order);

        await _events.PublishAsync(new OrderStateChanged
        {
            OrderId = order.OrderId,
            From = OrderStatus.Pending,
            To = OrderStatus.Confirmed,
            Timestamp = DateTime.UtcNow
        });

        return order;
    }

    private string GenerateOrderNumber()
    {
        // Format: ORD-YYYYMMDD-XXXXX
        var date = DateTime.UtcNow.ToString("yyyyMMdd");
        var sequence = Interlocked.Increment(
            ref _sequenceCounter);
        return $"ORD-{date}-{sequence:D5}";
    }
}
Order Number Generation: The order number serves two purposes: it is the user-facing reference number for customer support, and it must be unique and monotonic for audit purposes. We use a composite format (date + sequence) that is both human-readable and automatically sortable. The sequence counter is per-process, and uniqueness across processes is guaranteed by the database unique constraint on the order_number column.

17. Order Confirmation & Email Receipts

Order confirmation is a multi-step process that provides the user with immediate feedback and sends a detailed receipt via email. The confirmation must include the order summary, payment confirmation, estimated delivery date, and next steps. The email must be sent reliably (using a transactional email service with delivery tracking) and must include the complete order details for the customer records.

Confirmation Flow

graph TD A[Order Placed] --> B[Show Confirmation Page] A --> C[Publish OrderPlaced Event] C --> D[Email Service] C --> E[Analytics Service] C --> F[Fulfillment Service] C --> G[SMS Service] D --> H[Render Email Template] H --> I[Send via SendGrid/SES] I --> J{Delivery Status} J -->|Delivered| K[Log Success] J -->|Bounced| L[Log Bounce + Alert] J -->|Failed| M[Retry Queue] E --> N[Track Conversion Event] E --> O[Update Revenue Dashboard] F --> P[Create Fulfillment Task] F --> Q[Pick + Pack Queue]

Order Confirmation Email Template

C#
public class OrderConfirmationHandler
    : IEventHandler<OrderPlaced>
{
    private readonly IEmailService _email;
    private readonly ITemplateEngine _templates;

    public async Task HandleAsync(OrderPlaced orderEvent)
    {
        var order = await _orderRepo
            .GetOrderAsync(orderEvent.OrderId);

        var templateData = new OrderConfirmationModel
        {
            OrderNumber = order.OrderNumber,
            OrderDate = order.PlacedAt
                .ToString("MMMM dd, yyyy"),
            CustomerName = order.User.FirstName,
            Items = order.Items.Select(i => new
            {
                Name = i.Name,
                Quantity = i.Quantity,
                UnitPrice = i.UnitPrice.ToString("C"),
                LineTotal = i.LineTotal.ToString("C")
            }),
            Subtotal = order.Subtotal.ToString("C"),
            DiscountTotal = order.DiscountTotal.ToString("C"),
            TaxTotal = order.TaxTotal.ToString("C"),
            ShippingTotal = order.ShippingTotal.ToString("C"),
            GrandTotal = order.GrandTotal.ToString("C"),
            ShippingAddress = FormatAddress(
                order.ShippingAddress),
            EstimatedDelivery = CalculateEstimatedDelivery(
                order.ShippingMethod),
            TrackingUrl =
                $"https://example.com/track/{order.OrderNumber}"
        };

        var html = await _templates.RenderAsync(
            "order-confirmation", templateData);

        await _email.SendAsync(new EmailMessage
        {
            To = order.User.Email,
            Subject =
                $"Order Confirmed — {order.OrderNumber}",
            HtmlBody = html,
            Tags = new()
            {
                "order-confirmation",
                order.OrderNumber
            }
        });
    }
}

Email Delivery Tracking

EventSourceAction
DeliveredSendGrid webhookLog success, no further action
OpenedSendGrid webhookTrack engagement, update analytics
ClickedSendGrid webhookTrack link clicks (tracking URL)
BouncedSendGrid webhookMark email invalid, alert support
Spam reportSendGrid webhookFlag user, stop marketing emails
DeferredSendGrid webhookLog, no action (temporary issue)

18. Cart Abandonment Recovery

Cart abandonment recovery is one of the highest-ROI features in e-commerce. According to Moosend research, sending abandoned cart emails can recover approximately 10-15% of lost revenue. The system must track when users abandon their carts, trigger recovery emails at optimal intervals, and provide deep links that restore the user's exact cart state. For a platform processing $750M/month in GMV with a 70% abandonment rate, recovering even 5% of abandoned carts represents approximately $26.25M in recovered revenue monthly.

Abandonment Detection and Recovery Flow

graph TD A[User adds items to cart] --> B[Start abandonment timer 30 min idle] B --> C{User completes checkout?} C -->|Yes| D[Cancel abandonment timer] C -->|No - 30 min idle| E[Mark cart as abandoned] E --> F[Send Email 1 after 1 hour] F --> G{User returns?} G -->|Yes| H[Cancel remaining emails] G -->|No| I[Send Email 2 after 24 hours] I --> J{User returns?} J -->|Yes| H J -->|No| K[Send Email 3 after 72 hours with 10% discount] K --> L{User returns?} L -->|Yes| H L -->|No| M[Mark as unrecoverable]

Abandonment Tracking Implementation

C#
public class CartAbandonmentTracker : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _scopeFactory.CreateScope();
            var carts = scope.ServiceProvider
                .GetRequiredService<ICartRepository>();
            var events = scope.ServiceProvider
                .GetRequiredService<IEventPublisher>();

            // Find carts that have been idle for 30+ minutes
            // and have items but no checkout started
            var abandonedCarts = await carts
                .GetAbandonedCartsAsync(
                    idleThreshold:
                        TimeSpan.FromMinutes(30),
                    batchSize: 500);

            foreach (var cart in abandonedCarts)
            {
                // Only track if we have an email address
                if (string.IsNullOrEmpty(
                    cart.User?.Email))
                    continue;

                // Mark as abandoned
                cart.Status = CartStatus.Abandoned;
                await carts.UpdateCartAsync(cart);

                // Emit abandonment event
                await events.PublishAsync(
                    new CartAbandoned
                {
                    CartId = cart.CartId,
                    UserId = cart.UserId,
                    Email = cart.User.Email,
                    ItemCount = cart.Items.Count,
                    CartValue = cart.Totals.GrandTotal,
                    AbandonedAt = DateTime.UtcNow,
                    CartUrl =
                        $"https://example.com/cart" +
                        $"/recover/{cart.CartId}"
                });
            }

            await Task.Delay(
                TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

public class AbandonmentEmailHandler
    : IEventHandler<CartAbandoned>
{
    private readonly IEmailService _email;

    public async Task HandleAsync(CartAbandoned abandoned)
    {
        // Email 1: "You left something behind" (1 hour)
        var delay = TimeSpan.FromHours(1);

        await _email.ScheduleAsync(new ScheduledEmail
        {
            To = abandoned.Email,
            Subject = "You left items in your cart",
            Template = "cart-abandonment-1",
            Data = new
            {
                Items = abandoned.CartValue,
                CartUrl = abandoned.CartUrl
            },
            SendAt = DateTime.UtcNow.Add(delay),
            CancelIf = async () =>
            {
                // Cancel if cart was converted to order
                var cart = await _cartRepo
                    .GetCartAsync(abandoned.CartId);
                return cart?.Status ==
                    CartStatus.Converted;
            }
        });
    }
}

Recovery Email Strategy

EmailTimingContentExpected Recovery Rate
Reminder1 hourCart items, no discount5-8%
Follow-up24 hoursCart items + social proof reviews3-5%
Last chance72 hoursCart items + 10% discount code2-3%
Final7 daysUrgency ("items selling fast") + free shipping1-2%
Recovery Deep Link: The recovery email includes a deep link that pre-populates the user's cart with the exact items they left behind. This requires the cart to be persisted (not just in Redis with a short TTL) and the deep link to include a signed token that authenticates the user and restores their session. This seamless restoration eliminates the friction of re-adding items, which is one of the primary reasons users abandon carts in the first place.

19. A/B Testing Checkout Flows

The checkout flow is one of the most impactful areas for A/B testing in e-commerce. Small changes to the checkout flow — the number of steps, the placement of trust badges, the order of payment options — can significantly impact conversion rates. The system must support running multiple checkout flow variants simultaneously with proper statistical rigor and experiment isolation.

Checkout Experiment Framework

C#
public class CheckoutExperimentService
{
    private readonly IFeatureFlagService _flags;
    private readonly IExperimentTracker _tracker;

    public CheckoutVariant GetVariant(
        Guid userId, Guid sessionId)
    {
        // Deterministic variant assignment
        // using consistent hashing
        var hash = ComputeHash(
            $"checkout-experiment-2026-" +
            $"{userId ?? sessionId}");
        var bucket = hash % 100;

        var variant = bucket < 50
            ? CheckoutVariant.Control   // Multi-step
            : CheckoutVariant.Treatment; // Single-page

        _tracker.TrackAssignment(new ExperimentAssignment
        {
            ExperimentName = "checkout-flow-v2",
            UserId = userId,
            SessionId = sessionId,
            Variant = variant.ToString(),
            AssignedAt = DateTime.UtcNow
        });

        return variant;
    }
}

// Variants
public enum CheckoutVariant
{
    Control,   // Traditional 4-step checkout
    Treatment  // Single-page checkout with accordion
}

// In the checkout controller
[HttpGet("checkout")]
public async Task<IActionResult> Checkout()
{
    var variant = _experiments.GetVariant(
        CurrentUserId, SessionId);

    return variant switch
    {
        CheckoutVariant.Control =>
            View("Checkout_MultiStep"),
        CheckoutVariant.Treatment =>
            View("Checkout_SinglePage"),
        _ => View("Checkout_MultiStep")
    };
}

Key Metrics to Track

MetricDescriptionTarget Impact
Conversion Rate% of checkout initiations that result in an orderPrimary metric
Cart-to-Checkout Rate% of users who start checkout from cartFunnel step
Checkout Step Drop-off% of users abandoning at each stepDiagnostic
Time to CompleteSeconds from checkout start to order placedUX quality
Payment Failure Rate% of payment attempts that failReliability
Average Order ValueMean grand_total per orderRevenue impact
Revenue per SessionTotal revenue / total checkout sessionsBusiness outcome

Statistical Significance

Checkout experiments require careful statistical handling because conversion rates are typically low (2-5%) and order values are high-variance. A typical checkout A/B test needs 10,000-50,000 completed orders per variant to detect a 1% relative improvement in conversion rate with 95% confidence. At this sample size, the test typically runs for 2-4 weeks. We use a Bayesian approach with Thompson Sampling for early stopping when the probability of one variant being better exceeds 95%, allowing faster iteration on losing variants.

A/B Testing Pitfall: Never stop a checkout experiment early based on "obvious" results without statistical significance. A common mistake is running a test for 3 days, seeing one variant ahead by 20%, and declaring a winner. At low sample sizes, this difference is almost certainly noise. Always wait for the pre-determined sample size or use formal early stopping rules.

20. Performance Optimization

Checkout performance directly impacts conversion rates. Amazon found that every 100ms of additional latency cost them 1% in sales. Google found that a 0.5-second delay in search results reduced traffic by 20%. For e-commerce checkout, the stakes are even higher — users who encounter slow checkout pages are highly likely to abandon their carts entirely and never return. Performance optimization must be approached systematically, measuring and improving each component of the checkout latency pipeline.

Optimization Strategies

StrategyImplementationImpact
Parallel service callsExecute inventory, pricing, shipping, tax calls concurrentlyReduce checkout latency from 800ms to 250ms
Redis cart cacheHot cart data in Redis, fallback to PostgreSQL on missCart reads: 15ms to 1ms
Prefetch shipping ratesPre-calculate shipping rates for popular addressesReduce shipping call from 200ms to 20ms
Tax calculation cachingCache tax rates by (jurisdiction, product_type)Reduce tax call from 150ms to 5ms
Inventory count cachingRedis cache for stock counts, 30s TTLReduce inventory check from 100ms to 2ms
Pricing rule cachingCompile pricing rules to in-memory functionsReduce pricing calc from 50ms to 5ms
Connection poolingPre-established connections to all servicesEliminate TCP/TLS handshake overhead
Response compressionBrotli compression for API responsesReduce payload size by 60-70%

Checkout Latency Budget

StepTarget LatencyMethod
Cart load (Redis)2msDirect Redis GET
Inventory check10msRedis-cached stock counts
Price calculation15msIn-memory rule engine
Tax calculation10msCached tax rates
Shipping rates30msPrefetched + cached
Inventory reservation15msOptimistic lock + retry
Payment authorization200msStripe API (external)
Order creation20msPostgreSQL INSERT
Total~300msParallel execution where possible

Database Optimization

SQL
-- Partition orders by month for query performance
CREATE TABLE orders (
    -- ... columns as before ...
) PARTITION BY RANGE (placed_at);

CREATE TABLE orders_2026_07 PARTITION OF orders
    FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');

-- Composite index for the most common query pattern
CREATE INDEX idx_orders_user_status_placed
    ON orders(user_id, status, placed_at DESC);

-- Partial index for active orders only
CREATE INDEX idx_orders_active
    ON orders(placed_at DESC)
    WHERE status NOT IN ('cancelled', 'refunded', 'completed');

-- Materialized view for analytics dashboard
CREATE MATERIALIZED VIEW order_daily_summary AS
SELECT
    DATE_TRUNC('day', placed_at) AS day,
    COUNT(*) AS order_count,
    SUM(grand_total) AS revenue,
    AVG(grand_total) AS avg_order_value,
    COUNT(DISTINCT user_id) AS unique_customers
FROM orders
WHERE status != 'cancelled'
GROUP BY DATE_TRUNC('day', placed_at)
ORDER BY day DESC;
Performance Target: The checkout API should return a complete response (including all pricing, shipping options, and tax breakdown) within 300ms at p99. This is achievable by parallelizing external service calls and aggressively caching read-heavy data. The payment authorization call to Stripe (approximately 200ms) is the single largest contributor to checkout latency and cannot be cached or parallelized — it must complete before the order is confirmed.

21. Security, PCI DSS & Compliance

The checkout system handles highly sensitive data: credit card numbers, personal addresses, and financial transactions. Security must be a first-class concern at every layer of the architecture. The system must comply with PCI-DSS (Payment Card Industry Data Security Standard) for card data handling, GDPR/CCPA for personal data privacy, and SOC 2 for operational security.

PCI-DSS Compliance Levels

LevelAnnual TransactionsRequirementsOur Approach
Level 1> 6 millionAnnual on-site audit + quarterly ASV scanFull PCI-DSS compliance program
Level 21-6 millionAnnual SAQ + quarterly ASV scanSAQ A-EP compliance
Level 320K-1MAnnual SAQSAQ A compliance
Level 4< 20KSAQ recommendedMinimal requirements

Security Architecture

graph TB subgraph Client Layer Browser[Browser] --> TLS[TLS 1.3] end subgraph Edge Layer TLS --> WAF[WAF OWASP Top 10] WAF --> DDoS[DDoS Protection] DDoS --> LB[Load Balancer] end subgraph Application Layer LB --> GW[API Gateway Rate Limiting] GW --> Auth[Auth Service JWT + OAuth2] Auth --> Services[Microservices] end subgraph Data Layer Services --> EncDB[(Encrypted DB AES-256)] Services --> Vault[Secrets Vault HashiCorp Vault] Services --> EncRedis[(Redis TLS in Transit)] end subgraph Payment Layer Services --> PCI[PCI Zone Isolated Network] PCI --> StripeVault[Stripe Vault Card Tokenization] end

Security Measures

LayerMeasureImplementation
TransportTLS 1.3 everywhereAll internal and external communication encrypted
AuthenticationJWT with short expiry15-minute access tokens, refresh token rotation
AuthorizationRole-based + resource-basedUsers can only access their own carts/orders
Input validationServer-side validation on all inputsFluentValidation for all API endpoints
Rate limitingPer-user and per-IP rate limitsToken bucket algorithm at API Gateway
Data encryptionAES-256 at restTransparent data encryption for PostgreSQL
Card dataTokenization via StripeCard numbers never touch our servers
Audit loggingAll state changes loggedImmutable audit trail in append-only table
Secrets managementHashiCorp VaultNo secrets in code, config, or env vars
Vulnerability scanningContinuousSnyk for dependencies, Trivy for containers

GDPR/CCPA Data Handling

C#
public class DataPrivacyService : IDataPrivacyService
{
    // GDPR: Right to Erasure (Article 17)
    public async Task<DeletionResult> DeleteUserDataAsync(
        Guid userId)
    {
        // 1. Anonymize completed orders
        await _orders.AnonymizeUserAsync(userId);

        // 2. Delete all active carts
        await _carts.DeleteUserCartsAsync(userId);

        // 3. Delete personal data from profiles
        await _users.DeletePersonalDataAsync(userId);

        // 4. Remove from marketing lists
        await _marketing.UnsubscribeAsync(userId);

        // 5. Log deletion for audit
        await _audit.LogAsync(new DataDeletionEvent
        {
            UserId = userId,
            DeletedAt = DateTime.UtcNow,
            RetainedData = new[]
            {
                "anonymized_orders (financial reporting)"
            }
        });

        return new DeletionResult
        {
            Success = true,
            DeletedAt = DateTime.UtcNow
        };
    }

    // GDPR: Right to Data Portability (Article 20)
    public async Task<UserDataExport> ExportUserDataAsync(
        Guid userId)
    {
        var user = await _users.GetAsync(userId);
        var orders = await _orders
            .GetUserOrdersAsync(userId);
        var carts = await _carts
            .GetUserCartHistoryAsync(userId);

        return new UserDataExport
        {
            Profile = user,
            Orders = orders,
            CartHistory = carts,
            ExportedAt = DateTime.UtcNow,
            Format = "JSON"
        };
    }
}
Critical Security Rule: Card numbers must NEVER touch our application servers. All card data is tokenized client-side using Stripe.js (or Stripe Elements). The browser sends the card details directly to Stripe, which returns a token. Our server only ever sees and stores the token — never the actual card number. This offloads the most sensitive part of PCI compliance to Stripe and reduces our PCI scope from SAQ D (most stringent) to SAQ A (simplest).

22. Monitoring, Observability & Cost Estimation

The checkout system must be deeply observable because failures directly impact revenue. Every component must emit metrics, logs, and traces that allow operators to quickly identify and resolve issues. A 5-minute undetected checkout outage during peak hours can cost thousands of dollars in lost sales. The monitoring strategy must cover infrastructure health, application performance, business metrics, and external dependency status.

Key Metrics Dashboard

MetricTypeAlert ThresholdDashboard Panel
Cart API p99 latencyHistogram> 200ms for 5 minLatency heatmap
Checkout API p99 latencyHistogram> 1000ms for 5 minLatency heatmap
Cart conversion rateCounter< 2% (hourly) for 30 minConversion funnel
Payment success rateCounter< 95% for 10 minPayment health
Orders per secondGauge< 50% of expected (anomaly)Throughput graph
Cart abandonment rateCounter> 80% (hourly) for 2 hoursAbandonment trend
Inventory reservation conflictsCounter> 100/min for 5 minConflict rate
Stripe API error rateCounter> 5% for 5 minPayment errors
Redis cache hit rateGauge< 95%Cache performance
Cart TTL expiry rateCounterAnomaly detectionCart lifecycle

Distributed Tracing

Every checkout request is traced from the initial API call through all service hops to the final order creation. We use OpenTelemetry for instrumentation and Jaeger for trace collection. A single checkout request trace might include the following hops:

Text
TraceID: abc123def456
|-- [12ms] POST /api/v1/checkout/{id}/payment
|   |-- [2ms] Auth middleware (JWT validation)
|   |-- [5ms] Redis: GET checkout:{id}
|   |-- [3ms] Redis: GET cart:{cartId}
|   |-- PARALLEL:
|   |   |-- [8ms] gRPC: InventoryService.Reserve()
|   |   |   |-- [5ms] PostgreSQL: INSERT reservation
|   |   |-- [12ms] gRPC: PricingEngine.Recalculate()
|   |   |   |-- [3ms] Redis: GET pricing-rules
|   |   |-- [45ms] HTTP: Stripe PaymentIntent.create()
|   |   |-- [15ms] gRPC: TaxService.Calculate()
|   |       |-- [10ms] HTTP: Avalara API
|   |-- [8ms] PostgreSQL: INSERT order
|   |-- [2ms] Kafka: PUBLISH OrderPlaced
|   |-- [1ms] Redis: DEL checkout:{id}
|-- Total: 95ms

Cost Estimation

ComponentSpecificationMonthly Cost
Cart API (ECS Fargate)6 tasks x 2 vCPU, 4GB RAM$520
Checkout API (ECS Fargate)4 tasks x 2 vCPU, 4GB RAM$347
Order API (ECS Fargate)3 tasks x 2 vCPU, 4GB RAM$260
Redis Cluster6 nodes x r6g.xlarge (26GB)$1,800
PostgreSQL (RDS)db.r6g.2xlarge, Multi-AZ, 1TB$1,400
Kafka (MSK)3 brokers x kafka.m5.large$450
Stripe fees15M transactions x 2.9% + $0.30Variable (per-transaction)
S3 (logs + backups)5TB storage$115
CloudWatch + X-RayLogs + traces + dashboards$300
WAF + ShieldDDoS protection + WAF rules$150
Total Infrastructure~$5,342
Cost Note: The infrastructure cost of approximately $5,342/month supports 15M orders/month. At an average order value of $50, this represents $750M in GMV (Gross Merchandise Value). The infrastructure cost as a percentage of GMV is approximately 0.0007% — an extremely favorable ratio. However, Stripe's per-transaction fees (2.9% + $0.30) represent a much larger cost at approximately $21.75M/month, which is a standard cost of doing business for e-commerce platforms.

Alerting Runbook

AlertSeverityFirst ResponseEscalation
Checkout p99 > 1s for 5minP1 (Critical)Check Stripe status page, check Redis connectivityPage on-call if Redis/Stripe down
Payment success rate < 95%P1Check Stripe dashboard for merchant-level issuesContact Stripe support
Cart API p99 > 200msP2Check Redis memory/connection countScale Redis cluster if needed
Inventory oversell detectedP1Pause inventory reservations, investigate race conditionHotfix required
Order count drop > 50%P1Check for service degradation, DNS issuesFull incident response

23. Testing Strategy

The checkout system requires comprehensive testing at multiple levels because failures directly impact revenue. A checkout bug that causes 1% of payments to fail on a platform processing $750M/month in GMV costs approximately $7.5M/month in lost revenue (at 2.9% take rate). Testing must cover functional correctness, performance under load, failure scenarios, and integration with external services. The testing strategy follows the testing pyramid with emphasis on integration tests that verify service interactions.

Testing Pyramid

LevelTypeCoverage TargetTools
UnitIndividual service logic90%+ code coveragexUnit, Moq, FluentAssertions
IntegrationService-to-service interactionsAll critical pathsTestcontainers (PostgreSQL, Redis)
ContractAPI contract verificationAll external API boundariesPact, WireMock
E2EFull checkout flowAll checkout scenariosPlaywright, Cypress
LoadPerformance under loadPeak traffic simulationk6, Gatling
ChaosFailure injectionAll critical failure modesChaos Monkey, Polly

Integration Test Example

C#
public class CheckoutFlowTests
    : IClassFixture<TestDatabase>
{
    private readonly TestDatabase _db;
    private readonly TestRedis _redis;
    private readonly HttpClient _client;

    public CheckoutFlowTests(
        TestDatabase db, TestRedis redis)
    {
        _db = db;
        _redis = redis;
        _client = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureServices(services =>
                {
                    services.AddScoped<IDbConnection>(_ =>
                        _db.GetConnection());
                });
            }).CreateClient();
    }

    [Fact]
    public async Task CompleteCheckoutFlow_ShouldCreateOrder()
    {
        // Arrange: Create cart with items
        var cart = await CreateCartWithItemsAsync(
            new[] { ("SKU-001", 2), ("SKU-002", 1) });

        // Act: Initialize checkout
        var initResponse = await _client.PostAsJsonAsync(
            "/api/v1/checkout/initialize",
            new { cart_id = cart.CartId });
        initResponse.EnsureSuccessStatusCode();
        var checkout = await initResponse.Content
            .ReadFromJsonAsync<CheckoutInitResponse>();

        // Act: Set shipping address
        var addressResponse = await _client
            .PutAsJsonAsync(
                $"/api/v1/checkout/{checkout.CheckoutId}/address",
                new
                {
                    street = "123 Main St",
                    city = "San Francisco",
                    state = "CA",
                    zip = "94102",
                    country = "US"
                });
        addressResponse.EnsureSuccessStatusCode();

        // Act: Select shipping method
        var shippingResponse = await _client
            .PutAsJsonAsync(
                $"/api/v1/checkout/{checkout.CheckoutId}/shipping",
                new { shipping_method = "ups_ground" });
        shippingResponse.EnsureSuccessStatusCode();

        // Act: Place order with test payment
        var orderResponse = await _client.PostAsJsonAsync(
            $"/api/v1/checkout/{checkout.CheckoutId}/payment",
            new
            {
                payment_method_id = "pm_card_visa",
                shipping_address = new { /* ... */ },
                billing_address = new { /* ... */ }
            });

        // Assert
        orderResponse.EnsureSuccessStatusCode();
        var order = await orderResponse.Content
            .ReadFromJsonAsync<OrderResponse>();
        Assert.NotNull(order.OrderId);
        Assert.Equal("confirmed", order.Status);

        // Verify inventory was reserved
        var inventory = await _db.QueryAsync
            <InventoryReservation>(
            "SELECT * FROM inventory_reservations " +
            "WHERE order_id = @id",
            new { id = order.OrderId });
        Assert.Equal(2, inventory.Count());

        // Verify cart was cleared
        var cartAfter = await _redis
            .GetCartAsync(cart.CartId);
        Assert.Null(cartAfter);
    }
}

Load Testing Scenario

JavaScript
// k6 load test for checkout flow
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
    scenarios: {
        checkout_load: {
            executor: 'ramping-vus',
            startVUs: 0,
            stages: [
                { duration: '2m', target: 100 },   // Ramp up
                { duration: '5m', target: 500 },   // Sustained
                { duration: '2m', target: 1000 },  // Peak
                { duration: '3m', target: 1000 },  // Hold peak
                { duration: '2m', target: 0 },     // Ramp down
            ],
        },
    },
    thresholds: {
        http_req_duration: ['p(95)<300', 'p(99)<500'],
        http_req_failed: ['rate<0.01'],
    },
};

export default function () {
    // 1. Add items to cart
    const cartRes = http.post(
        `${BASE_URL}/api/v1/carts/${cartId}/items`,
        JSON.stringify({
            product_id: 'prod_8xk2m9',
            quantity: Math.floor(Math.random() * 3) + 1
        }),
        { headers: { 'Content-Type': 'application/json' } }
    );
    check(cartRes, {
        'cart add 200': (r) => r.status === 200
    });

    // 2. Initialize checkout
    const initRes = http.post(
        `${BASE_URL}/api/v1/checkout/initialize`,
        JSON.stringify({ cart_id: cartId }),
        { headers: { 'Content-Type': 'application/json' } }
    );
    check(initRes, {
        'checkout init 200': (r) => r.status === 200
    });

    sleep(1);

    // 3. Place order
    const orderRes = http.post(
        `${BASE_URL}/api/v1/checkout/${checkoutId}/payment`,
        JSON.stringify({
            payment_method_id: 'pm_card_visa',
            shipping_address: testAddress
        }),
        { headers: { 'Content-Type': 'application/json' } }
    );
    check(orderRes, {
        'order placed': (r) => r.status === 200
    });
}
Test Data Management: Checkout tests must use Stripe test mode (test API keys) and sandbox payment methods. Never run tests against production Stripe credentials. Use Stripe's test card numbers (4242 4242 4242 4242 for success, 4000 0000 0000 0002 for decline) and test the full payment lifecycle including 3D Secure simulation.

24. Interview Q&A Deep Dive

Core Concepts

Q: How would you handle a scenario where a user adds 5 items to their cart, but by the time they reach checkout, 2 items are out of stock?

A: The system handles this at two levels. First, during checkout initialization, we check real-time inventory for all cart items and return an unavailable items list. The user sees which items are out of stock and can choose to proceed without them or go back to the cart. Second, if inventory goes to zero after checkout starts (between initialization and payment), the inventory reservation step will fail for those items, and the order placement returns an error with the specific unavailable items. The user must update their cart to remove the unavailable items before retrying. This graceful degradation approach is better than rejecting the entire checkout or silently removing items without the user's knowledge. We also send a real-time notification (via WebSocket) if an item becomes unavailable while the user is browsing, prompting them to update their cart before starting checkout.

Q: Design the inventory reservation system to prevent overselling during a flash sale with 100,000 concurrent users competing for 1,000 units of a product.

A: This is a classic contention problem. The naive approach (read-then-write) fails because the read and write are not atomic. The solution uses a distributed lock per SKU: all inventory operations for a given SKU are serialized through a Redis lock (SET NX EX with 5-second TTL). During the flash sale, this lock becomes a bottleneck, so we use several optimizations: (1) Pre-reserve inventory in batches — instead of reserving 1 unit at a time, reserve in blocks of 10-50 units and manage the sub-allocation in-memory. (2) Use optimistic concurrency with a version column — attempt the reservation, and if the version has changed, retry with exponential backoff. (3) Implement a queue — flash sale items go through a virtual queue where requests are processed in FIFO order, ensuring fairness. (4) Pre-warm Redis inventory counts before the flash sale starts to avoid cache miss storms. (5) Use a separate flash-sale service that handles the high contention path independently from the normal checkout flow, with its own database connection pool and Redis cluster.

Q: How would you design the cart merge flow when an anonymous user logs in with an existing authenticated cart?

A: Cart merging requires careful handling to avoid data loss or confusion. The merge algorithm: (1) Load both the anonymous cart and the user's existing cart. (2) For each item in the anonymous cart, check if the same product+variant already exists in the user's cart. If it does, sum the quantities (respecting per-product quantity limits). If not, add the item to the user's cart. (3) Preserve the most recently applied coupon from either cart (the authenticated cart's coupon takes precedence if both have one). (4) Recalculate all totals on the merged cart. (5) Mark the anonymous cart as "merged" (not deleted — we keep it for analytics). (6) Update Redis and PostgreSQL atomically. (7) Remove the anonymous cart from Redis cache. The merge is idempotent — calling it multiple times with the same anonymous cart produces the same result because the anonymous cart is marked as merged after the first merge and subsequent merges are no-ops.

Q: Explain the difference between authorization and capture in payment processing and why we use manual capture.

A: Authorization verifies that the customer's payment method is valid and has sufficient funds, and places a hold on the funds. Capture actually transfers the money from the customer's account to the merchant's account. We use manual capture (authorize first, capture later) because it provides a safety window between payment and fulfillment. If we authorized and captured immediately, we would have charged the customer even if we later discovered we could not fulfill the order (e.g., inventory reservation expired, shipping address is undeliverable, the order was flagged for fraud review). With manual capture, we can void the authorization (releasing the hold) without ever transferring money, which is a much cleaner customer experience than processing a refund. The capture typically happens when the order enters the "processing" state and inventory is confirmed as committed.

Q: How do you handle the scenario where a user's payment succeeds but the order creation fails due to a database error?

A: This is the "saga with compensation" pattern. The payment has already been authorized but not captured. The checkout orchestrator detects the order creation failure and triggers a compensation action: it voids the payment authorization, releases the inventory reservation, and returns an error to the user explaining that the order could not be placed. The user can retry. The voided authorization releases the hold on the customer's card within 3-5 business days (this is a bank-side process, not something we control). If the compensation itself fails (e.g., the void call to Stripe also fails), the payment reconciliation worker will detect the orphaned authorization during its periodic sweep and attempt to void it automatically. In the worst case, the authorization expires after 7 days without capture, and the funds are automatically released by the bank.

Q: How would you implement cart abandonment tracking for users who never log in (anonymous users)?

A: Anonymous cart abandonment tracking is challenging because we do not have the user's email address. Our approach uses multiple signals: (1) If the user has provided an email address during checkout (even if they did not complete it), we can send a recovery email. This is the most valuable recovery channel. (2) For users who never provided an email, we use browser push notifications (if the user has opted in) with a deep link back to their cart. (3) We use retargeting pixels (Facebook, Google) to serve display ads showing the items they left in their cart. (4) On subsequent visits (identified by the same session cookie), we display a "Welcome back! Your cart is waiting" banner with the exact items. The key insight is that anonymous abandonment recovery has much lower ROI than authenticated recovery, so we invest less in the infrastructure and more in the anonymous-to-authenticated conversion (e.g., "Log in to save your cart for later").

Advanced Topics

Q: How would you handle internationalization for the checkout flow, including multi-currency pricing, country-specific tax rules, and localized payment methods?

A: International checkout requires several layers of localization. (1) Currency: Prices are stored in the store's base currency, but displayed in the customer's local currency using real-time exchange rates (updated every 15 minutes). The final charge is in the store's base currency, and Stripe handles the conversion. (2) Tax: Tax calculation varies dramatically by jurisdiction. US states have different sales tax rules, EU has VAT, Canada has GST/PST/HST, and some countries have no sales tax at all. We delegate to Avalara or TaxJar for accurate tax calculation based on the shipping address. (3) Payment methods: Different countries prefer different payment methods. We support credit cards globally, but also add local methods: iDEAL (Netherlands), Bancontact (Belgium), SEPA (EU), Alipay (China), UPI (India), and BNPL services (Klarna, Afterpay) in supported regions. (4) Address format: Address formats vary by country. We use a dynamic address form that changes fields based on the selected country (e.g., UK addresses have a "postcode" field, Japanese addresses have different ordering). (5) Legal compliance: Different regions have different consumer protection laws (EU right to withdraw, GDPR consent requirements). The checkout flow must adapt to show the required legal disclosures based on the customer's location.

Q: Describe the failure modes of the checkout system and how you would handle each one.

A: The checkout system has several critical failure modes, each with a specific mitigation strategy: (1) Redis failure: Cart reads fall back to PostgreSQL (with slightly higher latency). Cart writes are buffered in a local queue and flushed when Redis recovers. Checkout sessions stored in Redis are reconstructed from the checkout database. (2) Payment gateway failure (Stripe down): Circuit breaker trips after 5 failures in 10 seconds. Users see "Payment service temporarily unavailable, please try again in a few minutes." No orders are lost because payment was not authorized. (3) Inventory service failure: The checkout orchestrator uses a cached inventory count (30-second TTL) and proceeds with a warning that "stock levels may not be up to date." If the reservation fails, the order is placed on a retry queue. (4) Tax service failure: Fall back to cached tax rates for the shipping jurisdiction. If no cache exists, show a warning that "Tax will be calculated after order placement" and use the last known rate for the jurisdiction. (5) Database failure: Order creation is retried on a different database replica. If all replicas are down, the order is written to a local file and replayed when the database recovers. (6) Network partition between services: Each service degrades gracefully using cached data. The checkout orchestrator returns a partial response with a warning flag, and the client can retry or proceed with reduced functionality.

Q: How would you design the system to handle "buy now, pay later" (BNPL) options like Klarna or Afterpay?

A: BNPL integration adds complexity because the payment is not captured immediately — the BNPL provider pays the merchant upfront but collects from the customer in installments. Our system handles BNPL as a separate payment method type with these differences: (1) The payment authorization goes through the BNPL provider's API instead of Stripe. (2) The capture happens immediately (not deferred) because the BNPL provider guarantees payment. (3) Refunds must be processed through the BNPL provider, not Stripe. (4) The order state machine has a "BNPL Active" sub-state that tracks whether the customer is still making installment payments. (5) If a customer disputes a BNPL charge, the dispute is routed to the BNPL provider's dispute system, not our payment team. We abstract these differences behind an IPaymentProvider interface so the checkout orchestrator does not need to know whether the customer paid with a credit card, Apple Pay, or Klarna.

Interview Tip: When discussing the checkout system in an interview, always emphasize the trade-offs between consistency and availability. The checkout system must be highly available (every minute of downtime loses revenue) but also consistent (we cannot oversell inventory or double-charge customers). The key insight is that we use strong consistency for financial operations (payments, inventory) and eventual consistency for everything else (cart state, analytics, notifications). This mixed approach gives us the best of both worlds.

E-Commerce Checkout & Cart System — Senior+ Guide | Ayodhyya