Design Order, Cart & Inventory
- Scope an e-commerce checkout the way Chewy asks it, with schemas, endpoints and a multi-component flow
- Walk the cart, order, and inventory services end to end and name the database and key behind each one
- Go deep on never overselling a SKU, idempotent order placement, and Autoship recurring orders
Understanding the problem
This is Chewy's home turf. So the framing matters as much as the design. Chewy sells pet food, medication, and other supplies, and the interviewer wants to see you reason through a real retail checkout, not audition a distributed-systems thesis. Reports from the loop are consistent on the altitude: outline the database schemas and the API endpoints, reason through a multi-component system, and keep it grounded. One candidate summarized the system-design round as "minimal." That is a hint about scope, not a license to be vague. Minimal means you are not expected to shard the world, but you are very much expected to get correctness right where money and stock are on the line.
So read the problem for where it is genuinely hard rather than where it is merely big. Browsing the catalog is a read-heavy, cacheable problem that any e-commerce site solves the same way, and it is not what this round turns on. The hard parts are narrow and specific. Two customers must not both buy the last bag of a food when one unit is left. A customer who taps "place order" twice must not get charged twice. And Autoship has to fire thousands of orders on schedule with no human touching them. Name those three early and the rest of the design falls into place around them.
Functional requirements
Propose a tight core and let the interviewer widen it. The spine is add to cart, place an order against real stock, and reorder on a schedule.
In scope
- Add, update, and remove items in a cart, with current price and availability shown
- Place an order from a cart: reserve stock, take payment, then record the order
- Track inventory per SKU across fulfillment centers so a SKU is never oversold
- Create and manage Autoship subscriptions that place recurring orders on a cadence
Below the line (name it, then skip)
- Catalog browse and search (read-heavy and cacheable; a different design)
- The recommendation and pricing engines (they feed the catalog, not this flow)
- Warehouse robotics and last-mile shipping (downstream of order placement)
- Returns, refunds, and customer-service tooling (a separate surface)
- Fraud scoring and tax calculation (call them as services, do not design them)
Non-functional requirements
Attach a target to each one and reason about it out loud. The first is correctness of stock, and it is the whole round. Inventory has to be strongly consistent at the moment of purchase: the count you decrement is the count that decides whether the sale is allowed, and a stale read here means an oversell. Relax everywhere else. Not here.
The second is that placing an order must be exactly-once from the customer's point of view. A double-tap, a retried request after a timeout, or a browser back-button resubmit must produce one order and one charge, never two. This is an idempotency requirement, and it is cheap to get right if you plan for it and expensive to bolt on later.
The rest are ordinary retail targets. Cart and checkout should feel instant, so aim for reads and writes in the low tens of milliseconds, which means the cart lives in a fast store, not buried behind a slow join. The order write itself can take a beat longer, since a customer will wait a second for a confirmation screen. Availability leans on reads. Browsing and adding to cart must stay up even if a fulfillment center's inventory feed hiccups. A slightly stale "in stock" during browse is fine as long as the final decrement at checkout is exact. Autoship is a background job, so it trades latency for reliability: a subscription order can fire minutes late, but it must fire, and exactly once per cycle.
- Strong Inventory consistency at purchase. The decrement decides the sale; a stale read here is an oversell.
- Exactly-once Order placement. A retry or double-tap yields one order and one charge.
- < ~50 ms Cart read and write. Checkout should feel instant, so the cart lives in a fast store.
- Reliable, not fast Autoship firing. A recurring order can run late, but must fire once per cycle.
A quick sizing pass keeps everything at the right altitude. Assume something like 20 million active customers, a catalog on the order of 100,000 SKUs, and daily orders in the low millions at peak. None of those numbers force exotic infrastructure. A catalog of 100,000 SKUs and a few million orders a day fits comfortably in a well-indexed relational database. The write rate averages tens of orders a second with holiday spikes into the hundreds. That is a load a single primary Postgres handles with room to spare, which is exactly why the interview stays at schema and API level rather than sharding strategy.
The set-up
Core entities
Name the objects the system moves before drawing anything. Five carry the design, and the split between the SKU and its per-warehouse stock is the one to get right.
Product / SKU
A sellable item: sku, name, price, unitOfSale, and catalog attributes. The SKU is the stock-keeping unit, the exact thing you decrement (a 30-lb bag of one brand, not the brand in general).
InventoryItem
Stock for one SKU at one fulfillment center: (sku, fcId) with onHand, reserved, and available = onHand - reserved. Split by center because stock is physical and lives in specific warehouses, and it is the row the checkout locks and decrements.
Cart / CartLine
A customer's working basket: a Cart per customer holding CartLine rows of sku and quantity. Prices are shown from the catalog at read time, not frozen, until checkout.
Order / OrderLine
An immutable record of a placed purchase: orderId, customerId, status, a captured total, and OrderLine rows that freeze the sku, quantity, and priceAtPurchase. Once written, the price and quantity never change.
Autoship
A recurring subscription: customerId, a set of (sku, quantity) items, a cadence (every 4 weeks, say), a nextRunAt, and a status of active, paused, or canceled. Firing one produces a normal Order.
Now the storage decisions, named concretely, because a design that says "a database" reads as junior. Orders, order lines, and inventory belong in Postgres. They are relational, they need transactions, and inventory correctness depends on the ability to lock a row and decrement it atomically, which a relational engine gives you directly. Orders are keyed by orderId and indexed by customerId for a customer's history; inventory is keyed by the composite (sku, fcId). There is nothing to shard at these volumes, so one primary with read replicas is the honest answer.
The cart is different. It is small, per-customer, written constantly as people fiddle with quantities, and it does not need heavy relational guarantees, so it belongs in a fast key-value store like Redis or DynamoDB, keyed by customerId. Keeping the cart out of the transactional database keeps the checkout path clean and the cart snappy. Autoship subscriptions live in Postgres too, since firing them is a transactional job and nextRunAt is the column a scheduler queries. Product catalog data is read-heavy and cacheable and sits behind its own service, which this design treats as a dependency it reads from.
API
Keep the interface small and let each endpoint do one clear thing. The cart endpoints are chatty and cheap; the order endpoint is the one that carries the weight, and it takes an idempotency key so a retry is safe.
Two design choices are worth saying aloud. First, POST /v1/orders takes an Idempotency-Key header, a unique token the client generates once per checkout attempt, so the server can recognize a retry and return the same order instead of creating a second one. Second, availability shown during browse comes from a read replica and is allowed to be slightly stale, but the decrement at checkout goes through the primary and is exact. Naming that split early prevents the classic mistake of trusting a browse-time stock read at purchase time.
High-level design
Build it one requirement at a time and aim for a working skeleton before polishing anything. The system is a handful of focused services over the two stores from the set-up, plus a background scheduler for Autoship. The diagram codes each box by role: blue for Customer and outside dependencies, violet for our services, amber for scheduling and queues, green for stores. The walkthrough below tags each component the same way.
1) Build the cart. The Customer adds items, and the Cart service writes the basket to the Cart store keyed by customer, reading current price and availability from the Catalog service to display on each line. Nothing here is committed or reserved. Prices are shown live and stock is shown best-effort, because a cart is a wish list, not a promise. The bottleneck we are punting: the availability shown on a cart line can be stale by the time the customer checks out, so the real stock decision has to happen at checkout, not here. That is the overselling deep dive.
2) Place the order. The Customer posts to the Order service with an idempotency key. The order service asks the Inventory service to reserve and decrement stock, which locks the exact (sku, fcId) row in the Inventory DB and subtracts the quantity inside a transaction. It then charges through the Payment gateway, writes the immutable order to the Order DB, and returns a confirmation. This is the one place strong consistency is non-negotiable, because the decrement is what decides whether the sale is allowed. The bottleneck we are punting: the reservation, the charge, and the order write are three separate steps that can each fail independently. Coordinating them so a failure never strands stock or double-charges is a deep dive.
3) Hand off fulfillment. Once the order is written, the Order service drops a message on the Fulfillment queue and returns immediately. The warehouse and shipping systems pick the order up asynchronously, so the slow physical work never sits on the customer's confirmation path. A queue is the right tool because fulfillment is bursty and downstream, and it must not fail the order if a warehouse system is briefly slow. What we keep off the hot path is every second of picking and packing and label printing.
4) Fire Autoship. Separately, the Autoship scheduler periodically reads the Autoship DB for subscriptions whose nextRunAt has passed and drives each one through the same Order service path, then advances nextRunAt to the next cycle. A recurring order is just an order with no human present, so it reuses the reservation, charge, and write logic rather than duplicating it. The bottleneck we are punting: a scheduler that fires without a customer watching must be exactly-once per cycle. It must also handle a subscription whose item is now out of stock or repriced. That is the Autoship deep dive.
Technology choices
Each box is a category so far. Name a real tool for each and be ready to defend the category, because Chewy's system-design round leans on judgment more than on novelty. The theme: these are load levels a mature relational stack handles directly, so the right move is boring, correct tools, and a clear reason for each.
- The Order service, Cart service, and Inventory service are stateless Spring services behind a load balancer, which matches the stack Chewy runs. Stateless means any instance serves any request and a dead one drops out of rotation, so scaling is just adding boxes. The state that matters lives in the stores, not the services.
- The Order DB and Inventory DB are Postgres, one primary with read replicas. Orders and inventory are relational and need transactions, and the inventory decrement needs a real row lock, which Postgres gives you with
SELECT ... FOR UPDATEor an atomic conditionalUPDATE. If the primary fails, a replica is promoted; the brief write pause is acceptable because a customer will retry a checkout, and the idempotency key makes that retry safe. At a few million orders a day there is nothing to shard, so proposing sharding here reads as over-building. - The Cart store is Redis or DynamoDB, keyed by
customerId. Carts are tiny and disposable, so a fast key-value store beats a relational table, and a lost cart is a minor annoyance rather than a lost order. Set a TTL so abandoned carts expire on their own. - The Fulfillment queue is SQS or Kafka. A durable queue decouples the fast checkout from slow warehouse work and absorbs holiday bursts. The failure answer is built in: a message that fails processing retries with backoff and lands in a dead-letter queue for inspection, so a flaky downstream never fails a confirmed order.
- The Payment gateway is an external processor called with an idempotency key. We do not store raw card data; we hold a token and a charge id. The probe here is what happens on a timeout, and the answer is that an idempotent charge call plus a reconciliation check turns a "did it go through?" ambiguity into a safe retry.
- The Autoship scheduler is a cron-driven job or a managed scheduler querying
nextRunAt. It needs no streaming machinery, because the work arrives on a predictable cadence and the volume per run is bounded. Idempotency per cycle, not throughput, is the thing to design for.
Potential deep dives
The skeleton is deliberately calm. The signal is in the next four questions, and the interviewer will pick one or two and push. The first is where most of the round is won, because it is the one place correctness is truly on the line.
1) How do we never oversell a SKU under concurrent checkout?
Here is the tension, stated plainly. Two customers both have the last unit of a SKU in their carts, because the cart showed each of them "in stock" from a slightly stale read. They both hit checkout within the same second. If both are allowed to decrement, you have sold one bag of food twice. Someone gets a cancellation email, which for a customer whose dog is out of food is a real failure. The whole point of the checkout path is to serialize that last unit so exactly one of them wins.
Put a number on why this is not rare. A hot SKU during a promotion might see hundreds of checkouts a minute against a stock count in the dozens. Once available stock nears zero, the odds that two checkouts race for the same row stop being theoretical, so the decrement has to be correct under contention, not merely correct in the quiet case.
Approach
The order service reads available for the SKU, checks it is at least the requested quantity in application code, then writes back available - quantity.
Challenges
This is the textbook race. Two requests read the same available = 1, both pass the check, both write available = 0, and both orders are accepted. The gap between the read and the write is where the oversell lives, and no amount of retry logic around this shape closes it. Being able to name exactly why it fails is the setup for the tiers that fix it.
Approach
Do the check and the decrement as a single atomic database write: UPDATE inventory SET reserved = reserved + :q WHERE sku = :sku AND fcId = :fc AND (onHand - reserved) >= :q. The database evaluates the condition and applies the change under one row lock, so two concurrent checkouts for the last unit cannot both succeed. If the statement updates zero rows, there was not enough stock and you reject that checkout. This is the single most important line in the design, and it turns overselling from a race into an impossibility at the row level.
Challenges
It is correct, and for a Chewy SWE-I answer it is enough. It does not yet handle the multi-step nature of real checkout. You have reserved the stock, but you still have to charge the card and write the order. If the charge fails, you must give the reservation back. A raw decrement with no reservation concept leaks stock whenever a later step fails.
The good tier fixes the race, which is the core of the question. What forces the upgrade is that checkout is not one step. Reserve, charge, and record can each fail independently, and a design that decrements stock and then loses the payment has quietly removed a unit from sale that nobody bought. The great tier adds the reservation lifecycle that makes the whole transaction safe.
Approach
Split the stock change into a reservation and a commit. At checkout, the atomic statement moves stock into reserved (not yet gone) with a short expiry, say a few minutes. Then charge the Payment gateway and write the Order DB record. On success, convert the reservation to a real decrement by lowering onHand. On a payment failure or an abandoned checkout, release the reservation so the stock returns to available. A background sweep expires reservations whose TTL has passed, so a crashed checkout never strands stock forever. This is a small saga: the reservation is the compensable step, and releasing it is the compensation.
Challenges
It adds a reservation state and a sweeper to operate, and you have to be honest that the guarantee is careful bookkeeping rather than a single global lock. Name two edge cases. A customer whose reservation expires mid-checkout should get a clear "your cart item just sold out" instead of a silent failure. And the sweeper must be idempotent, so releasing an already-released reservation is a no-op. Both are cheap once you have named the reservation as its own state.
UPDATE ... WHERE available >= quantity line out loud; it is the sentence that tells the interviewer you have built this.2) How do we make placing an order exactly-once?
The failure is common and expensive. A customer taps "place order," the request succeeds on the server, but the response is lost to a dropped connection, so the app retries. Or an impatient shopper double-taps. Without protection, that is two orders, two charges, and two decrements for one intended purchase. The fix is to make the order write recognize a retry as the same request rather than a new one.
Approach
On each POST /v1/orders, look for a recent order from the same customer with the same items and total, and if one exists, assume it is a duplicate and skip.
Challenges
This guesses, and it guesses wrong in both directions. A customer who genuinely wants to buy the same food twice in a day is blocked, and two legitimately different carts that happen to total the same amount can collide. Content-matching is not identity, so it cannot deliver exactly-once. Name it to show you know an explicit key is the fix.
Approach
The client generates a unique Idempotency-Key once when the checkout screen loads and sends it on every attempt for that checkout. The Order service stores keys in a table with a unique constraint. The first request inserts the key inside the same transaction that reserves stock and writes the order, then records the resulting orderId against the key. A retry carrying the same key hits the unique constraint, so instead of creating a second order the service looks up and returns the original orderId. The payment call uses the same key downstream, so the processor also collapses the retry into one charge. One key, one order, one charge, no matter how many times the request arrives.
Challenges
Two details make it robust. The key insert and the order write must be in one transaction, or a crash between them leaves a key with no order and the retry returns nothing. And keys need a retention window, long enough to cover any realistic retry (hours, not seconds) but not forever, so an expiry job trims the table. Handle the in-flight case too: if a retry arrives while the first request is still processing, return a "in progress, retry shortly" rather than starting a second attempt.
Idempotency and the reservation from the first dive fit together cleanly: the key makes the whole reserve-charge-write sequence safe to retry, and the reservation makes each attempt safe to roll back. Together they turn a fragile multi-step checkout into one that tolerates the messy reality of mobile networks.
3) How does Autoship place recurring orders reliably?
Autoship is the piece that makes this Chewy's design rather than any store's. A customer subscribes to a 30-lb bag of food every four weeks, and from then on the system places that order on schedule with no one watching. The requirements shift: latency stops mattering (an order can fire minutes late) and reliability becomes everything (it must fire once per cycle, and it must handle the world having changed since the customer subscribed).
Size it so the mechanism is clear. If millions of active subscriptions spread across their cycles, a given hour might have tens of thousands due. That is a steady trickle, not a firehose, which is why a scheduled job querying nextRunAt is the right shape and a streaming system is not.
Approach
Every hour, one job selects all subscriptions where nextRunAt has passed and, in a loop, places each order and advances nextRunAt.
Challenges
It works until it does not. If the job crashes halfway, the subscriptions it already processed advanced and the rest did not, and a naive restart either skips the unprocessed ones or reprocesses the done ones, firing a duplicate order. A single serial loop also cannot keep up during a busy hour. The lesson is that "fire once per cycle" needs per-subscription bookkeeping, not one big batch.
Approach
The Autoship scheduler queries the Autoship DB for due subscriptions and claims each one atomically (an UPDATE ... SET status = 'processing' WHERE id = :id AND nextRunAt <= now()), so two scheduler instances never grab the same subscription and the work parallelizes safely. For each claim it calls the normal Order service with an idempotency key derived from (autoshipId, cycleDate). Even if the scheduler retries after a crash, the order path collapses it to one order per cycle. Only after the order is confirmed does it advance nextRunAt to the next cadence. Reusing the order path means the reservation logic and idempotency come for free.
Challenges
The interesting cases are about the world changing between subscribe and fire. If the item is out of stock at fire time, do not silently drop it: retry within a window, then notify the customer and reschedule rather than skipping their pet's food. If the price changed, Autoship's promise is to charge the current price, so freeze it at fire time like any order, and a large jump may warrant a heads-up. If the saved payment method is expired, fail gracefully and prompt the customer. Each of these is a policy decision, and naming them shows you understand Autoship is a customer promise, not just a cron job.
4) Where does the cart live, and how do carts merge?
A lighter dive the interviewer may reach for if there is time. The cart has softer guarantees than the order, which is exactly why it earns a different store, and the one genuinely fiddly case is a guest who builds a cart and then logs in.
Keep the cart in a fast key-value store (Redis or DynamoDB) keyed by customerId, with a TTL so abandoned carts expire on their own. A cart does not need transactions or joins, and treating a lost cart as a minor annoyance rather than a data-integrity event is the right call. The one correctness question is the merge: a shopper adds items as a guest under a temporary session id, then signs in and already had items saved from last week. The clean answer is to union the two carts by SKU, summing quantities and capping at available stock, then delete the guest cart. Read-your-writes matters here too. Route a customer's cart reads to the same node or use a store that gives per-key consistency. Otherwise the item they just added can appear to vanish on the next screen.
Prices and availability on cart lines are shown live from the catalog rather than frozen. A cart that has sat for a week reflects today's price and today's stock, and nothing is promised until checkout runs the atomic decrement. Saying that keeps the cart honest and defers every hard guarantee to the one path built to handle it.
What is expected at each level
Chewy scopes this round by level, and how far you carry the inventory and Autoship pieces is roughly what sorts the levels. Chewy's own careers page says round count and format vary by team, scope, and level, so treat the split below as a guide, not a contract.
SWE I
This level leans on breadth, roughly 80/20, and reports describe the round here as minimal, sometimes a single lighter session. Reach a working skeleton: the cart, order, and inventory services, sensible schemas with named keys, the checkout flow, and the atomic conditional decrement that prevents overselling. Above all, land that decrement correctly and do not over-build the rest. The bar: a clean, right-sized design with schemas and endpoints you can defend, and a correct answer for the last-unit race.
SWE II
Now depth enters, roughly 60/40. You should reach the reservation-with-TTL extension of the inventory answer without being pushed there. Design order placement as idempotent from the start instead of patching it later, and treat Autoship as a real component with exactly-once firing. You should be able to say why Postgres and not a sharded store, and why the cart lives somewhere else. The bar: you find the reservation and idempotency concerns yourself and defend the reasoning behind each.
Senior and above
Depth leads, roughly 40/60, and the scope widens past the happy path. Expect to reason about the reserve-charge-write sequence as a saga with clean compensation. Treat the Autoship edge cases (out of stock, price change, expired payment) as first-class policy. Plan degradation when the payment processor or a fulfillment center is down, and explain how inventory correctness would hold if you ever had to split stock across regions. Senior loops also draw the occasional generic prompt (a "design Twitter" or "car parking" style question), so be ready to apply the same requirements-first discipline to an unfamiliar shape. The bar: you own the correctness argument end to end and the interviewer redirects you rather than rescues you.
Key Takeaways
- ✓Read the altitude right: Chewy asks this at schema-and-API level, so size the load, notice a single Postgres primary handles a few million orders a day, and skip the reflexive sharding tour.
- ✓The round is won on inventory correctness. Prevent overselling with an atomic conditional decrement (
UPDATE ... WHERE available >= quantity), then extend it to a reservation with a TTL so a failed payment never strands stock. - ✓Make order placement exactly-once with a client-generated idempotency key stored under a unique constraint in the same transaction as the order write, so a retry returns the original order and never a second charge.
- ✓Autoship is the Chewy-specific piece: claim each due subscription atomically, drive it through the idempotent order path keyed by (autoshipId, cycleDate), and handle out-of-stock, price change, and expired payment as real customer-facing policy.
- ✓Keep the cart in a fast key-value store with softer guarantees, and defer every hard promise about price and stock to the one checkout path built to enforce it.
Chewy interview guideKnow what Chewy asks before you walk in.
9 coding · 1 system design · 28 behavioral questions
Get their questions first.
InterviewLegend is independent and not affiliated with or endorsed by any company it covers.