An e-commerce order platform is where system design interviews stop being abstract: the cart has to survive a flaky mobile connection, inventory has to stay accurate under simultaneous checkouts without serializing the whole store through one lock, and a flash sale on one product can turn a single database row into the busiest resource in the entire system. Architect-level interviewers use this scenario because it forces explicit trade-offs between consistency and throughput in different parts of the same system — you cannot treat "consistency" as one setting you turn on everywhere and call it correct. It also rewards candidates who've actually looked at how a real .NET reference implementation handles this, because the trade-offs stop being hypothetical the moment you see them made concretely. This page works through the design the way an architect-level loop runs: service boundaries, cart, inventory, the order saga, search, flash sales, consistency choices, payment integration, and what the dotnet/eShop reference app gets right and where a production system would diverge from it.

Q1 What are the bounded contexts in an e-commerce order management system, and how do you decide where to draw the service boundaries?#

Short answer: Draw boundaries around distinct business capabilities with different data ownership and different rates of change — catalog (what can be bought), basket (what a specific shopper currently intends to buy), ordering (a placed, immutable commitment to buy), payment, and identity — rather than around technical layers, and let each service own its data exclusively, communicating with the others only through APIs and events.

The catalog changes on a product-management cadence and is read overwhelmingly more than it's written, which argues for aggressive caching and a search-optimized read model. The basket is ephemeral, per-user, high-write, low-durability-requirement state that doesn't belong in the same store or scaling profile as a placed order, which is exactly why it's usually its own service. Ordering is the one place strict consistency actually matters — once a customer has committed to a purchase, that record needs to be durable, auditable and never silently lost — so it typically gets its own relational store with real transactional guarantees, deliberately separated from the domain logic and the API surface (a domain layer, an infrastructure layer, and an API layer as three distinct projects) so business rules aren't entangled with persistence or transport concerns. The dotnet/eShop reference application draws exactly this line: Catalog.API, Basket.API, and Ordering.API (backed by separate Ordering.Domain and Ordering.Infrastructure projects) are distinct services with their own data stores, coordinated through an event bus rather than direct synchronous calls wherever a synchronous dependency would create tight coupling.

What interviewers look for: boundaries justified by data ownership and rate-of-change differences rather than an arbitrary split, and specifically naming why the basket and the order are not the same concept even though a shopper experiences them as a continuous flow.

Common mistakes: merging basket and order into one service or table "because they're related," which then forces the durability and consistency requirements of orders onto high-churn basket writes.

Q2 How would you design the shopping cart — server-side, client-side, or both — and how does it interact with inventory?#

Short answer: Store the cart server-side, keyed by user or session ID, in a fast key-value store like Redis rather than the primary relational database, and do not reserve inventory when an item is added to the cart — inventory reservation happens at checkout, not at add-to-cart, because carts are abandoned far more often than they convert and reserving eagerly would lock up stock for shoppers who never check out.

A Redis-backed cart gives you sub-millisecond reads and writes for an operation that happens constantly and doesn't need relational integrity — the cart is a document (user ID, line items, quantities) with no need for joins, and Redis's native TTL support handles abandoned-cart expiry without a separate cleanup job. Keeping the cart out of the inventory system entirely also keeps the read-heavy catalog and cart paths from ever contending with the write-heavy, consistency-sensitive inventory path.

C#
public sealed class BasketService(IConnectionMultiplexer redis)
{
    private IDatabase Db => redis.GetDatabase();

    public async Task<CustomerBasket?> GetBasketAsync(string userId)
    {
        var data = await Db.StringGetAsync($"basket:{userId}");
        return data.IsNullOrEmpty ? null : JsonSerializer.Deserialize<CustomerBasket>(data!);
    }

    public Task SaveBasketAsync(CustomerBasket basket) =>
        Db.StringSetAsync($"basket:{basket.UserId}", JsonSerializer.Serialize(basket),
            expiry: TimeSpan.FromDays(14));
}

At checkout, the basket is read one final time, validated against current price and availability, and converted into an inventory reservation and an order — the moment the system transitions from "intent" to "commitment," which is exactly where the next question's concurrency control takes over.

What interviewers look for: explicitly deferring inventory reservation past add-to-cart, since reserving too early is one of the most common design mistakes candidates make on this question.

Q3 Design inventory reservation. How do you prevent overselling without serializing every checkout through a single lock?#

Short answer: Use optimistic concurrency — a version or row-version column on the inventory record, decremented only if the expected version still matches — combined with a reservation that has a short expiry, so a checkout in progress holds stock temporarily without a long-held database lock, and an abandoned checkout automatically releases it back to availability.

A naive SELECT then UPDATE under a pessimistic row lock forces every simultaneous checkout attempt on the same product to queue behind the lock, which is exactly the bottleneck a flash sale exposes. Optimistic concurrency instead lets every request read the current available quantity and attempt an update conditioned on it not having changed since the read; if two requests race, one succeeds and the other retries against the new state or fails fast with "insufficient stock," without ever holding a lock across the round trip to the caller. EF Core supports this directly with a concurrency token.

C#
public sealed class InventoryItem
{
    public int ProductId { get; set; }
    public int AvailableQuantity { get; set; }

    [Timestamp]
    public byte[] RowVersion { get; set; } = [];
}

var item = await db.InventoryItems.FirstAsync(i => i.ProductId == productId, cancellationToken);
if (item.AvailableQuantity < requestedQuantity)
{
    return ReservationResult.InsufficientStock;
}

item.AvailableQuantity -= requestedQuantity;
try
{
    await db.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateConcurrencyException)
{
    return ReservationResult.Retry;
}

Make the reservation itself time-boxed — decrement available quantity but record a reservation row with an expiry, and run a background sweep that releases expired, unconfirmed reservations back to available stock — so a customer who reserves an item and abandons checkout doesn't lock that unit away indefinitely.

What interviewers look for: optimistic concurrency as the default rather than pessimistic locking, and a concrete answer for reservation expiry and release, since "reserve forever until confirmed" is a subtle but real bug most candidates miss.

Common mistakes: using a pessimistic lock as the default answer without being asked to justify it against the throughput cost under contention.

Q4 Walk through the order saga from "place order" to "order fulfilled," including the failure branches.#

Short answer: Orchestrate the flow as an explicit sequence — reserve inventory, charge payment, confirm the order, trigger fulfillment — with a defined compensation for every step that can fail after an earlier step already succeeded, publishing durable integration events at each transition so other services (shipping, notifications) react without the ordering service waiting on them synchronously.

Model states explicitly rather than as an implicit sequence of service calls: Submitted → AwaitingValidation → StockConfirmed → PaymentConfirmed → Shipped, with a Cancelled branch reachable from any pre-shipment state. If inventory reservation fails, the order is rejected before payment is ever attempted — cheap to fail early. If reservation succeeds but the payment step fails, the compensation releases the reserved inventory back to availability and marks the order cancelled; if payment succeeds but a downstream step (fraud hold, address validation) later fails, the compensation now includes a refund, issued through the same idempotent path used everywhere in the payment integration. Publish each transition as an integration event through a durable outbox rather than calling the next service directly, so a crash between committing the order state and notifying the next service can't lose the notification.

C#
public enum OrderStatus
{
    Submitted, AwaitingValidation, StockConfirmed, PaymentConfirmed, Shipped, Cancelled
}

For the broader pattern this saga is an instance of, see Saga Pattern and Distributed Transactions Interview Questions.

What interviewers look for: compensations that get more expensive the further the saga has progressed (reject early is cheap, refund after payment is not), and durable event publication at every transition instead of direct synchronous coupling between steps.

Follow-up questions:

  • How would you detect and recover an order stuck in AwaitingValidation for too long?
  • What happens if the fulfillment step fails after payment has already succeeded?

Q5 How does catalog search stay fast and accurate when inventory and pricing change constantly?#

Short answer: Separate the write model (the transactional catalog and inventory data) from the read model used for search — a denormalized, search-optimized index updated asynchronously via events published whenever the underlying data changes — and accept that search results can lag the true state by a small, bounded window rather than trying to make every search query strongly consistent with live inventory.

This is a direct application of CQRS at the catalog boundary: writes go through the catalog service's own store with whatever consistency it needs for correctness, and every meaningful change (price update, new product, stock going to zero) publishes an event that a separate indexing consumer applies to the search store, which is optimized for the very different access pattern of full-text and faceted queries rather than point lookups or transactional updates. The practical consequence to state explicitly is that a search result showing "in stock" can occasionally be stale by however long the indexing pipeline lags, which is why the authoritative stock check has to happen again at add-to-cart or checkout time against the real inventory service, not trusted from the search index — search answers "what can I probably buy," inventory reservation answers "can I actually buy this right now."

What interviewers look for: explicitly stating that search is allowed to be eventually consistent while checkout-time inventory checks are not, rather than assuming one consistency model has to cover both.

Common mistakes: trying to keep the search index synchronously consistent with inventory, which either serializes catalog writes behind search indexing or makes search queries themselves hit the transactional store, defeating the point of a separate read model.

Q6 What is a "hot key" problem, and how would you handle a flash sale on a single, extremely popular product?#

Short answer: A hot key is a single record — here, one product's inventory row — that receives disproportionately more concurrent traffic than the system's normal per-row throughput budget, and the fix is to stop treating it as a single row under contention: shard the counter, admit demand through a queue instead of letting every request race the database directly, and cache the product page aggressively since read traffic on a flash-sale product dwarfs write traffic even during the sale.

Sharding the counter — splitting one product's stock into several sub-counters that requests are randomly assigned to, then reconciling at the end — trades perfect real-time accuracy of "exactly how many are left" for eliminating the single-row bottleneck, since each shard absorbs a fraction of the contention independently. A queue-based admission pattern goes further: instead of letting every simultaneous request attempt a reservation directly, requests take a ticket from a queue (or a rate limiter with a bounded permit count matching remaining stock), and only admitted requests attempt the actual reservation, which turns an uncontrolled thundering herd into a controlled, sequential drain of a known quantity. The product page itself should be served almost entirely from cache during the sale window — HybridCache or a CDN-fronted cache with a short TTL — because the overwhelming majority of flash-sale traffic is people looking at the page and clicking "buy," not the reservation attempt itself, and there's no reason that read traffic should touch the database at all.

What interviewers look for: naming sharded counters or admission-queue patterns specifically, rather than "add more database replicas," which doesn't help a single hot row since replicas don't parallelize writes to the same key.

Common mistakes: assuming horizontal database scaling (read replicas, more nodes) solves a hot-key write problem, when the constraint is contention on one logical row, not aggregate read capacity.

Q7 What consistency model would you choose for different parts of this system, and why isn't strong consistency everywhere the right answer?#

Short answer: Match the consistency model to what's actually at stake in each part of the system: inventory reservation at checkout needs strong, immediate consistency because overselling is a real business and customer-trust cost; catalog browsing and search tolerate eventual consistency well because a few seconds of staleness is invisible to the user; and order history needs read-your-writes consistency for the customer who just placed the order, without needing global strong consistency for every other reader.

Strong consistency everywhere means every read has to go to a single source of truth (or a synchronously replicated one), which caps your read throughput at whatever that source can sustain and adds latency to reads that don't actually need the guarantee — the catalog page doesn't need to reflect a price change from three seconds ago, so paying a consistency tax on every catalog read is pure waste. The CAP framing is useful here specifically because different parts of this system sit at different points on it: inventory reservation prioritizes consistency over availability during a network partition (better to reject a checkout than risk overselling), while catalog search prioritizes availability (better to serve a slightly stale result than no result). Read-your-writes for order history is a narrower, cheaper guarantee than global strong consistency — route the customer's own post-order read to the primary or to a replica known to have caught up, without requiring every other reader in the system to pay for the same guarantee.

What interviewers look for: a consistency choice justified per subsystem with a stated cost of getting it wrong in either direction, not a single blanket answer applied uniformly across the whole platform.

Q8 How does the order service integrate with payment processing without becoming tightly coupled to a single provider?#

Short answer: Treat payment as one step in the order saga, invoked through an internal payment service's own API (which in turn integrates with the external payment service provider), using an idempotent charge request keyed to the order ID so a retried saga step can't double-charge, and let the payment confirmation arrive asynchronously as an event rather than blocking the entire order flow on it.

Coupling the order service directly to a specific PSP's SDK means every future PSP change or multi-provider requirement becomes an order-service change; routing payment through its own bounded context — its own API, its own idempotency and webhook handling exactly as described in a dedicated payment design — keeps that volatility contained. The order-to-payment call passes the order ID as (or as part of) the idempotency key, so a saga retry after a timeout is safe by construction, and the order service transitions to PaymentConfirmed only when it receives the payment service's own confirmation event, not merely when its synchronous call returns, for the same reason a payment system treats its PSP's webhook as authoritative over the synchronous response. See System Design Interview: Payment Processing System for the payment side of this boundary in full.

What interviewers look for: payment treated as an owned upstream dependency accessed through a stable internal contract, not a direct integration scattered through order-service code, and idempotency carried through the saga step specifically.

Q9 What does the dotnet/eShop reference app teach about structuring a .NET e-commerce system, and where would you diverge from it in production?#

Short answer: eShop demonstrates the bounded-context split from the first question concretely — separate Catalog.API, Basket.API, Ordering.API (with its domain and infrastructure layers split into their own projects), and Identity.API services, coordinated through an event bus (EventBus/EventBusRabbitMQ) with a transactional outbox (IntegrationEventLogEF) so integration events are published reliably, all orchestrated locally through a .NET Aspire AppHost project — and it's a genuinely good teaching example of these patterns, but it deliberately simplifies several things a real production system can't.

The parts worth internalizing directly: a dedicated OrderProcessor and PaymentProcessor as separate services rather than logic embedded in Ordering.API itself, which mirrors the saga-with-compensations structure from earlier in this page; IntegrationEventLogEF specifically implementing the outbox pattern so a database write and its corresponding integration event commit together; and eShop.ServiceDefaults centralizing telemetry, health checks and resilience configuration so every service gets the same operational baseline instead of reimplementing it. Where a real production system diverges: eShop runs PostgreSQL, Redis and RabbitMQ as local containers suitable for demonstration and evaluation, not the managed, highly-available, multi-region equivalents a real platform needs; its payment flow is a simulated PaymentProcessor, not a real, PCI-scoped PSP integration with the webhook and reconciliation depth covered earlier on this page; and it doesn't need to solve the hot-key flash-sale problem or multi-region active-active consistency at the scale a real high-traffic retailer would. Treat it as an accurate blueprint for service boundaries and the outbox/event-bus mechanics, and as a starting point — not a finished answer — for the scale and compliance concerns a production deployment adds on top.

What interviewers look for: specific, accurate recall of what the reference app actually demonstrates (the outbox via IntegrationEventLogEF, the Aspire-orchestrated service split) rather than a vague "it's a good example," paired with a clear-eyed list of what it doesn't attempt to solve.

Q10 How would you design the order history / "my orders" read path so it stays fast as order volume grows into the tens of millions?#

Short answer: Serve order history from a denormalized, per-customer read model rather than joining live across normalized order and line-item tables on every request, paginate with keyset (seek) pagination instead of offset-based paging, and move orders past a certain age into cheaper, separately queried archival storage so the hot table stays small relative to total historical volume.

Offset pagination (OFFSET 10000 ROWS FETCH NEXT 20) gets progressively slower as the offset grows because the database still has to scan and discard every skipped row; keyset pagination — "give me the next 20 orders older than this order's timestamp and ID" — stays roughly constant time regardless of how deep into history the customer pages, because it's a direct index seek rather than a scan-and-skip. A read model that stores exactly what the "my orders" page needs (summary fields, not a live join across normalized tables) avoids repeating expensive joins on every request and is a natural fit for the same event-driven projection approach used for catalog search — the order service publishes an event on every status change, and a projection consumer updates the denormalized view. Archiving orders past a defined age (a year, for most retail contexts) into separate, less frequently accessed storage keeps the primary table's working set small and its indexes efficient, while still making old orders available on request through a distinctly slower, clearly-labeled path.

What interviewers look for: keyset pagination named specifically as the fix for offset pagination's degradation at depth, and a denormalized read model as the answer to repeated-join cost, both of which are concrete, testable design decisions rather than general "add an index" hand-waving.

Quick-Fire Round#

QuestionAnswer
Why shouldn't add-to-cart reserve inventory immediately?Most carts are abandoned; eager reservation locks stock unnecessarily.
What EF Core feature enables optimistic concurrency on inventory?A [Timestamp]/row-version concurrency token column.
What pattern guarantees an order state change and its event commit together?The transactional outbox pattern.
What does eShop's IntegrationEventLogEF project implement?The outbox pattern for reliably publishing integration events.
What's the main fix for a hot-key flash sale on one product?Shard the counter and/or admit demand through a queue.
Why doesn't adding database read replicas fix a hot-key write problem?Replicas don't parallelize writes to the same row.
Which needs strong consistency: inventory reservation or catalog search?Inventory reservation; search can be eventually consistent.
What pagination style avoids slowing down on deep order-history pages?Keyset (seek) pagination, not offset pagination.
How should a retried order-to-payment saga step stay safe?An idempotency key derived from the order ID.
What orchestrates eShop's services locally?A .NET Aspire AppHost project.

How to Prepare#

  • Practice justifying service boundaries by data ownership and rate of change, using cart-versus-order as the concrete example.
  • Know optimistic concurrency well enough to explain why it beats pessimistic locking under checkout contention.
  • Have a specific, named fix ready for the hot-key question — sharded counters or admission queues, not "scale the database."
  • Be able to state, per subsystem, which consistency model you'd choose and what the cost of the wrong choice would be.
  • If you reference eShop, know a concrete detail (IntegrationEventLogEF, the Aspire AppHost) rather than a general impression of the repo.
  • Rehearse keyset pagination as the specific answer to "how does this stay fast at scale" for any list-of-history question.