requirements

Say PlateRoute expands beyond made-to-order food into a marketplace tab where restaurants sell packaged goods with real, finite inventory — spice kits, branded merchandise, bottled sauces. Unlike a food order (which a kitchen always has the capacity to "make more of"), a packaged item can genuinely sell out, which changes the design in an important way.
FunctionalNon-functional
Browse and search the marketplace catalogNever sell an item that's actually out of stock (no overselling)
Add items to a cart and check outCheckout must stay fast even during a flash sale
View order historyPayment and inventory changes must be consistent with each other

high-level design

ComponentRole
Catalog ServiceBacked by a document store (see SQL vs. NoSQL) since items have irregular attributes (a spice kit has heat level; a t-shirt has size).
Search ServiceA distributed search index kept in sync with the catalog for fuzzy, ranked browsing.
Cart ServiceSession-scoped, a natural fit for a key-value store.
Order ServiceThe relational core of checkout — needs ACID transactions for the inventory-and-payment sequence below.
Payment ServiceTalks to an external payment gateway; every call is designed to be idempotent (see API Design).

the interesting part: not overselling the last item

This is the distinctive component of this design — the one piece that isn't boilerplate for a marketplace checkout. Picture the last unit of a popular spice kit, and two customers checking out within the same second.
The naive approach — charge the card first, then decrement inventory — risks charging both customers for an item only one of them can actually receive. The fix is ordering the steps so inventory is reserved before payment is attempted:

BEGIN TRANSACTION;
  UPDATE inventory SET qty = qty - 1 WHERE item_id = 'spice_042' AND qty > 0;
  -- if this UPDATE affects 0 rows, the item is already sold out — abort here,
  -- before ever contacting the payment gateway
COMMIT;

-- only if the inventory decrement succeeded:
call PaymentService.charge(customer, amount)
            
The qty > 0 guard combined with a single atomic transaction (see ACID Transactions Explained) is what prevents two concurrent checkouts from both successfully decrementing a quantity of 1 — the second transaction's UPDATE simply affects zero rows and the order is correctly rejected before payment is ever attempted.

handling the abandoned checkout

Reserving inventory before payment creates a new problem: what if a customer's card is decremented in inventory but they close the browser tab before paying? The item is stuck "reserved" forever with no completed order.
The fix is a reservation with a short TTL, held in a distributed cache: when inventory is decremented, a reservation record (order ID, item, timestamp) is written with, say, a 5-minute expiry. If payment completes within that window, the reservation is deleted and the order is finalized. If it expires unclaimed, a background process restores the inventory count automatically — the same TTL-expiry pattern covered in Distributed Caching, applied to a business problem instead of a performance one.
A genuine race condition remains: payment can succeed at almost the exact instant the reservation expires. The practical fix is to delete the reservation record as soon as payment succeeds, and treat a payment success that arrives just after expiry as a special case — either manually re-reserving inventory for it, or refunding if the item is now truly gone. This edge case is worth naming explicitly in a design review, not silently ignored.

scaling order history: moving finished orders out of the hot path

Active orders (placed, in preparation, out for delivery) need the strong consistency a relational database provides. But over years, the orders table accumulates far more completed orders than active ones, and a customer's occasional "view my order history" query doesn't need transactional guarantees the way an active order does.
The fix: once an order reaches a terminal state (delivered or cancelled), an archival job moves it out of the relational database into a columnar store better suited to a large, append-heavy, rarely-updated dataset — keeping the relational database's hot path (active orders) small and fast, while historical queries are served from storage built for exactly that access pattern.

evaluation

RequirementHow it's met
No oversellingInventory decrement is atomic and happens before payment, with a guard clause preventing negative stock.
Fast checkout under loadCart and catalog reads are cache-backed; only the final inventory-and-payment step needs strong consistency.
Consistency between payment and inventoryA single ACID transaction for the inventory step, plus TTL-based reservation cleanup for abandoned checkouts.
Scalable order historyTerminal-state orders are archived out of the relational hot path into a store built for large-scale historical reads.

related topics

reference