Designing a Real-Time, Cross-Device Shopping Cart Sync System

Designing a Real-Time, Cross-Device Shopping Cart Sync System

Designing a Real-Time, Cross-Device Shopping Cart Sync System

A shopper adds a jacket to their cart on the subway using their phone. Twenty minutes later they open their laptop at home — and the jacket is already sitting in the cart, waiting for checkout. This guide designs that system end to end: the architecture, the sync algorithms, the databases, and the exact questions an interviewer will ask you about it.

01

Introduction & History

Shopping carts are one of the oldest metaphors in e-commerce, and also one of the most quietly complicated engineering problems on any retail platform. What looks like a simple “list of items” on the surface hides a genuine distributed-systems challenge underneath — and that is exactly what makes this problem a favorite for system design interviews.

In the earliest days of the web, a “cart” was nothing more than a browser cookie or a server-side session — the moment you closed the tab or switched to a different computer, the cart was gone. That was acceptable in an era when most people shopped from a single desktop computer. It is completely unacceptable today, when the average shopper moves fluidly between a phone on the train, a tablet on the couch, and a laptop at a desk, often within the same hour, and expects their cart to simply follow them.

The shift began in the mid-2000s as e-commerce companies started tying carts to logged-in user accounts instead of anonymous browser sessions, so a cart could at least survive a login on a different device. But “surviving a login” and “syncing in real time” are very different engineering problems. Early cross-device carts required a manual page refresh to see items added elsewhere. Real, live synchronization — where an item added on a phone appears on an already-open laptop tab within a second or two, with no refresh — is a genuinely modern capability, built on the same real-time infrastructure (WebSockets, push notification services, event streaming) that also powers live chat, multiplayer games, and collaborative documents.

This class of problem sits at the intersection of two classic distributed systems challenges: state synchronization (keeping the same logical piece of data consistent across multiple independent copies) and conflict resolution (deciding what to do when two devices change the same data at nearly the same moment, such as a phone and a laptop both trying to change the quantity of the same item within a second of each other). Both problems have been studied for decades in distributed databases and real-time collaborative software, and this guide borrows heavily from that body of knowledge — CRDTs, vector clocks, eventual consistency, and optimistic concurrency — and applies it specifically to something as everyday as a shopping cart.

Real-Life Analogy

Think of a shared grocery list on your refrigerator that two people in a household can both edit — except this version magically updates itself the instant either person writes on it, no matter where they are standing. If you cross “milk” off the list at the store, your partner’s copy at home updates itself immediately, without either of you needing to call each other. A cross-device cart is exactly that: the same logical list, kept perfectly in sync across every device where the same person is logged in, with changes appearing everywhere within roughly a second.

A short history of cart evolution

EraMilestone
1994–1999The first web storefronts (NetMarket, early Amazon) implement carts as cookie- or session-scoped state on a single machine — close the browser and the cart is gone.
2000–2007Carts start being tied to logged-in user accounts. A cart survives login on another device, but you still have to refresh the page manually to see the latest state.
2008–2014Mobile explodes. Users routinely start a purchase on one device and finish on another; retailers begin investing in genuine cross-device persistence and background sync.
2015–2020Real-time infrastructure (WebSockets, managed push, event streaming) matures. Cart changes propagate to every open device within seconds, without a refresh.
PresentCarts are treated as a first-class distributed data structure — versioned, offline-tolerant, conflict-resolved — with strict, stronger-consistency handling reserved specifically for the checkout moment.

Over the last decade, nearly every major retailer — Amazon, Walmart, Target, eBay, Shopify-powered storefronts — has invested heavily in this exact capability, because a cart that doesn’t follow the shopper directly threatens revenue: if someone adds three items on their phone during a commute and then can’t find them later on their laptop, a meaningful percentage of those shoppers simply never complete the purchase at all. This guide walks through designing that system end to end.

💡
Why this topic matters for system design interviews

This problem combines several classically-hard system design themes in one place: real-time push infrastructure, distributed state synchronization, conflict resolution across replicas, mobile offline tolerance, and a hard consistency edge case at checkout — all in the guise of something as familiar as an “add to cart” button. It surfaces regularly at retail, marketplace, and platform-focused engineering interviews (Amazon, Walmart, Shopify, eBay, Instacart, DoorDash), and answering it well requires demonstrating fluency in trade-offs, not just naming technologies.

02

The Problem & Why It Matters

2.1 Precise problem statement

Design a shopping cart backend where:

  • A logged-in user can add, remove, or change the quantity of items from any device (phone, tablet, laptop, in-store kiosk).
  • Every other device the same user currently has open reflects that change within roughly one to two seconds, without a manual refresh.
  • The cart persists indefinitely (or until checkout/expiry) even if all devices are closed — reopening any device later still shows the correct, latest cart.
  • Concurrent edits from two devices at nearly the same instant are resolved sensibly, without silently losing an item one of the devices added.
  • The system stays correct and available even under partial failures — a flaky mobile network, a dropped WebSocket connection, a backend node restarting mid-request.

2.2 Why this is harder than it sounds

At first glance, a shopping cart looks like “just a list stored in a database.” The difficulty comes from three compounding factors:

FactorWhy it complicates the design
Multiple simultaneous writersUnlike a single-device app, more than one device can modify the exact same cart at the exact same time, creating classic race-condition and conflict scenarios that a single-writer design would never need to handle.
Real-time expectationUsers expect near-instant propagation, not “eventually, next time you refresh” — this rules out simple polling-based designs for the common case and pushes toward persistent, push-based connections.
Unreliable mobile networksA phone can lose connectivity mid-edit (going into a tunnel, switching from WiFi to cellular); the system must handle reconnection and resynchronization gracefully rather than silently dropping changes.
💰
Why this matters to the business

Cart abandonment is already one of the largest sources of lost revenue in e-commerce, and a broken or inconsistent cross-device cart experience actively makes it worse. If a shopper adds items on one device and cannot find them on another, some fraction of those shoppers assume the platform lost their selections and simply give up rather than re-adding everything from memory. Conversely, a cart that reliably and instantly follows the shopper across devices measurably increases completed checkouts, because it removes friction at exactly the moment purchase intent is highest.

2.3 What “good” looks like

RequirementTargetWhy it’s hard
Sync latencyChanges visible on other open devices within roughly 1–2 secondsRequires persistent server-push infrastructure, not just periodic polling
No lost updatesTwo near-simultaneous edits from different devices must never silently overwrite each otherDemands explicit conflict resolution beyond “last write wins”
Offline resilienceDevice that goes offline mid-session reconciles correctly on reconnectClient-side queuing plus server-side idempotency and merge logic
Consistent cart at checkoutThe device initiating checkout must see the true, fully merged, up-to-date cartRequires switching from eventual consistency to strict consistency for this single moment
AvailabilityCart reads and writes stay available under partial failureEvery dependency (cache, database, WebSocket layer) needs its own fallback

2.4 Functional requirements

Before drawing any boxes and arrows, it helps to write down, in plain language, exactly what the system must be able to do. This becomes the checklist every architectural decision downstream gets measured against.

  • Add, remove, and update-quantity operations on cart line items, from any authenticated device.
  • Real-time propagation of changes to every currently connected device belonging to the same user.
  • Durable persistence of the cart independent of any specific device or session.
  • Deterministic, product-aware conflict resolution when concurrent edits collide.
  • Safe offline queuing on mobile clients with automatic replay on reconnect.
  • A stricter, blocking-consistency mode specifically during checkout.
  • Re-validation of price and stock at both add-time and checkout, never trusting client-supplied pricing.
💬
What an interviewer may ask

“What happens if a user adds an item on their phone while offline, and someone (or another device) removes that same item from the server-side cart before the phone reconnects?” A strong answer recognizes this as a genuine conflict scenario and proposes a concrete resolution policy — for example, treating additions as generally safe to replay (an “add” rarely needs to be silently dropped), while destructive actions like quantity reduction or removal use timestamp-based or version-based conflict resolution, explained in detail in Section 07.

03

Core Concepts You Must Know

Every term below reappears constantly in the rest of this guide, so take the time to internalize each one with its analogy. By the end of Section 04, none of the labels on the architecture diagram should feel unfamiliar.

3.1 WebSockets

What it is: A persistent, two-way network connection between a client and a server that stays open, allowing either side to send messages at any time without the client needing to repeatedly ask “anything new?” Why it exists: Traditional HTTP requires the client to initiate every exchange; WebSockets let the server push data the instant something changes. Where it’s used: Live chat apps, multiplayer games, stock tickers, and cross-device cart sync. Analogy: A regular HTTP request is like mailing a letter and waiting for a reply letter each time you have a question. A WebSocket is like an open phone line that stays connected, so either person can speak up the moment they have something to say. Example: A laptop keeps an open WebSocket connection to the Cart Sync Service; the instant the phone adds an item, the server pushes a small “cart updated” message down that same open connection to the laptop.

3.2 Eventual consistency

What it is: A consistency model where, after writes stop occurring, all replicas of a piece of data will eventually converge to the same value — but at any given instant, different replicas might briefly disagree. Why it exists: Enforcing perfect, instant agreement across every replica on every write (strong consistency) is expensive and can hurt availability; eventual consistency trades a small, usually sub-second window of possible disagreement for much better availability and performance. Where it’s used: Shopping carts, social media like-counts, DNS. Analogy: If you tell three friends the same piece of news by phone one after another, there’s a brief window where one friend knows and the other two don’t yet — but within a few minutes, all three know the same thing. Example: The instant after adding an item on mobile, the laptop’s locally cached cart view might be stale for a fraction of a second until the push notification and refreshed data arrive.

3.3 Conflict-Free Replicated Data Types (CRDTs)

What it is: A family of data structures specifically designed so that multiple copies can be updated independently and later merged automatically into a consistent result, with no coordination required at write time and no possibility of a merge conflict. Why it exists: In a system where two devices can edit the same cart while offline from each other, you need a mathematically guaranteed way to merge their changes back together without manual conflict resolution. Where it’s used: Collaborative text editors (like Google Docs’ underlying sync tech), distributed databases like Redis with CRDT support, and shopping carts. Analogy: Imagine two people independently adding stickers to two copies of the same sticker book while apart; if the rule is simply “the final book contains every sticker either person added,” merging the books later is trivial and never creates a conflict — that’s the spirit of a CRDT, applied to cart line items. Example: A cart modeled as a CRDT “grow-only set with removal tombstones” lets each device add items freely offline; merging two devices’ changes later simply unions the additions and applies any removals, with a well-defined rule for what happens if the same item was both added and removed by different devices.

3.4 Optimistic UI updates

What it is: Updating the user interface immediately when the user takes an action (like tapping “Add to Cart”), before waiting for server confirmation, then quietly correcting the UI if the server ultimately rejects or modifies the change. Why it exists: Waiting for a full network round-trip before showing any feedback makes an app feel slow and unresponsive; optimistic updates make the app feel instant. Where it’s used: Nearly every modern mobile app, including cart and “like” buttons. Example: Tapping “Add to Cart” instantly shows the item in the cart icon’s count, while the actual server write happens in the background milliseconds later.

3.5 Idempotency

What it is: A property of an operation meaning that performing it multiple times has the same effect as performing it once. Why it exists: Unreliable networks mean a client sometimes can’t tell whether a request succeeded or simply the response was lost; if the client safely retries, idempotency guarantees the retry doesn’t cause a duplicate effect (like adding the same item twice by accident). Where it’s used: Payment processing, cart mutations, any operation that might be retried. Example: An “add item” request carries a unique client-generated request ID; if the same request ID arrives twice (because the client retried after a timeout), the server recognizes the duplicate and applies the change only once.

3.6 Vector clocks / logical versioning

What it is: A mechanism for tracking the relative order of events across multiple independent devices without relying on wall-clock time (which can differ slightly between devices), typically by attaching a version number or a small per-device counter to every change. Why it exists: Wall-clock timestamps from different devices can’t always be trusted to determine “which change happened first,” especially with clock drift; logical versioning gives a more reliable way to detect and order conflicting changes. Where it’s used: Distributed databases like DynamoDB and Riak, collaborative applications. Example: Each cart carries a version number that increments on every change; when a device submits an update, it includes the version it last saw — if that version is stale by the time the update reaches the server, the server knows a conflicting change happened elsewhere first and can trigger the merge logic.

3.7 Push notification / fan-out service

What it is: A service responsible for delivering a message to all of a user’s currently-connected devices, not just one. Why it exists: A user might have the app open on three devices simultaneously; a cart change needs to reach all of them, not just the device that isn’t currently the one that made the change. Example: After a phone adds an item, the Fan-out Service looks up every open WebSocket connection registered to that user ID and pushes the update to each one, including the phone itself (for confirmation) and the laptop.

3.8 Distributed locking

What it is: A mechanism that ensures only one process (or device, in this context) can hold exclusive access to a specific piece of data at a time, implemented in a way that works correctly even though the “processes” involved are running on entirely separate machines with no shared memory. Why it exists: Some operations — most notably checkout, discussed in Section 07.4 — are too risky to leave to eventual consistency and instead need a brief window of exclusive access to guarantee correctness. Where it’s used: Checkout flows, inventory reservation systems, distributed job schedulers. Analogy: A “occupied” sign on a single-user restroom is a simple distributed lock — anyone approaching sees the sign and waits, rather than two people trying to use the same space at once, and the sign is removed (the lock released) as soon as the person is done. Example: DynamoDB’s conditional write feature is used to implement a short-lived “checkout in progress” lock on a cart, ensuring no other device can mutate that cart’s contents until checkout completes or the lock expires.

04

Architecture & Components

Below is the full architecture. Every box names exactly what kind of component it is — client, edge, gateway, load balancer, service, cache, database, or queue — so nothing is ambiguous.

flowchart TB Mobile[“Mobile Client
iOS / Android App”] Laptop[“Laptop Client
Web Browser (open tab)”] Kiosk[“In-Store Kiosk Client”] LB[“Load Balancer
(L7 – AWS ALB / NGINX)
Distributes HTTP + WebSocket upgrade traffic”] GW[“API Gateway
(Kong / AWS API Gateway)
AuthN, rate limiting, REST routing”] WSGW[“WebSocket Gateway
(Managed connections layer)
Maintains persistent client connections”] Mobile –>|REST: add/remove item| LB Laptop –>|WebSocket: live updates| LB Kiosk –>|REST: add/remove item| LB LB –> GW LB –> WSGW subgraph CartCore[“Cart Service Cluster (Microservices)”] CartSvc[“Cart Service
Handles add/remove/update-quantity requests”] MergeSvc[“Conflict Resolution Service
CRDT merge / version reconciliation”] FanoutSvc[“Fan-out Service
Pushes updates to all connected devices for a user”] PricingSvc[“Pricing and Inventory Service
Validates price + stock at read/write time”] end GW –> CartSvc CartSvc –> MergeSvc CartSvc –> PricingSvc MergeSvc –> FanoutSvc FanoutSvc –> WSGW WSGW –>|push cart update| Laptop WSGW –>|push cart update| Mobile subgraph Caches[“Caching Layer”] RedisCart[“Redis Cache
Hot cart state (per userId), sub-ms reads”] RedisConn[“Redis Cache
WebSocket connection registry (userId to gateway node)”] end CartSvc –> RedisCart FanoutSvc –> RedisConn subgraph Stores[“Persistent Data Stores”] DynamoCart[“DynamoDB
Durable Cart Store (source of truth, versioned)”] PgCatalog[“PostgreSQL
Product Catalog, Price, Inventory”] Cassandra[“Cassandra
Cart Change Event Log (audit + replay)”] end CartSvc –> DynamoCart PricingSvc –> PgCatalog MergeSvc –> Cassandra subgraph Async[“Event Streaming and Async Processing”] Kafka[“Message Queue
(Apache Kafka)
Cart change events”] AbandonJob[“Batch Job
Abandoned Cart Detection + Reminder Trigger”] AnalyticsJob[“Stream Processor
(Flink) Real-time cart analytics”] end CartSvc -.publish change event.-> Kafka Kafka –> AbandonJob Kafka –> AnalyticsJob Kafka –> Cassandra
Diagram 1 — End-to-end architecture of the cross-device cart sync system.

Let’s walk through each layer.

Client

Client Layer

Three kinds of clients: the mobile app, a web browser tab on a laptop, and an in-store kiosk. Mobile and kiosk typically issue standard REST calls for cart mutations (simpler to implement reliably on constrained or public devices), while the laptop’s browser tab keeps a live WebSocket connection open to receive instant push updates while the tab stays open.

Edge

Load Balancer

Distributes both regular HTTP traffic and WebSocket upgrade requests across the gateway fleet, using health checks to route around unhealthy nodes. WebSocket connections require sticky routing considerations — once a client’s WebSocket handshake completes on a specific gateway node, that connection stays pinned to that node for its lifetime, since a WebSocket is a stateful, long-lived connection rather than a stateless request.

Gateway

API Gateway

Handles authentication, rate limiting, and routing for standard REST cart operations (add item, remove item, change quantity) coming from mobile and kiosk clients.

Gateway

WebSocket Gateway

A separate, specialized gateway layer that manages persistent client connections. It authenticates the WebSocket handshake once, then keeps the connection open, forwarding server-pushed messages down to the client and forwarding any client-sent messages (like live typing indicators, not usually cart mutations) up to the backend. Because WebSocket connections are stateful and long-lived, this layer is scaled and monitored differently from the stateless REST API Gateway.

Service

Cart Service

The core service that processes every add/remove/update-quantity request. It reads and writes the authoritative cart state, coordinates with the Pricing & Inventory Service to validate that an item is still available and correctly priced, and publishes a change event for every mutation.

Service

Conflict Resolution Service

Invoked whenever the Cart Service detects that an incoming change might conflict with another recent change to the same cart (typically identified by a stale version number). Applies the CRDT-based or version-based merge logic detailed in Section 07, and produces the reconciled, authoritative cart state.

Service

Fan-out Service

Looks up every device currently connected for a given user (via the Redis connection registry) and pushes the newly updated cart state to all of them through the WebSocket Gateway — including back to the originating device, which is how a mobile app confirms its own optimistic UI update actually succeeded.

Service

Pricing & Inventory Service

Validates, on every cart mutation, that the item is still in stock and that the price shown matches the current catalog price — critical because prices and stock levels change independently of the cart itself, and a cart should never let a user check out with stale pricing.

Cache

Caching Layer

Two distinct Redis caches: one holding hot cart state for fast reads (avoiding a database hit on every cart view), and one acting as a connection registry mapping each user ID to which WebSocket Gateway node(s) currently hold an open connection for them — essential for the Fan-out Service to know where to push updates.

Storage

Persistent Data Stores

DynamoDB (a managed, horizontally scalable key-value store) holds the durable, versioned cart record — the true source of truth. PostgreSQL holds the relational product catalog, pricing, and inventory data. Cassandra holds the full append-only log of every cart change event, used for auditing, debugging, and replay.

Async

Event Streaming & Async Processing

Every cart mutation is published to Kafka. Downstream, a batch job detects abandoned carts (no activity for a defined window) to trigger reminder emails or notifications, while a stream processor computes real-time analytics such as cart-add rates per product.

💬
What an interviewer may ask

“Why use a separate WebSocket Gateway instead of pushing everything through the same API Gateway used for REST calls?” A strong answer: WebSocket connections are long-lived and stateful, requiring sticky session routing and very different scaling characteristics (connection count matters more than request throughput) than the stateless, short-lived REST API Gateway — separating them lets each be scaled, deployed, and tuned independently.

05

Internal Working

5.1 Step-by-step walkthrough: adding an item on mobile

1

User taps “Add to Cart” on mobile

The mobile client immediately updates its local UI optimistically (Section 3.4), before any server response arrives.

2

Request sent

The mobile client sends a REST request through the Load Balancer and API Gateway to the Cart Service, including a unique idempotency key and the cart’s last-known version number.

3

Validation

The Cart Service calls the Pricing & Inventory Service to confirm the item is in stock and to fetch the current price.

4

Version check

The Cart Service compares the version number the mobile client sent against the current authoritative version in DynamoDB. If they match, the write proceeds cleanly. If they don’t match (meaning another device changed the cart in between), the request is routed to the Conflict Resolution Service.

5

Write

The Cart Service writes the updated cart to DynamoDB and increments its version number, then publishes a change event to Kafka.

6

Cache update

The Redis hot-cart cache is updated (write-through) so subsequent reads don’t need to hit DynamoDB.

7

Fan-out

The Fan-out Service looks up all currently connected devices for this user in the Redis connection registry and pushes the updated cart down every open WebSocket connection — including the laptop, if it has a tab open.

8

Laptop receives push

The laptop’s open browser tab receives the push message over its existing WebSocket connection and updates its UI — typically within one to two seconds of the original tap on mobile, with no refresh needed.

Real-Life Analogy

This is very similar to how a restaurant’s kitchen display system works. A waiter enters an order on a tablet (mobile add-to-cart); the order is validated against what’s actually available in the kitchen (inventory check); it’s recorded on the central order log (durable write); and it instantly appears on every screen that needs to see it — the kitchen screen, the expo screen, the manager’s tablet — all at once, without anyone needing to walk over and check manually.

5.2 What happens when a device is offline

If the mobile device is offline when the user taps “Add to Cart,” the optimistic UI update still happens locally, but the request queues locally on the device instead of being discarded. When connectivity returns, the client replays the queued request (using its idempotency key, so it’s safe even if part of it had actually gone through before the connection dropped). If the cart changed on another device in the meantime, the version mismatch triggers the Conflict Resolution Service, which merges the offline device’s queued change with whatever happened elsewhere.

5.3 Why every step matters

Each step above corresponds to one architectural decision worth defending in an interview. The optimistic UI update earns responsiveness at the cost of a small reconciliation risk. The idempotency key earns safe retries at the cost of the client having to generate and remember a stable ID. The version check earns cheap conflict detection at the cost of the client having to track the version. The durable write earns correctness at the cost of one extra round trip before acknowledgement. The fan-out earns cross-device propagation at the cost of maintaining a connection registry. Together they form a coherent whole — but each individual choice would look arbitrary if isolated from the rest.

06

Data Flow & Lifecycle

sequenceDiagram participant M as Mobile Client participant L as Laptop Client (WebSocket open) participant LB as Load Balancer participant GW as API Gateway participant CS as Cart Service participant PS as Pricing/Inventory Service participant DB as DynamoDB (Cart Store) participant FO as Fan-out Service participant WS as WebSocket Gateway participant KQ as Kafka M->>M: Optimistic UI update (instant) M->>LB: POST /cart/items (idempotency key, version) LB->>GW: Route request GW->>CS: Add item request CS->>PS: Validate stock + price PS–>>CS: OK, current price CS->>DB: Write updated cart (version+1) DB–>>CS: Write confirmed CS->>KQ: Publish cart-changed event (async) CS->>FO: Trigger fan-out (userId) FO->>WS: Push updated cart WS–>>L: Deliver update over open WebSocket L->>L: Update UI (no refresh needed) CS–>>GW: 200 OK GW–>>LB: Response LB–>>M: Confirm add (reconcile optimistic UI if needed)
Diagram 2 — End-to-end request lifecycle for a single “add to cart” action.

6.1 Cart lifecycle states

State 1

Active

Cart has items and recent activity; fully synced across devices in real time.

State 2

Idle / Abandoned

No activity for a defined window (commonly a few hours to a few days); the Abandoned Cart batch job may trigger a reminder notification.

State 3

Checked out

The cart’s contents are converted into an order; the cart record is cleared or archived, and any devices with the cart open are pushed a “cart cleared” update.

State 4

Expired

After a longer window of inactivity (platform-specific, often 30–90 days) or if items go out of stock, the cart may be automatically pruned or flagged for the user to review before checkout.

6.2 Read path vs. write path

The read path (opening the cart page, refreshing the cart icon count) is designed for the fast, common case: Redis first, DynamoDB on a miss, with the response served in a few milliseconds. The write path (add/remove/quantity change) is designed for correctness: it always goes through the version check, the durable write, and the fan-out. Separating these two mental models — even though they touch the same components — is what keeps the design coherent as new features (wishlists, save-for-later, shared carts) get layered on top of it.

💬
What an interviewer may ask

“What happens to a cart item if the product goes out of stock or its price changes while it’s sitting in someone’s cart?” A strong answer describes re-validating price and stock at read time (whenever the cart is displayed) and again at checkout time — never trusting a price or availability that was only checked at add-to-cart time, since carts can sit for days — and surfacing a clear “price changed” or “no longer available” notice to the user rather than silently charging a different amount.

07

Sync & Conflict Resolution Algorithms

7.1 Optimistic concurrency control with version numbers

The simplest and most widely used approach: every cart carries a monotonically increasing version number. A client must include the version it last read when submitting a change; if the server’s current version doesn’t match, the write is rejected and the conflict resolution path kicks in instead of silently overwriting data.

CartVersionCheck.java — optimistic concurrency control for cart writesjava
public class CartVersionCheck {

    public static class VersionConflictException extends RuntimeException {
        public VersionConflictException(String message) {
            super(message);
        }
    }

    // Attempts a conditional write; throws if the version has moved on
    public CartRecord applyChange(CartRecord currentServerCart,
                                   CartChangeRequest request) {

        if (currentServerCart.getVersion() != request.getExpectedVersion()) {
            // Someone else changed the cart since this client last read it
            throw new VersionConflictException(
                "Expected version " + request.getExpectedVersion() +
                " but server is at version " + currentServerCart.getVersion());
        }

        CartRecord updated = currentServerCart.copy();
        updated.applyItemChange(request.getItemId(), request.getQuantityDelta());
        updated.setVersion(currentServerCart.getVersion() + 1);
        updated.setLastModifiedTimestamp(System.currentTimeMillis());
        updated.setLastModifiedDeviceId(request.getDeviceId());

        return updated;
    }
}

7.2 CRDT-style merge for offline-tolerant conflict resolution

When a version conflict is detected (or when reconciling a device that queued changes while offline), a naive “last write wins” approach risks silently discarding a legitimate addition from one of the two devices. A CRDT-inspired merge instead treats the cart as a set of line-item operations that can be combined deterministically, regardless of the order they arrive in.

CartCrdtMerger.java — grow-set-with-tombstones mergejava
import java.util.*;

public class CartCrdtMerger {

    public static class LineItem {
        String itemId;
        int quantity;
        long lastModified;
        String deviceId;

        LineItem(String itemId, int quantity, long lastModified, String deviceId) {
            this.itemId = itemId;
            this.quantity = quantity;
            this.lastModified = lastModified;
            this.deviceId = deviceId;
        }
    }

    // Merge two cart states (e.g. server's authoritative cart and a
    // device's locally-queued offline changes) into one reconciled cart.
    public Map<String, LineItem> merge(
            Map<String, LineItem> cartA,
            Map<String, LineItem> cartB,
            Set<String> removedItemIdsA,
            Set<String> removedItemIdsB) {

        Map<String, LineItem> merged = new HashMap<>();
        Set<String> allItemIds = new HashSet<>();
        allItemIds.addAll(cartA.keySet());
        allItemIds.addAll(cartB.keySet());

        for (String itemId : allItemIds) {
            boolean removedInA = removedItemIdsA.contains(itemId);
            boolean removedInB = removedItemIdsB.contains(itemId);
            LineItem itemFromA = cartA.get(itemId);
            LineItem itemFromB = cartB.get(itemId);

            if (removedInA && removedInB) {
                continue; // both removed it - stays removed
            }
            if (removedInA && itemFromB != null
                    && itemFromB.lastModified > removedTimestamp(removedItemIdsA, itemId)) {
                merged.put(itemId, itemFromB); // B re-added after A's removal
                continue;
            }
            if (removedInB && itemFromA != null
                    && itemFromA.lastModified > removedTimestamp(removedItemIdsB, itemId)) {
                merged.put(itemId, itemFromA); // A re-added after B's removal
                continue;
            }
            if (removedInA || removedInB) {
                continue; // one side removed, the other never re-added
            }

            // Neither side removed it - merge by taking the higher-quality winner
            if (itemFromA != null && itemFromB != null) {
                LineItem winner =
                    itemFromA.lastModified >= itemFromB.lastModified ? itemFromA : itemFromB;
                merged.put(itemId, winner);
            } else {
                merged.put(itemId, itemFromA != null ? itemFromA : itemFromB);
            }
        }

        return merged;
    }

    private long removedTimestamp(Set<String> removedSet, String itemId) {
        // In a real implementation, tombstones carry their own timestamp;
        // simplified here for clarity.
        return 0L;
    }
}

The key design decision worth calling out: this merge strategy treats additions as generally safe to preserve and uses timestamped tombstones to make sure a removal isn’t accidentally undone by a stale, late-arriving add from a device that was offline when the removal happened — and vice versa. This is a deliberate, product-level choice about what “correct” merge behavior means for a shopping cart specifically (favoring not losing something the user wanted to buy) rather than a purely mechanical one.

7.3 Choosing between strategies

StrategyBest forTrade-off
Last-Write-Wins (simple timestamp)Low-stakes fields (e.g., which variant/color is selected)Simple, but can silently discard a legitimate concurrent change
Optimistic concurrency (version check + reject)Systems where conflicts are rare and immediate feedback to the user is acceptableRequires the client to handle retry/merge logic on conflict
CRDT-style automatic mergeOffline-tolerant, multi-device carts where conflicts are common and must resolve silentlyMore complex to implement and reason about correctly
💬
What an interviewer may ask

“Why not just use ‘last write wins’ based on timestamp for everything?” A strong answer explains that last-write-wins is simple but dangerous for additive actions — if a phone adds Item A offline and, in the meantime, a laptop adds Item B, a naive last-write-wins merge of the whole cart object could silently discard one of the two additions entirely, rather than including both, which is not what either device’s user actually intended.

7.4 The simultaneous checkout race

A particularly tricky edge case worth designing for explicitly: what if a user starts the checkout process on their laptop while, at the very same moment, they (or a shared-cart family member) removes an item from the same cart on their phone? If checkout has already locked in a snapshot of the cart before the removal arrives, the user could end up being charged for an item they just removed seconds earlier.

The standard solution is to treat checkout initiation as acquiring a short-lived, explicit lock on the cart — typically implemented as a conditional write in DynamoDB that sets a “checkout in progress” flag with a brief expiry (for example, five minutes, in case the user abandons checkout partway through). While this flag is set, any cart-mutation request from another device is either rejected with a clear “checkout in progress” response, or queued and applied only after the checkout completes or the lock expires. This is one of the few places in the entire system where the design deliberately favors strict consistency and blocking behavior over the eventual-consistency, always-available approach used everywhere else, precisely because the financial stakes at that specific moment are high enough to justify the trade-off.

Common failure mode

A subtle but costly mistake is applying the CRDT-style merge logic during checkout the same way it’s applied during normal browsing. Silently merging a stale add-item request into a cart that’s already mid-checkout can result in a customer being charged for something they never intended to buy at that moment, or a checkout total that doesn’t match what the customer saw on screen when they confirmed the purchase. Checkout deserves its own, stricter consistency rules layered on top of the general-purpose sync system described in the rest of this guide.

08

Advantages, Disadvantages & Trade-offs

✓ Advantages of this architecture

  • Genuine real-time sync (roughly 1–2 second propagation) rather than “eventually, on next refresh,” which meaningfully improves the shopping experience.
  • Offline-tolerant — devices can queue changes locally and reconcile cleanly on reconnect, rather than failing outright.
  • Conflict resolution is deterministic and favors not losing intentional user actions, rather than silently discarding data.
  • Each layer (REST gateway, WebSocket gateway, cart logic, conflict resolution, fan-out) scales and fails independently.
  • The design has a clear, defensible answer for every “what happens if…” question an interviewer can throw at it.

✗ Disadvantages & limitations

  • Persistent WebSocket connections are more operationally complex than stateless REST — they need sticky routing, connection-count-aware scaling, and careful reconnection handling.
  • CRDT-style merge logic is genuinely harder to implement correctly and test than simple overwrite semantics.
  • More infrastructure (a dedicated WebSocket gateway, a connection registry cache) than a simpler polling-based design would require.
  • Real-time push doesn’t eliminate the need for careful re-validation of price/stock at checkout — sync speed and business correctness are separate concerns.

8.1 Key trade-off: WebSockets vs. polling

ApproachProCon
WebSocket pushNear-instant updates, low overhead once connected, no wasted requests when nothing changesMore complex infrastructure; connections consume server resources even when idle
Short-interval pollingSimple, stateless, easy to scale with standard REST infrastructureEither wastes bandwidth polling when nothing changed, or has noticeably higher latency if the interval is lengthened to save resources

Most production systems use WebSockets (or a similar push mechanism like Server-Sent Events) for the actively-open-app case, while falling back to a lightweight “refresh on app foreground” check for cases where maintaining a persistent connection isn’t practical, such as a mobile app that’s been backgrounded for a long time.

8.2 Key trade-off: strong consistency vs. availability at checkout

Throughout normal browsing, the system favors availability and low latency (eventual consistency, optimistic UI) over strict consistency. But at the moment of checkout, correctness matters far more than speed — so the checkout flow deliberately re-reads the authoritative cart directly from DynamoDB (bypassing the Redis cache) and re-validates price and stock synchronously, accepting slightly higher latency in exchange for certainty that the user is charged for exactly what they intended to buy.

8.3 The two-priority system, made explicit

It is worth naming this pattern directly, because it is the single most important architectural idea in the entire design: the same system deliberately runs two different consistency modes at two different times. During the ninety-nine percent of the time when a user is browsing, adding, removing, or just leaving the tab open, the system prizes availability and responsiveness. During the small window when checkout begins, the system flips to prizing strict consistency and correctness. Recognizing that a system does not have to make one global consistency choice for all time — it can and often should make different choices for different operations within the same product — is a mark of a genuinely senior systems answer in an interview.

09

Performance & Scalability

9.1 Scaling the WebSocket layer

The WebSocket Gateway is the component with the most distinctive scaling profile in this system: what matters is concurrent connection count, not just request throughput. A single gateway node can typically hold anywhere from tens of thousands to low hundreds of thousands of idle connections, depending on tuning, so horizontal scaling here is about adding more gateway nodes and distributing connections across them, tracked via the Redis connection registry so the Fan-out Service always knows which node holds which user’s connection.

9.2 Read path optimization

Cart reads (displaying the cart icon count, opening the cart page) are far more frequent than cart writes, so the Redis hot-cart cache is critical: most reads never touch DynamoDB at all. A cache-aside pattern is used — read from Redis first, fall back to DynamoDB on a miss, then repopulate Redis.

9.3 Write path optimization

Cart writes are relatively low-volume compared to, say, a social media like-button, so the write path is optimized more for correctness (version checking, conflict resolution) than raw throughput. DynamoDB’s built-in conditional writes (using the version number as a condition) provide an efficient, database-native way to implement the optimistic concurrency check from Section 7.1 without a separate locking mechanism.

9.4 Fan-out efficiency

Because most users have at most two or three simultaneously connected devices, fan-out is cheap per event — but at platform scale, with millions of concurrent carts, the aggregate volume of fan-out messages still matters. The Fan-out Service batches lookups against the Redis connection registry and uses a lightweight internal pub/sub mechanism between WebSocket Gateway nodes, so a single cart change reaches the correct node(s) directly rather than broadcasting to every gateway node in the fleet.

1–2s
Target sync latency
~2–3
Typical concurrent devices per user
100k+
Idle connections per gateway node
🏭
Production example

Large e-commerce platforms handling this exact problem typically separate “hot” cart storage (Redis, for active carts with recent activity) from “cold” cart storage (DynamoDB or a similar durable store, for the full historical/authoritative record), so the overwhelming majority of both reads and writes for actively shopping users stay in the fast in-memory tier, while the durable store guarantees nothing is ever lost.

9.5 Capacity planning for WebSocket connections

Unlike a stateless REST service, where capacity planning centers on requests-per-second, the WebSocket Gateway’s capacity planning centers on three separate numbers that each matter independently: peak concurrent connection count, connection churn rate (how often connections open and close, which matters because each handshake has its own CPU cost), and message fan-out rate (how many push messages per second the fleet needs to deliver). A gateway node that comfortably handles a large number of mostly-idle connections can still become a bottleneck under a high churn rate — for example, during a mobile network transition event affecting many users simultaneously, such as a citywide cellular outage causing a wave of reconnections all at once. Capacity plans for this layer are typically validated with dedicated load tests that specifically simulate connection churn and reconnection storms, not just steady-state idle connection counts, since steady-state testing alone can miss exactly the failure mode most likely to occur during a real incident.

10

High Availability & Reliability

10.1 Handling WebSocket disconnects gracefully

Mobile networks are unreliable by nature — connections drop when switching from WiFi to cellular, entering elevators, or losing signal. The client is designed to detect a dropped WebSocket connection and automatically reconnect with exponential backoff, and on reconnection, explicitly requests a fresh full cart state (rather than assuming it missed nothing) to guarantee it’s fully caught up on anything that happened while disconnected.

10.2 Redundancy & replication

Durable store

DynamoDB

Natively replicates data across multiple availability zones, so a single AZ failure doesn’t lose cart data.

Cache

Redis (clustered)

Runs with replicas; if a cache node fails, cart reads simply fall back to DynamoDB temporarily while the cache warms back up.

Gateway

WebSocket Gateway nodes

Deployed across multiple availability zones behind the Load Balancer; if a node fails, affected clients detect the dropped connection and reconnect to a healthy node, then resync full state.

Event log

Kafka

Topics replicated across multiple brokers so the change-event log survives individual broker failures.

10.3 Guaranteeing no lost writes

The single most important reliability guarantee in this system is that a cart mutation the user believes succeeded must never silently vanish. This is achieved through the combination of: durable, replicated writes to DynamoDB before returning success to the client; idempotency keys so client retries after a timeout never cause duplicate or lost effects; and the Kafka change-event log acting as an independent, replayable record that can rebuild cart state if ever needed.

Common failure mode

A frequent mistake is acknowledging a cart write to the client as soon as it’s written to the fast Redis cache, before it’s durably persisted to DynamoDB. If the Redis node crashes in that narrow window, the write is lost even though the client was told it succeeded. The fix: always write to the durable store first (or at minimum, to both in the same transactional step) and only return success to the client once the durable write is confirmed; the cache update can happen immediately after.

10.4 Backup & disaster recovery

Beyond replication, a complete disaster recovery strategy protects against scenarios replication alone doesn’t cover, such as accidental data corruption or a bug that writes bad data widely before anyone notices:

  • Point-in-time recovery: DynamoDB’s point-in-time recovery feature allows restoring cart data to any point within a retention window, which is invaluable if a deployment bug corrupts cart records before it’s caught.
  • Event log as a rebuild mechanism: Because every cart mutation is also durably logged to Kafka and archived in Cassandra, the full cart state for any user can theoretically be reconstructed by replaying their event history from scratch, providing an independent recovery path if the primary store is ever compromised.
  • Region-level failover: If an entire primary region becomes unavailable, GeoDNS redirects traffic to a secondary region serving from its own DynamoDB Global Table replica; because replication is asynchronous, a small window of very recent writes may not yet have propagated, which the system explicitly accepts as a deliberate availability-over-consistency trade-off during a regional outage.
  • Regular restore testing: Backup and recovery procedures are periodically tested end to end in a non-production environment, since an untested backup is not a reliable one.
💬
What an interviewer may ask

“If a bad deployment silently corrupts cart data for a subset of users over several hours before anyone notices, how would you recover?” A strong answer points to point-in-time recovery on the durable store combined with the independent, replayable Kafka/Cassandra event log as a cross-check — comparing the corrupted state against what the event log says should be true, and using that to identify exactly which users and which time window were affected before restoring.

11

Security

11.1 Authenticating WebSocket connections

Unlike a REST request, which carries an auth token on every single call, a WebSocket connection authenticates once at handshake time and then stays open, potentially for hours. The WebSocket Gateway validates the session token during the initial handshake, and the connection is tied to that authenticated user ID for its entire lifetime; if the underlying session is revoked (e.g., the user logs out), the gateway proactively closes the connection rather than waiting for it to naturally expire.

11.2 Preventing cart tampering

Every cart mutation request is scoped strictly to the authenticated user’s own cart — the Cart Service always derives the cart ID from the authenticated session, never from a client-supplied cart ID, preventing one user from being able to guess or manipulate another user’s cart by tampering with request parameters.

11.3 Price integrity

The client never supplies the price of an item when adding it to the cart — only the item ID and desired quantity. The server always looks up the authoritative current price from the Pricing & Inventory Service. This closes off an entire class of attack where a manipulated client could otherwise attempt to add an item at an artificially low, client-supplied price.

11.4 Rate limiting

The API Gateway enforces per-user rate limits on cart mutation endpoints, both to protect backend capacity and to blunt automated abuse (such as a script rapidly adding and removing items to probe for race conditions or inventory information).

Auth

Session-scoped WebSocket auth

Handshake-time token validation, proactive close on session revocation, no long-lived connection surviving a logout.

Access

Server-derived cart IDs

Cart identity always comes from the authenticated session, never from a request parameter, closing off cart-hijacking attempts.

Trust

Server-derived prices

Client requests only carry item ID and quantity; price is fetched server-side, so a tampered request cannot lower an item’s price.

Abuse

Per-user rate limits

Enforced at the API Gateway to blunt scripted probing, race-condition abuse, and inventory-scraping bots.

11.5 Encryption in transit and at rest

Traffic between clients and the Load Balancer is served over TLS. Internal service-to-service calls use mutual TLS with short-lived certificates, so a single compromised service cannot impersonate another. Data at rest in DynamoDB, Redis, PostgreSQL, and Cassandra is encrypted using platform-managed keys, keeping cart contents protected even if physical storage is ever compromised.

💬
What an interviewer may ask

“How do you make sure a user can’t manipulate the price of an item in their cart by intercepting and modifying the network request?” Look for the answer that the price is never trusted from the client at all — the server always re-fetches the authoritative price from the Pricing & Inventory Service on every add and again at checkout, so a tampered client request simply has no effect on the actual price charged.

12

Monitoring, Logging & Metrics

CategoryExample metricsWhy it matters
Sync latencyTime from a write on one device to the push arriving on another connected device (p50/p95/p99)Directly measures whether the core promise (“your cart follows you instantly”) is being kept
Connection healthActive WebSocket connection count per node, reconnection rate, average connection lifetimeEarly warning of gateway saturation, network instability, or a rolling-deployment issue
Conflict ratePercentage of writes that hit a version conflict and required merge logicSudden spike often indicates a client-side caching bug or an offline queuing regression
AvailabilityCart Service error rate, DynamoDB throttling events, cache hit/miss ratioStandard service-health signals feeding SLO tracking and paging
BusinessCart abandonment rate, time from add-to-cart to checkout, cross-device session rateTies infrastructure health back to actual revenue impact

Sync latency is the single most important metric unique to this system — it directly measures whether the core promise (“your cart follows you instantly”) is actually being kept, and is typically tracked as an end-to-end measurement: timestamp when a write is confirmed on the originating device, versus timestamp when the push update is received and rendered on a second connected device.

12.1 Logging & tracing

Every cart mutation carries a shared traceId propagated from the client through the Load Balancer, API Gateway, Cart Service, Conflict Resolution Service, and Fan-out Service, enabling a single distributed trace to show exactly how long a specific request spent in each layer — indispensable when debugging why one specific user’s cart update reached their other device forty seconds late instead of within the usual two.

12.2 Alerting without fatigue

Not every anomaly deserves a page. Alerts are tiered: P0 for end-to-end sync latency SLO breaches or a Cart Service that’s failing writes outright, since these represent the system failing at its core job during a live shopping session. P1 for elevated conflict rate, climbing consumer lag on the Kafka change-event topic, or WebSocket reconnection storms that haven’t yet degraded the user experience but are trending badly. P2 for slower-moving quality metrics like a gradual drift in cache hit rate, surfaced in a daily digest rather than an overnight page. Every P0 alert links directly to a runbook with concrete diagnostic queries, because the middle of a real incident is the worst possible time to be improvising a checklist.

💬
What an interviewer may ask

“How would you detect that cross-device sync has silently broken for some users, before they complain?” A strong answer proposes synthetic monitoring — automated test accounts with two simulated connected devices that continuously perform cart operations and measure end-to-end sync latency — combined with real-user monitoring that samples actual client-reported timestamps, alerting if sync latency or conflict rate drifts outside normal bounds.

13

Deployment & Cloud

13.1 Containerization & orchestration

Each service (Cart Service, Conflict Resolution Service, Fan-out Service, Pricing & Inventory Service) runs as a container on Kubernetes, providing automated scaling, self-healing, and rolling deployments. The WebSocket Gateway, due to its stateful, long-lived-connection nature, requires additional care during deployments — rolling out a new version must drain connections gracefully (allowing clients to reconnect to a new node) rather than abruptly severing them.

13.2 Multi-region considerations

flowchart LR subgraph RegionA[“Region: US-East (Primary)”] LBA[“Load Balancer”] –> GWA[“API + WebSocket Gateways (Kubernetes)”] GWA –> SvcA[“Cart Service Cluster (Kubernetes)”] SvcA –> CacheA[“Redis Cluster”] SvcA –> DBA[“DynamoDB (Global Table)”] end subgraph RegionB[“Region: EU-West (Secondary)”] LBB[“Load Balancer”] –> GWB[“API + WebSocket Gateways (Kubernetes)”] GWB –> SvcB[“Cart Service Cluster (Kubernetes)”] SvcB –> CacheB[“Redis Cluster”] SvcB –> DBB[“DynamoDB (Global Table)”] end GeoDNS[“GeoDNS / Global Traffic Routing”] –> LBA GeoDNS –> LBB DBA <-. multi-region replication .-> DBB
Diagram 3 — Multi-region deployment with GeoDNS routing and DynamoDB Global Tables replication.

DynamoDB Global Tables (or an equivalent multi-region replicated store) allow a user’s cart to be written in whichever region they’re physically closest to, with changes replicating to other regions asynchronously — meaning a user who adds an item while traveling and switches regions between devices still eventually sees a consistent cart, typically within a small replication delay.

13.3 CI/CD & canary deployments

Given how central correctness is to a cart system (a bug here directly costs revenue), changes to the Cart Service and Conflict Resolution Service are rolled out via canary deployment — a small percentage of traffic first, with close monitoring of conflict rate and error rate, before a full rollout.

13.4 Cost optimization

Because WebSocket connections consume server resources even while completely idle (an open connection with no traffic still holds memory and a file descriptor), cost management for this system looks somewhat different from a typical stateless service:

  • Idle connection timeouts: Connections that have been idle beyond a reasonable window (e.g., a laptop tab left open and unattended for many hours) are proactively closed, with the client expected to reconnect automatically the next time the tab becomes active, freeing up server resources without materially affecting perceived responsiveness.
  • Right-sizing DynamoDB capacity: On-demand or auto-scaled capacity modes track actual cart traffic patterns rather than provisioning fixed capacity for worst-case load, which for a typically spiky retail traffic pattern (daytime peaks, overnight lulls, seasonal surges) avoids paying for unused capacity most of the time.
  • Batching non-urgent downstream work: Abandoned-cart detection and analytics consumption from Kafka are processed in efficient batches on a schedule, rather than triggering expensive per-event processing that would scale linearly and unnecessarily with cart traffic.
💬
What an interviewer may ask

“How do you safely deploy a change to the WebSocket Gateway without disconnecting every connected user at once?” A strong answer walks through connection draining: the old node stops accepting new WebSocket handshakes, existing connections are given a short grace window to complete or migrate, and clients that hit the drained node reconnect through the Load Balancer to a healthy node that then serves them a full-state resync — so the visible impact is at most a brief reconnection blip, not a full logout or lost cart.

14

Databases, Caching & Load Balancing

14.1 Choosing the right database for each job

DataStoreWhy
Authoritative cart stateDynamoDB (key-value, versioned)Fast, horizontally scalable key-value access by user/cart ID, with native conditional-write support for optimistic concurrency
Product catalog, pricing, inventoryPostgreSQL (relational)Structured data with relationships (categories, variants, suppliers) that benefits from relational integrity
Cart change event logCassandra (wide-column)High write volume, append-only, needs to scale horizontally for audit and replay purposes
Hot cart cache, connection registryRedis (in-memory)Sub-millisecond reads for the most frequently accessed data — active carts and live connection mappings

14.2 Load balancing strategies

REST traffic uses standard least-connections load balancing across API Gateway nodes. WebSocket traffic requires sticky (session-affinity) routing — once a connection is established with a specific WebSocket Gateway node, subsequent traffic for that same connection must continue routing to that same node, since the connection itself is stateful and lives on that node’s memory.

14.3 Cache invalidation strategy

  • Hot cart cache (Redis): Write-through — updated immediately on every write, with a moderate TTL as a safety net in case of missed invalidation.
  • Connection registry (Redis): Updated on connect/disconnect events directly from the WebSocket Gateway, with a short TTL as a safety net against stale entries from ungracefully terminated connections (e.g., an app crash that doesn’t send a clean disconnect).
  • Product catalog cache: Time-based invalidation, since price and stock changes need to propagate reasonably quickly but don’t require the sub-second freshness that cart state itself does.

14.4 Sharding & partitioning

DynamoDB partitions cart records by a hash of the user ID, which spreads write load evenly across the underlying storage nodes regardless of any one user’s activity level. This avoids the classic hot-partition problem where one especially active user (or one popular shared cart) could otherwise concentrate load on a single shard. The Cassandra event log is partitioned by (userId, day), keeping any single user’s recent history on one partition for efficient replay while spreading total load across the cluster over time. The Redis hot cache uses consistent hashing so an individual node addition or removal only reshuffles a small fraction of keys, avoiding the cache stampede that would follow a full re-hash.

💬
What an interviewer may ask

“Why partition the event log by (userId, day) rather than just userId?” Because a very long-lived user’s history could otherwise grow the partition indefinitely; combining the user with the day keeps each partition bounded in size, which is a general good practice in wide-column stores that heavily favor bounded-size partitions for consistent performance.

15

APIs & Microservices

15.1 Public API contract

Cart API — REST + WebSocket contracthttp
POST /v1/cart/items
  Request:  { itemId, quantityDelta, expectedVersion, idempotencyKey }
  Response: { cart: {...}, version, status: "OK" | "CONFLICT" }

DELETE /v1/cart/items/{itemId}
  Request:  { expectedVersion, idempotencyKey }
  Response: { cart: {...}, version }

GET /v1/cart
  Response: { items: [...], version, lastModified, lastModifiedDevice }

WebSocket message (server to client, pushed):
  {
    type: "CART_UPDATED",
    cart: { items: [...], version },
    changedBy: "deviceId-mobile-XYZ"
  }

WebSocket message (server to client, on reconnect):
  {
    type: "FULL_SYNC",
    cart: { items: [...], version }
  }

Notice the changedBy field on the push message — this lets a client optionally suppress a redundant UI animation on the very device that made the original change (since it already updated itself optimistically), while still fully updating any other connected device.

15.2 Why microservices instead of a monolith here

The Cart Service, Conflict Resolution Service, Fan-out Service, and Pricing & Inventory Service have distinct scaling and reliability profiles — Fan-out and the WebSocket layer scale with connection count, Cart Service scales with write volume, and Pricing & Inventory Service is largely read-heavy against a separate, slower-changing catalog. Splitting them allows each to be scaled, deployed, and evolved independently, and allows a spike in WebSocket connection churn to be isolated from cart-write throughput concerns.

15.3 Internal service-to-service communication

Between internal services, the design uses gRPC over HTTP/2 rather than REST/JSON. The strongly-typed protobuf contracts catch schema mismatches at compile time rather than in production, and gRPC’s binary encoding plus HTTP/2 multiplexing meaningfully reduces both latency and payload size on the fan-out path, where the Cart Service and the Fan-out Service exchange messages hundreds of times per second during peak. External APIs, exposed to browsers and mobile clients, remain REST/JSON for the usual reasons: universal client support, ease of debugging, and cache-friendliness at the edge.

Common anti-pattern

A common early-stage mistake is embedding conflict-resolution logic directly and repeatedly inside the Cart Service’s write path, scattered across multiple endpoints, rather than centralizing it in its own service. This makes the merge behavior inconsistent between different mutation types (add vs. remove vs. quantity-change) and much harder to test and reason about in isolation.

16

Design Patterns & Anti-patterns

16.1 Patterns used in this design

Concurrency

Optimistic concurrency control

Version-checked writes avoid the overhead and complexity of distributed locking while still detecting conflicts reliably.

Merge

CRDT-inspired merge

Automatic, deterministic conflict resolution that favors not losing intentional user actions.

Cache

Cache-aside

Reads check Redis first, falling back to DynamoDB on a miss.

Messaging

Publish/Subscribe (Kafka)

Decouples the fast cart-write path from slower downstream consumers like abandoned-cart detection and analytics.

Routing

Sticky session routing

Ensures stateful WebSocket connections are consistently routed to the node that holds them.

Retry

Idempotent write endpoints

Client-supplied idempotency keys make retry logic safe by design, not by convention.

16.2 Anti-patterns to avoid

✗ Treating the cache as the source of truth

  • Acknowledging a write to the client before it’s durably persisted risks silent data loss if the cache node fails.

✗ Naive last-write-wins on the entire cart object

  • Can silently discard one device’s legitimate concurrent addition, which directly costs the business a sale the user intended to make.

✗ Trusting client-supplied prices

  • Opens the system to trivial manipulation; price must always be server-derived.

✗ No reconnection resync

  • Assuming a reconnected WebSocket client is automatically caught up, instead of explicitly requesting a full state refresh on reconnect, risks the client silently missing changes that happened during the disconnect window.
17

Best Practices & Common Mistakes

17.1 Best practices

  • Always write durably before acknowledging success: Never tell a client a cart change succeeded until it’s confirmed in the durable store.
  • Use idempotency keys on every mutation: Makes client-side retry logic safe by design, rather than something that has to be carefully avoided.
  • Re-validate price and stock at checkout, always: Never trust cached or stale pricing at the moment money changes hands.
  • Explicit full-resync on reconnect: Don’t assume incremental push messages are sufficient after any connection gap.
  • Favor not losing data in merge logic: When in doubt during conflict resolution, prefer preserving both devices’ intended changes over silently picking one.
  • Monitor sync latency as a first-class metric: It’s the most direct measurement of whether the system is delivering on its core promise.

17.2 Common mistakes

  • Polling as the only sync mechanism: Leads to a noticeable, unsatisfying delay that undermines the “instant” cross-device experience users now expect.
  • No handling for offline queuing: A mobile client that simply fails silently when offline, rather than queuing and replaying, loses user trust the first time it happens.
  • Ignoring WebSocket connection churn in capacity planning: Treating the WebSocket Gateway like a stateless REST service in capacity planning underestimates the very different resource profile of holding open connections.
  • Skipping synthetic end-to-end sync monitoring: Relying only on infrastructure-level metrics (CPU, error rate) can miss a subtle bug where sync silently stops working for a subset of users while every individual service still reports healthy.

17.3 A practical rollout approach

Teams building this system for the first time are generally well served by a staged approach rather than attempting to ship every piece described in this guide at once:

1

Stage one — durable, correct cart storage

Get the basic add/remove/update-quantity flow working reliably against DynamoDB, with proper idempotency and version checking, even before any real-time push exists. A cart that’s simply correct on refresh is already a major improvement over a session-only cart.

2

Stage two — cross-device visibility on refresh

Confirm that opening the app on a second device shows the correct, up-to-date cart, validating the core storage and read-path design before adding real-time complexity.

3

Stage three — real-time push

Layer in the WebSocket Gateway and Fan-out Service once the underlying storage and conflict resolution are proven solid, since debugging real-time push issues on top of an already-shaky storage layer is significantly harder than debugging them independently.

4

Stage four — offline tolerance and advanced conflict resolution

Add local request queuing on mobile clients and the full CRDT-style merge logic last, since these are the most complex pieces and benefit from a stable, already-working foundation underneath them.

18

Real-World Industry Examples

Retail

Amazon

A logged-in Amazon shopper’s cart persists across the mobile app, desktop site, and Alexa-based shopping, with additions on one surface reliably appearing on others — reflecting a cart architecture built around a durable, account-tied cart record rather than a device-local session.

Retail + In-store

Walmart

Walmart’s app and website share a unified cart tied to the customer account, supporting both online cart persistence and integration with in-store pickup flows, which requires the same underlying cross-device consistency guarantees discussed in this guide.

Platform

Shopify (merchant storefronts)

Shopify-powered stores maintain a cart tied to a customer token that persists across sessions and devices when a shopper is logged in, with storefront APIs designed around similar versioned-cart and re-validation-at-checkout principles.

Marketplace

eBay

eBay’s cart and watchlist sync across devices for logged-in users, with particular care around price and availability re-validation given how frequently listings and prices can change on an auction/marketplace platform compared to a fixed-catalog retailer.

18.1 A shared underlying pattern

Despite very different product surfaces — a fixed-price retail catalog, an auction marketplace, a merchant-hosted storefront — these platforms converge on a remarkably similar underlying architecture: a durable, versioned, account-tied cart as the single source of truth; a fast cache in front of it for the overwhelming majority of read traffic; some form of push-based propagation to currently active sessions; and notably stricter, more consistency-favoring rules specifically at the moment of checkout, where the cost of getting it wrong is highest. This convergence isn’t a coincidence — it reflects the same fundamental trade-offs discussed throughout this guide, arrived at independently by teams solving the same underlying distributed systems problem.

🏭
Production insight

A pattern shared across nearly all large-scale cart systems: the “instant sync” experience users perceive is almost always built on a foundation of durable, versioned server-side storage plus a push mechanism layered on top — never on the client devices talking to each other directly, and never on trusting any single device’s local state as authoritative.

19

Frequently Asked Questions

Q1What happens if the same item is added on two devices at nearly the same instant?

Depending on the merge policy chosen, most systems either sum the quantities (treating it as the user wanting more of that item) or take the higher of the two resulting quantities — the key design principle is that neither addition should be silently discarded.

Q2Do guest (not logged in) shoppers get cross-device sync too?

Generally no, or only in a very limited form. Cross-device sync fundamentally requires a stable identity to tie the cart to; a guest cart is typically bound to a single device/browser session, and the platform usually offers to merge a guest cart into the account cart at the moment the user logs in or signs up.

Q3How is this different from just storing the cart in a regular database and having each device poll it?

Polling technically achieves eventual synchronization too, but with a much coarser and less pleasant user experience — either frequent polling wastes resources and battery, or infrequent polling introduces a noticeable, often multi-second-to-minutes delay. Push-based sync via WebSockets achieves the same end goal with far lower latency and less wasted network traffic.

Q4What happens to the cart if a user is logged into the same account on five devices at once?

The Fan-out Service pushes updates to every currently connected device, without a hard limit in principle, though most platforms track and can display “active sessions” and may apply reasonable practical limits on simultaneous connections per account for cost and abuse-prevention reasons.

Q5Should the same architecture be used for a wishlist or “save for later” list?

Largely yes — a wishlist has very similar cross-device sync requirements, though typically with lower urgency around real-time propagation and price/stock re-validation, since a wishlist item isn’t about to be purchased in the next few seconds the way a cart item might be. Many platforms reuse the same underlying sync infrastructure for both, with slightly relaxed latency and validation requirements for the wishlist case.

Q6How do you test conflict resolution logic reliably before shipping it?

Through deterministic unit tests that simulate specific conflict scenarios directly (two devices adding different items offline, two devices changing the same item’s quantity within the same second, a removal racing an addition) rather than relying purely on real-world usage to surface these cases, since genuine simultaneous multi-device conflicts can be rare enough in ad-hoc manual testing to hide bugs that only appear at scale in production.

Q7Does this system need to handle a shared cart between multiple different users, like a family account?

That’s a related but distinct problem — a shared cart across multiple distinct user identities (not just multiple devices of one user) adds additional considerations around permissions and attribution (who added what), but the same core sync and conflict-resolution primitives described in this guide extend naturally to that case, typically by keying the connection registry and fan-out logic on the shared cart ID rather than a single user ID.

Q8What’s a reasonable target for sync latency, and how strict should it be?

Most production systems target roughly one to two seconds end to end for the common case of two actively connected devices on reasonable network conditions, treating this as a strong goal rather than a hard guarantee — occasional outliers driven by network conditions on the receiving device are expected and acceptable, as long as they remain rare and the system reliably catches up on reconnect. Chasing sub-100-millisecond sync for a shopping cart specifically is rarely worth the added complexity, since the user experience benefit beyond roughly one second is minimal for this particular use case, unlike, say, a competitive multiplayer game where every millisecond matters.

20

Summary & Key Takeaways

A real-time, cross-device shopping cart is fundamentally a distributed state synchronization problem wearing a familiar, everyday disguise. The architecture in this guide combines a durable, versioned source of truth (DynamoDB), a fast cache for the common read path (Redis), a persistent push mechanism for instant propagation (WebSockets, fanned out to every connected device), and a deliberate, product-aware conflict resolution strategy that favors never silently losing something a user intended to buy — all wrapped behind a load balancer and API/WebSocket gateways, with careful re-validation of price and stock at the moments that matter most.

What makes this problem genuinely interesting from a systems design perspective is how it forces two normally opposing priorities to coexist within a single product: the system spends most of its time favoring availability and low latency over strict correctness, precisely because a shopper browsing casually should never be blocked by synchronization concerns, and then deliberately flips that priority the instant checkout begins, favoring strict consistency because that is the one moment where getting it wrong costs real money and real trust.

📌
Key takeaways
  • Real-time cross-device sync is best achieved with a persistent push mechanism (WebSockets) rather than polling, fanned out to every device the user currently has open.
  • Optimistic UI updates make the app feel instant, while the actual write and confirmation happen in the background.
  • Version-based optimistic concurrency control detects conflicts cheaply; CRDT-inspired merge logic resolves them without silently losing data.
  • Durability comes first — never acknowledge a cart write as successful until it’s confirmed in the durable store, not just the cache.
  • Price and stock must always be re-validated server-side at add-time and again at checkout — never trusted from the client or from a stale cache.
  • WebSocket infrastructure has a fundamentally different scaling profile (connection count) than stateless REST services, and needs sticky routing and connection-aware capacity planning.
  • Sync latency and conflict rate are the two most important metrics unique to this system, and deserve first-class, synthetic end-to-end monitoring.
📡
One final analogy

If a single-device shopping cart is a paper list you keep in your pocket, this system is more like a shared whiteboard in a household kitchen: anyone in the family can write on it from wherever they are, changes appear on every copy nearly instantly, additions are never accidentally erased, and when it’s finally time to actually go to the store, everyone agrees on exactly what the list says before anybody spends a dollar. That shift — from private, single-device state to shared, deliberately-synchronized state — is the single idea underneath every component in this guide.