Skip to content

Design Amazon (cart, order, inventory, payment)

TL;DR

  • You build the checkout core: a catalog you can filter, a cart with no prices in it, stock that is reserved, then committed or released, and an order that walks a state machine.
  • Four decisions carry the interview: one lock per SKU taken in sorted order so a basket is all-or-nothing and cannot deadlock, a TTL on every hold so abandoned carts give stock back, an idempotency key with in-progress and completed states, and compensate across services, roll back inside one.
  • Patterns that earn their place: Facade (CheckoutFacade), State (the order table), Strategy (discount, tax, shipping), Event Bus plus Observer, Repository and Unit of Work, Specification (catalog search), Null Object (NoDiscount).

Problem statement

"Design the order and inventory core of a large online store. Customers browse a catalog, add items to a cart, and check out with an address and a card. You must never sell the same unit twice, even in a flash sale where thousands want the last dozen items. The order then moves through paid, packed, shipped and delivered, and can be cancelled or returned. Focus on the classes, the checkout flow, and what happens when two buyers race for the last unit or the same request arrives twice."

Requirements

Functional

  • Catalog of products, SKUs and categories, searchable by composable filters (in stock, under a price, by attribute).
  • Cart: add, update quantity, remove; a guest cart merges into the customer's cart at sign-in.
  • Inventory per SKU per warehouse, with available and reserved counters, reserve, commit, release and restock.
  • Checkout: price the basket (discount, tax, shipping), hold the stock, create the order, then take payment.
  • Order pricing through pluggable discount, tax and shipping rules.
  • Order state machine: created, paid, packed, shipped, delivered, cancelled, returned.
  • Low-stock alerts and restocking; order history per customer; notifications at each step.

Non-functional and constraints

  • Never oversell: if twelve units exist, at most twelve orders can hold them, whatever the concurrency.
  • A basket is all-or-nothing: a checkout that cannot get every line reserves nothing.
  • A retried checkout request must not buy twice, and a concurrent duplicate must not race.
  • Held stock must not be held forever: every hold has a deadline.
  • In-memory and single-process; repositories are the seam where a database goes; time and IDs are injected.

Out of scope: search ranking and relevance, recommendations, the real payment gateway, warehouse picking optimisation, and cross-region replication.

Clarifying questions and assumptions

Question to ask Assumption taken here
Is checkout one call or two? Two: checkout holds stock and creates the order, pay takes the money. A customer sits on the payment page for minutes, and that gap is where the TTL, the abandoned cart and the hold-expiry race live.
Do we decrement stock at checkout or at payment? Neither: we move it from available to reserved at checkout and out of both at payment. Two counters, not one, is what makes cancellation and expiry exact.
What if a basket has five lines and one is short? Nothing is reserved. All-or-nothing, because a partly reserved basket is a state nobody wants to write compensation for.
Can one SKU come from two warehouses? Yes. Allocation walks warehouses in a fixed order, which is also where a real system would put distance and cost.
What stops a bot parking all the stock in a cart? The hold TTL, swept by a background job. Fifteen minutes here; a flash sale would use two.
Does the price in the cart bind us? No. The cart holds ids and quantities; the order snapshots prices at checkout. The client may pass the total it displayed and get a conflict rather than a surprise charge.
Should reserving stock be an event? No: it must be synchronous and ordered with the payment. Low-stock alerts, notifications and shipment creation go on the bus, because those you can retry.

Core entities and relationships

  • Sku (frozen) is what you actually sell: a product plus variant attributes and a price. Product and Category organise them.
  • Cart holds SKU ids and quantities for one customer or one guest — and no prices at all.
  • InventoryItem is one SKU in one warehouse: available, reserved, and a version counter. It is the row a SQL UPDATE ... WHERE version = ? would target.
  • StockHold is an all-or-nothing reservation across SKUs and warehouses, made of HoldLines, with expires_at and a HoldStatus.
  • Order owns immutable OrderItem price snapshots, the address, the money components, the hold that backs it, and its idempotency key.
  • InventoryService owns every stock row and the per-SKU locks; OrderService owns the order repository and every transition; CheckoutFacade sequences them and owns the idempotency store.
  • EventBus carries order.placed, order.paid, order.shipped and order.cancelled to NotificationService, LowStockMonitor and ShipmentDispatcher; UnitOfWork groups repository writes; Specification composes catalog filters.

Multiplicities: product 1 → * SKUs, SKU 1 → * inventory rows (one per warehouse), cart 1 → * lines, order 1 → * order items, order 1 → 1 hold, hold 1 → * hold lines, order 1 → 0..1 payment and 1 → 0..1 shipment.

Class diagram

Domain: the catalog, the cart, and the two numbers that matter.

classDiagram
    class Category {
        +id: str
        +parent_id: str
    }
    class Product {
        +id: str
        +title: str
        +category_id: str
    }
    class Sku {
        +id: str
        +price: Money
        +attributes: tuple
        +label() str
    }
    class Warehouse {
        +id: str
        +region: str
    }
    class Cart {
        +customer_id: str
        +add(sku_id, quantity)
        +set_quantity(sku_id, quantity)
        +merge_from(guest)
    }
    class Address {
        +city: str
        +region: str
    }
    class InventoryItem {
        +sku_id: str
        +available: int
        +reserved: int
        +version: int
        +hold(quantity, expected_version)
        +commit(quantity)
        +release(quantity)
    }
    class HoldLine {
        +sku_id: str
        +warehouse_id: str
        +quantity: int
    }
    class StockHold {
        +id: str
        +owner: str
        +expires_at: float
        +status: HoldStatus
        +is_live(now) bool
    }
    class OrderItem {
        +sku_id: str
        +unit_price: Money
        +quantity: int
        +line_total Money
    }
    class Order {
        +id: str
        +status: OrderStatus
        +hold_id: str
        +idempotency_key: str
        +subtotal Money
        +total Money
        +can_move_to(target) bool
    }
    class Payment {
        +amount: Money
        +status: PaymentStatus
    }
    class Shipment {
        +carrier: str
        +tracking: str
    }
    class InventoryService {
        +reserve(lines, owner) StockHold
        +commit(hold_id) StockHold
        +release(hold_id) StockHold
        +expire_holds() List~StockHold~
        +available(sku_id) int
    }
    Category "1" o-- "many" Product
    Product "1" *-- "many" Sku
    Sku "1" o-- "many" InventoryItem : one row per warehouse
    InventoryItem --> Warehouse
    Cart ..> Sku : ids only, no prices
    InventoryService "1" *-- "many" InventoryItem
    InventoryService "1" o-- "many" StockHold
    StockHold "1" *-- "many" HoldLine
    Order "1" *-- "many" OrderItem
    Order --> "1" StockHold : backed by
    Order --> Address
    Order "1" o-- "0..1" Payment
    Order "1" o-- "0..1" Shipment

Services and patterns: one facade, three owners of state, and the rules you swap.

classDiagram
    class CheckoutFacade {
        +checkout(cart_id, customer_id, ship_to, idempotency_key) Order
        +pay(order_id) Payment
        +cancel(order_id) Order
        +accept_return(order_id) Order
        +sweep_expired_holds() List~str~
    }
    class CheckoutRecord {
        +key: str
        +order_id: str
    }
    class CatalogService {
        +search(spec, category_id) List~Sku~
        +reprice(sku_id, price) Sku
    }
    class CartService {
        +open(customer_id) Cart
        +merge_guest_cart(guest_cart_id, customer_id) Cart
    }
    class OrderService {
        +transition(order_id, target) Order
        +history(customer_id) List~Order~
    }
    class InventoryService
    class Repository {
        <<interface>>
        +add(entity)
        +get(entity_id)
        +all() List~Any~
    }
    class InMemoryRepository
    class UnitOfWork {
        +commit()
        +rollback()
    }
    class EventBus {
        +subscribe(topic, handler)
        +publish(event) int
    }
    class NotificationService {
        +inbox(customer_id) List~str~
    }
    class LowStockMonitor {
        +alerts() List~str~
    }
    class ShipmentDispatcher {
        +shipment_for(order_id) Shipment
    }
    class DiscountStrategy {
        <<interface>>
        +discount(items) Money
    }
    class NoDiscount
    class PercentOff
    class TaxCalculator {
        <<interface>>
        +tax(taxable, ship_to) Money
    }
    class ShippingStrategy {
        <<interface>>
        +cost(items, ship_to, speed) Money
    }
    class PaymentGateway {
        <<interface>>
        +authorize(amount, reference) bool
        +capture(amount, reference) bool
    }
    class Specification {
        <<abstract>>
        +is_satisfied_by(sku, stock) bool
    }
    CheckoutFacade --> CatalogService
    CheckoutFacade --> CartService
    CheckoutFacade --> InventoryService
    CheckoutFacade --> OrderService
    CheckoutFacade --> UnitOfWork
    CheckoutFacade --> EventBus
    CheckoutFacade --> PaymentGateway
    CheckoutFacade "1" o-- "many" CheckoutRecord
    CheckoutFacade --> DiscountStrategy
    CheckoutFacade --> TaxCalculator
    CheckoutFacade --> ShippingStrategy
    Repository <|.. InMemoryRepository
    UnitOfWork o-- "many" InMemoryRepository
    OrderService --> InMemoryRepository
    CatalogService ..> Specification
    DiscountStrategy <|.. NoDiscount
    DiscountStrategy <|.. PercentOff
    EventBus --> NotificationService : notifies
    EventBus --> LowStockMonitor : notifies
    EventBus --> ShipmentDispatcher : notifies

Design patterns applied

Pattern Where Why it earns its place
Facade CheckoutFacade Checkout touches five collaborators in a fixed order, and getting that order right is the answer. One class owns it; the API layer owns none of it.
State (as a table) ORDER_TRANSITIONS plus OrderService.transition Seven statuses touched by customers, warehouse staff and a sweeper. One dict, one gate, and the state diagram below is a drawing of that dict.
Strategy DiscountStrategy, TaxCalculator, ShippingStrategy The three rules that change per country, per campaign and per carrier. CheapestFreeInBundle shows why discount takes the lines and not the total: a bundle rule cannot be computed from one number.
Event Bus + Observer EventBus with three subscribers Notifications, low-stock alerts and shipment creation are retryable and nobody is waiting on them, so they go on the bus. Reserving stock is neither, so it does not. That sentence is the whole test for "should this be an event?".
Repository + Unit of Work InMemoryRepository, UnitOfWork The persistence seam, and why a declined card leaves no payment row. It wraps pay, where two repositories change together — wrapping the single write in checkout would be theatre.
Specification InStock() & PriceBelow(...) Filters become named, testable objects that compose, instead of a search() signature that grows a keyword argument per release.
Null Object NoDiscount, ZeroTax Pricing never writes if discount is None. The default is an object.
Dependency Injection Clock, IdGenerator, gateway, strategies The tests advance a FakeClock by 61 seconds to expire a hold and use a declining gateway to prove the release path. No sleeps, no wall clock.

What was deliberately not used: a Factory for orders (one construction path, so it adds a file and removes nothing) and Command for fulfilment (three one-line methods, and command objects buy undo you do not want — you cannot un-ship a parcel). Name both out loud: knowing which patterns to leave out is the same skill as knowing which to use.

Key flows

Checkout and payment: hold first, take money second, publish last.

sequenceDiagram
    autonumber
    actor U as Customer
    participant F as CheckoutFacade
    participant C as CatalogService
    participant I as InventoryService
    participant O as OrderService
    participant G as PaymentGateway
    participant B as EventBus
    U->>F: checkout(cart, address, key)
    F->>F: claim the idempotency key
    F->>C: snapshot SKU prices into order items
    F->>F: discount, tax, shipping, compare with the total shown
    F->>I: reserve(lines, owner=key)
    I->>I: lock every SKU in sorted order, plan, then write
    I-->>F: StockHold(HELD) with a TTL
    F->>O: write Order(CREATED)
    F-)B: order.placed
    B-)B: low-stock alert and customer notification
    F-->>U: Order(CREATED), stock is yours for 15 minutes
    U->>F: pay(order_id)
    F->>G: authorize(total)
    G-->>F: approved
    F->>I: commit(hold_id)
    F->>O: CREATED to PAID
    F-)B: order.paid
    B-)B: shipment created, customer notified

The three ways it goes wrong, and what each one gives back.

sequenceDiagram
    autonumber
    participant F as CheckoutFacade
    participant I as InventoryService
    participant O as OrderService
    participant G as PaymentGateway
    alt one line of the basket is short
        F->>I: reserve(lines)
        I-->>F: OutOfStockError, nothing written
        F->>F: drop the idempotency record so a retry is allowed
    else card declined at pay
        F->>G: authorize(total)
        G-->>F: declined
        F->>F: unit of work rolls the payment row back
        F->>I: release(hold_id) as a compensating call
        F->>O: CREATED to CANCELLED
    else customer took too long
        F->>I: commit(hold_id)
        I-->>F: HoldExpiredError, the sweeper already gave the units back
        F->>I: release(hold_id), safe even when overdue
        F->>O: CREATED to CANCELLED
    end

Stock hold lifecycle. Only HELD counts against available, and only one caller can move a hold out of it.

stateDiagram-v2
    [*] --> HELD : reserve moves units available to reserved
    HELD --> COMMITTED : payment taken, units leave the warehouse
    HELD --> RELEASED : declined, cancelled, or basket rejected
    HELD --> EXPIRED : TTL elapsed, sweeper returns the units
    COMMITTED --> [*]
    RELEASED --> [*]
    EXPIRED --> [*]

Order lifecycle. This is ORDER_TRANSITIONS drawn out; nothing else enforces it.

stateDiagram-v2
    [*] --> CREATED : stock held, awaiting payment
    CREATED --> PAID : card authorised, hold committed
    CREATED --> CANCELLED : declined, expired, or abandoned
    PAID --> PACKED : picked in the warehouse
    PAID --> CANCELLED : cancelled before picking, units restocked
    PACKED --> SHIPPED : handed to the carrier
    PACKED --> CANCELLED : pulled from the line, units restocked
    SHIPPED --> DELIVERED : signed for
    DELIVERED --> RETURNED : returned, units restocked and refunded
    CANCELLED --> [*]
    RETURNED --> [*]

Implementation

Write it in the order the interviewer wants: vocabulary and the transition table, then the inventory row, then the service that locks it, then the facade that sequences everything. RESTOCK_ON_CANCEL_FROM sits beside the enums because "when do units come back" is a business rule, not a service detail:

code/lld/ecommerce_order_inventory/models.py — statuses and the transition table
class OrderStatus(StrEnum):
    CREATED = "created"  # stock is held, money is not taken yet
    PAID = "paid"  # stock committed, ready to pick
    PACKED = "packed"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"
    RETURNED = "returned"


class HoldStatus(StrEnum):
    HELD = "held"  # units moved from available to reserved, TTL running
    COMMITTED = "committed"  # units have left the warehouse
    RELEASED = "released"  # given back deliberately
    EXPIRED = "expired"  # given back by the sweeper


class PaymentStatus(StrEnum):
    AUTHORIZED = "authorized"
    CAPTURED = "captured"
    VOIDED = "voided"
    REFUNDED = "refunded"


class ShippingSpeed(StrEnum):
    STANDARD = "standard"
    EXPRESS = "express"


#: The order lifecycle in one dict. ``OrderService.transition`` is the only gate,
#: and the state diagram on the page is a drawing of exactly this table.
ORDER_TRANSITIONS: dict[OrderStatus, frozenset[OrderStatus]] = {
    OrderStatus.CREATED: frozenset({OrderStatus.PAID, OrderStatus.CANCELLED}),
    OrderStatus.PAID: frozenset({OrderStatus.PACKED, OrderStatus.CANCELLED}),
    OrderStatus.PACKED: frozenset({OrderStatus.SHIPPED, OrderStatus.CANCELLED}),
    OrderStatus.SHIPPED: frozenset({OrderStatus.DELIVERED}),
    OrderStatus.DELIVERED: frozenset({OrderStatus.RETURNED}),
    OrderStatus.CANCELLED: frozenset(),
    OrderStatus.RETURNED: frozenset(),
}

#: Cancelling from these states must give the held or committed units back.
RESTOCK_ON_CANCEL_FROM = frozenset({OrderStatus.CREATED, OrderStatus.PAID, OrderStatus.PACKED})
code/lld/ecommerce_order_inventory/models.py — errors
class OutOfStockError(ConflictError):
    """Not enough units of a SKU to satisfy the request. The oversell guard."""


class HoldExpiredError(ConflictError):
    """The reservation TTL elapsed and the units went back on the shelf."""


class OrderStateError(InvalidStateError):
    """The order is not in a state that allows this transition."""


class PriceChangedError(ConflictError):
    """The basket total moved between the cart page and the checkout call."""


class CheckoutInProgressError(ConflictError):
    """Another request with the same idempotency key is still running."""


class PaymentDeclinedError(ConflictError):
    """The gateway refused the charge."""


class UnknownSkuError(NotFoundError):
    """No SKU with that id."""


class UnknownOrderError(NotFoundError):
    """No order with that id."""

The catalog is ordinary; the line to point at is that Cart stores ids and quantities and never a price.

code/lld/ecommerce_order_inventory/models.py — catalog and cart
@dataclass(frozen=True, slots=True)
class Category:
    id: str
    name: str
    parent_id: str | None = None


@dataclass(frozen=True, slots=True)
class Product:
    id: str
    title: str
    category_id: str
    brand: str = ""


@dataclass(frozen=True, slots=True)
class Sku:
    """The unit you actually sell: one product plus its variant attributes."""

    id: str
    product_id: str
    price: Money
    attributes: tuple[tuple[str, str], ...] = ()
    weight_grams: int = 500

    def label(self) -> str:
        return f"{self.id} ({', '.join(f'{k}={v}' for k, v in self.attributes)})" if self.attributes else self.id


@dataclass(frozen=True, slots=True)
class Warehouse:
    id: str
    name: str
    region: str


@dataclass(frozen=True, slots=True)
class Address:
    line1: str
    city: str
    postcode: str
    country: str
    region: str = ""


@dataclass(slots=True)
class Cart:
    """SKU ids and quantities. It holds no prices, which is the whole point."""

    id: str
    customer_id: str | None = None  # None means a guest cart
    lines: dict[str, int] = field(default_factory=dict)

    def add(self, sku_id: str, quantity: int = 1) -> None:
        if quantity <= 0:
            raise ValidationError("quantity must be positive")
        self.lines[sku_id] = self.lines.get(sku_id, 0) + quantity

    def set_quantity(self, sku_id: str, quantity: int) -> None:
        if quantity < 0:
            raise ValidationError("quantity cannot be negative")
        if quantity == 0:
            self.lines.pop(sku_id, None)
        else:
            self.lines[sku_id] = quantity

    def remove(self, sku_id: str) -> None:
        self.lines.pop(sku_id, None)

    def is_empty(self) -> bool:
        return not self.lines

    def merge_from(self, guest: Cart) -> None:
        """Sign-in merge: quantities add up, and the guest cart is left untouched."""
        for sku_id, quantity in guest.lines.items():
            self.lines[sku_id] = self.lines.get(sku_id, 0) + quantity

InventoryItem is where the interview is won or lost. Two counters instead of one, and a version that is the row version a database would check:

code/lld/ecommerce_order_inventory/models.py — the inventory row and the hold
@dataclass(slots=True)
class InventoryItem:
    """One SKU in one warehouse: what is on the shelf and what is spoken for.

    ``available`` and ``reserved`` are the two numbers the whole problem turns
    on, and ``version`` is the row version a SQL ``UPDATE ... WHERE version = ?``
    would check. In this process the per-SKU lock plays that role, but the
    counter is still here: it is how a caller that read the row earlier finds out
    the row moved underneath it, and it is the exact field you would carry into
    a database implementation.
    """

    sku_id: str
    warehouse_id: str
    available: int = 0
    reserved: int = 0
    version: int = 0

    def hold(self, quantity: int, expected_version: int | None = None) -> None:
        self._check_version(expected_version)
        if quantity <= 0:
            raise ValidationError("quantity must be positive")
        if quantity > self.available:
            raise OutOfStockError(f"{self.sku_id}@{self.warehouse_id}: {self.available} left, wanted {quantity}")
        self.available -= quantity
        self.reserved += quantity
        self.version += 1

    def commit(self, quantity: int, expected_version: int | None = None) -> None:
        """The units left the building. They are gone from both counters."""
        self._check_version(expected_version)
        if quantity > self.reserved:
            raise ConflictError(f"{self.sku_id}@{self.warehouse_id}: only {self.reserved} reserved")
        self.reserved -= quantity
        self.version += 1

    def release(self, quantity: int, expected_version: int | None = None) -> None:
        self._check_version(expected_version)
        if quantity > self.reserved:
            raise ConflictError(f"{self.sku_id}@{self.warehouse_id}: only {self.reserved} reserved")
        self.reserved -= quantity
        self.available += quantity
        self.version += 1

    def restock(self, quantity: int) -> None:
        if quantity <= 0:
            raise ValidationError("restock quantity must be positive")
        self.available += quantity
        self.version += 1

    def _check_version(self, expected_version: int | None) -> None:
        if expected_version is not None and expected_version != self.version:
            raise ConflictError(
                f"{self.sku_id}@{self.warehouse_id} moved: version {self.version}, expected {expected_version}"
            )


@dataclass(frozen=True, slots=True)
class HoldLine:
    sku_id: str
    warehouse_id: str
    quantity: int


@dataclass(slots=True)
class StockHold:
    """An all-or-nothing reservation across SKUs and warehouses, with a deadline."""

    id: str
    owner: str  # the checkout key or order id that owns these units
    lines: tuple[HoldLine, ...]
    created_at: float
    expires_at: float
    status: HoldStatus = HoldStatus.HELD

    def is_live(self, now: float) -> bool:
        return self.status is HoldStatus.HELD and now < self.expires_at

    def units(self) -> int:
        return sum(line.quantity for line in self.lines)

InventoryService owns the locks. reserve plans every line first and writes only once all of them can be satisfied, holding every lock throughout.

code/lld/ecommerce_order_inventory/inventory.py — reserve, commit, release, expire
class InventoryService:
    """The only object allowed to move a unit between available and reserved.

    **Lock granularity.** One lock per SKU, created lazily. Not one global lock
    (every buyer in the shop would queue behind the one popular item) and not one
    lock per warehouse row (a two-SKU basket would then take an unbounded number
    of locks in an order nobody has reasoned about). A basket spanning several
    SKUs acquires their locks **in sorted id order**, so two baskets containing
    the same pair of SKUs in opposite order still cannot deadlock.

    **Why all-or-nothing works.** ``reserve`` holds every lock it needs for the
    whole check-then-write. Either it finds enough of everything and writes all
    the rows, or it has written nothing at all and raises. There is no window in
    which a partly-reserved basket exists, so there is no compensation to run.

    **The TTL.** A hold is a promise with a deadline. ``expire_holds`` puts the
    units of abandoned checkouts back on the shelf, which is what stops a
    flash-sale bot from parking your whole stock in a cart for an hour.
    """

    HOLD_TTL_SECONDS = 900.0  # 15 minutes to finish a checkout

    def __init__(
        self,
        warehouses: Sequence[Warehouse] = (),
        clock: Clock | None = None,
        ids: IdGenerator | None = None,
        hold_ttl: float = HOLD_TTL_SECONDS,
    ) -> None:
        self._warehouses = list(warehouses)
        self._clock = clock or SystemClock()
        self._ids = ids or SequentialIdGenerator("H")
        self._ttl = hold_ttl
        self._rows: dict[tuple[str, str], InventoryItem] = {}
        self._sku_locks: dict[str, threading.Lock] = {}
        self._registry_lock = threading.Lock()  # guards _sku_locks and _rows membership
        self._holds: dict[str, StockHold] = {}
        self._holds_lock = threading.Lock()

    # -- stock levels -------------------------------------------------------------
    def add_stock(self, sku_id: str, warehouse_id: str, quantity: int) -> InventoryItem:
        with self._locked([sku_id]):
            row = self._row(sku_id, warehouse_id, create=True)
            row.restock(quantity)
            return row

    def row(self, sku_id: str, warehouse_id: str) -> InventoryItem:
        with self._locked([sku_id]):
            return self._row(sku_id, warehouse_id)

    def available(self, sku_id: str) -> int:
        """Free units across every warehouse. A read, so it takes only that SKU's lock."""
        with self._locked([sku_id]):
            return sum(r.available for r in self._rows_for(sku_id))

    def reserved(self, sku_id: str) -> int:
        with self._locked([sku_id]):
            return sum(r.reserved for r in self._rows_for(sku_id))

    def on_hand(self, sku_id: str) -> int:
        """available + reserved. This number only changes when goods physically move."""
        with self._locked([sku_id]):
            return sum(r.available + r.reserved for r in self._rows_for(sku_id))

    # -- the reserve / commit / release cycle -------------------------------------
    def reserve(self, lines: dict[str, int], owner: str) -> StockHold:
        """All-or-nothing hold across SKUs and warehouses. The oversell guard."""
        if not lines:
            raise ValidationError("nothing to reserve")
        allocated: list[HoldLine] = []
        with self._locked(lines):
            for sku_id in sorted(lines):
                allocated.extend(self._plan(sku_id, lines[sku_id]))  # raises before any write
            for line in allocated:
                self._rows[(line.sku_id, line.warehouse_id)].hold(line.quantity)
        now = self._clock.now()
        hold = StockHold(self._ids.next_id(), owner, tuple(allocated), now, now + self._ttl)
        with self._holds_lock:
            self._holds[hold.id] = hold
        return hold

    def commit(self, hold_id: str) -> StockHold:
        """The parcel is packed: the units leave both counters for good.

        This is the one operation the deadline blocks. A checkout that comes back
        after its TTL must not take units the shop has already re-offered.
        """
        return self._settle(hold_id, HoldStatus.COMMITTED)

    def release(self, hold_id: str) -> StockHold:
        """Payment failed or the order was cancelled: the units go back on the shelf.

        Giving units back is always safe, so an overdue hold can still be released
        -- only committing it is refused.
        """
        return self._settle(hold_id, HoldStatus.RELEASED)

    def expire_holds(self) -> list[StockHold]:
        """Sweep abandoned checkouts. Run it from a timer, or before a flash sale."""
        now = self._clock.now()
        with self._holds_lock:
            stale = [h for h in self._holds.values() if h.status is HoldStatus.HELD and now >= h.expires_at]
        expired: list[StockHold] = []
        for stale_hold in stale:
            try:
                expired.append(self._settle(stale_hold.id, HoldStatus.EXPIRED))
            except HoldExpiredError:
                continue  # a checkout committed or released it while we were sweeping
        return expired

    def hold(self, hold_id: str) -> StockHold:
        with self._holds_lock:
            try:
                return self._holds[hold_id]
            except KeyError:
                raise UnknownSkuError(f"no hold {hold_id}") from None

    def low_stock(self, threshold: int) -> list[tuple[str, int]]:
        """SKUs at or below the reorder point, for the restock alert."""
        with self._registry_lock:
            sku_ids = {sku_id for sku_id, _ in self._rows}
        low = [(sku_id, self.available(sku_id)) for sku_id in sorted(sku_ids)]
        return [(sku_id, free) for sku_id, free in low if free <= threshold]

    # -- internals ----------------------------------------------------------------
    def _settle(self, hold_id: str, target: HoldStatus) -> StockHold:
        """Check-and-flip the hold, then move the counters. One winner per hold."""
        hold = self.hold(hold_id)
        with self._holds_lock:
            if hold.status is not HoldStatus.HELD:
                raise HoldExpiredError(f"hold {hold_id} is already {hold.status}")
            if target is HoldStatus.COMMITTED and not hold.is_live(self._clock.now()):
                raise HoldExpiredError(f"hold {hold_id} expired at {hold.expires_at}")
            hold.status = target
        with self._locked({line.sku_id for line in hold.lines}):
            for line in hold.lines:
                row = self._rows[(line.sku_id, line.warehouse_id)]
                if target is HoldStatus.COMMITTED:
                    row.commit(line.quantity)
                else:
                    row.release(line.quantity)
        return hold

    def _plan(self, sku_id: str, quantity: int) -> list[HoldLine]:
        """Split one SKU across warehouses. Raises before anything is written."""
        remaining = quantity
        plan: list[HoldLine] = []
        for row in self._rows_for(sku_id):
            if remaining == 0:
                break
            take = min(row.available, remaining)
            if take:
                plan.append(HoldLine(sku_id, row.warehouse_id, take))
                remaining -= take
        if remaining:
            raise OutOfStockError(f"{sku_id}: short by {remaining} of {quantity}")
        return plan

    def _rows_for(self, sku_id: str) -> list[InventoryItem]:
        with self._registry_lock:
            keys = [k for k in self._rows if k[0] == sku_id]
        return [self._rows[k] for k in sorted(keys)]

    def _row(self, sku_id: str, warehouse_id: str, create: bool = False) -> InventoryItem:
        key = (sku_id, warehouse_id)
        with self._registry_lock:
            row = self._rows.get(key)
            if row is None:
                if not create:
                    raise UnknownSkuError(f"no stock row for {sku_id} at {warehouse_id}")
                row = self._rows.setdefault(key, InventoryItem(sku_id, warehouse_id))
            return row

    def _lock_for(self, sku_id: str) -> threading.Lock:
        with self._registry_lock:
            return self._sku_locks.setdefault(sku_id, threading.Lock())

    @contextmanager
    def _locked(self, sku_ids: Iterable[str]) -> Iterator[None]:
        """Take every SKU lock in sorted id order, so lock cycles are impossible."""
        locks = [self._lock_for(sku_id) for sku_id in sorted(set(sku_ids))]
        for lock in locks:
            lock.acquire()
        try:
            yield
        finally:
            for lock in reversed(locks):
                lock.release()

The unit of work is what makes a declined card leave nothing behind. Its docstring is deliberate about what it does not do:

code/lld/ecommerce_order_inventory/repository.py — Repository and UnitOfWork
class Repository(Protocol):
    """Collection-like access to one entity type. The only shape services depend on."""

    def add(self, entity: Any) -> None: ...

    def get(self, entity_id: str) -> Any: ...

    def all(self) -> list[Any]: ...


class InMemoryRepository:
    """Dict-backed repository. Swap it for a SQL one without touching a service."""

    def __init__(self, name: str) -> None:
        self.name = name
        self._items: dict[str, Any] = {}
        self._lock = threading.Lock()

    def add(self, entity: Any) -> None:
        with self._lock:
            self._items[entity.id] = entity

    def remove(self, entity_id: str) -> None:
        with self._lock:
            self._items.pop(entity_id, None)

    def get(self, entity_id: str) -> Any:
        with self._lock:
            try:
                return self._items[entity_id]
            except KeyError:
                raise NotFoundError(f"no {self.name} {entity_id}") from None

    def find(self, entity_id: str) -> Any | None:
        with self._lock:
            return self._items.get(entity_id)

    def all(self) -> list[Any]:
        with self._lock:
            return list(self._items.values())

    def snapshot(self) -> dict[str, Any]:
        with self._lock:
            return dict(self._items)

    def restore(self, snapshot: dict[str, Any]) -> None:
        with self._lock:
            self._items = dict(snapshot)


class UnitOfWork:
    """Group writes across repositories so a failure leaves nothing half-written.

    ``with uow:`` takes a membership snapshot of every repository. Leaving the
    block normally commits; leaving it with an exception restores every snapshot,
    so a checkout that dies after writing the order but before taking payment
    leaves no orphan order behind.

    What it does *not* do is undo field-level edits to entities already in a
    repository -- that needs a Memento, and saying so out loud is the honest
    answer to "is this a real unit of work?". Inventory is deliberately outside
    it: those units live in another service, so giving them back is a
    compensating call, not a rollback. That distinction is the saga in miniature.
    """

    def __init__(self, **repositories: InMemoryRepository) -> None:
        self._repositories = repositories
        self._snapshots: dict[str, dict[str, Any]] = {}
        self._lock = threading.RLock()
        for name, repository in repositories.items():
            setattr(self, name, repository)

    def __enter__(self) -> UnitOfWork:
        self._lock.acquire()
        self._snapshots = {name: repo.snapshot() for name, repo in self._repositories.items()}
        return self

    def __exit__(self, exc_type: type[BaseException] | None, *_: object) -> bool:
        try:
            if exc_type is not None:
                self.rollback()
        finally:
            self._snapshots = {}
            self._lock.release()
        return False  # never swallow the exception

    def commit(self) -> None:
        self._snapshots = {name: repo.snapshot() for name, repo in self._repositories.items()}

    def rollback(self) -> None:
        for name, snapshot in self._snapshots.items():
            self._repositories[name].restore(snapshot)

Pricing rules are Strategies over the order lines, not over a total:

code/lld/ecommerce_order_inventory/strategies.py — discount, tax, shipping
class DiscountStrategy(Protocol):
    """How much comes off the basket. Implementations are stateless."""

    def discount(self, items: tuple[OrderItem, ...]) -> Money: ...


class NoDiscount:
    """Null Object: the default. Pricing never writes ``if discount is None``."""

    def discount(self, items: tuple[OrderItem, ...]) -> Money:
        return Money(0)


class PercentOff:
    """A capped percentage off the basket, above a minimum spend."""

    def __init__(self, percent: Decimal, cap: Money, min_subtotal: Money | None = None) -> None:
        if not 0 < percent < 1:
            raise ValidationError("percent must be a fraction between 0 and 1")
        self._percent = percent
        self._cap = cap
        self._min = min_subtotal or Money(0)

    def discount(self, items: tuple[OrderItem, ...]) -> Money:
        subtotal = total_of(items)
        if subtotal < self._min:
            return Money(0)
        return min(subtotal * self._percent, self._cap)


class CheapestFreeInBundle:
    """Buy N of anything, the cheapest one is free. Needs the lines, not the total."""

    def __init__(self, bundle_size: int = 3) -> None:
        self._size = bundle_size

    def discount(self, items: tuple[OrderItem, ...]) -> Money:
        units = sorted(item.unit_price for item in items for _ in range(item.quantity))
        free = len(units) // self._size
        total = Money(0)
        for price in units[:free]:
            total = total + price
        return total


class TaxCalculator(Protocol):
    def tax(self, taxable: Money, ship_to: Address) -> Money: ...


class ZeroTax:
    def tax(self, taxable: Money, ship_to: Address) -> Money:
        return Money(0)


class RegionTax:
    """One rate per region, with a default. Regions are strings from the address."""

    def __init__(self, rates: dict[str, Decimal], default: Decimal = Decimal("0.20")) -> None:
        self._rates = dict(rates)
        self._default = default

    def tax(self, taxable: Money, ship_to: Address) -> Money:
        return taxable * self._rates.get(ship_to.region, self._default)


class ShippingStrategy(Protocol):
    def cost(self, items: tuple[OrderItem, ...], ship_to: Address, speed: ShippingSpeed) -> Money: ...


class WeightBandShipping:
    """Flat fee per speed, free over a threshold. The rule every store starts with."""

    def __init__(
        self,
        standard: Money | None = None,
        express: Money | None = None,
        free_over: Money | None = None,
    ) -> None:
        self._standard = standard or Money.of("4.99")
        self._express = express or Money.of("12.99")
        self._free_over = free_over or Money.of("50.00")

    def cost(self, items: tuple[OrderItem, ...], ship_to: Address, speed: ShippingSpeed) -> Money:
        if speed is ShippingSpeed.EXPRESS:
            return self._express
        return Money(0) if total_of(items) >= self._free_over else self._standard
code/lld/ecommerce_order_inventory/strategies.py — Specification
class Specification(ABC):
    """A composable catalog predicate. ``InStock() & PriceBelow(...)`` reads as English.

    The point is not the operators; it is that search filters become objects you
    can name, unit-test and reuse in a query builder, instead of a growing pile
    of keyword arguments on ``search()``.
    """

    @abstractmethod
    def is_satisfied_by(self, sku: Sku, stock: int) -> bool: ...

    def __and__(self, other: Specification) -> Specification:
        return AndSpecification(self, other)

    def __or__(self, other: Specification) -> Specification:
        return OrSpecification(self, other)

    def __invert__(self) -> Specification:
        return NotSpecification(self)


class AndSpecification(Specification):
    def __init__(self, left: Specification, right: Specification) -> None:
        self._left, self._right = left, right

    def is_satisfied_by(self, sku: Sku, stock: int) -> bool:
        return self._left.is_satisfied_by(sku, stock) and self._right.is_satisfied_by(sku, stock)


class OrSpecification(Specification):
    def __init__(self, left: Specification, right: Specification) -> None:
        self._left, self._right = left, right

    def is_satisfied_by(self, sku: Sku, stock: int) -> bool:
        return self._left.is_satisfied_by(sku, stock) or self._right.is_satisfied_by(sku, stock)


class NotSpecification(Specification):
    def __init__(self, inner: Specification) -> None:
        self._inner = inner

    def is_satisfied_by(self, sku: Sku, stock: int) -> bool:
        return not self._inner.is_satisfied_by(sku, stock)


class InStock(Specification):
    def __init__(self, minimum: int = 1) -> None:
        self._minimum = minimum

    def is_satisfied_by(self, sku: Sku, stock: int) -> bool:
        return stock >= self._minimum


class PriceBelow(Specification):
    def __init__(self, ceiling: Money) -> None:
        self._ceiling = ceiling

    def is_satisfied_by(self, sku: Sku, stock: int) -> bool:
        return sku.price < self._ceiling


class HasAttribute(Specification):
    def __init__(self, key: str, value: str) -> None:
        self._pair = (key, value)

    def is_satisfied_by(self, sku: Sku, stock: int) -> bool:
        return self._pair in sku.attributes

The bus is dull on purpose, and its docstring names what is not allowed on it:

code/lld/ecommerce_order_inventory/events.py — EventBus
class EventBus:
    """Topic to handlers, dispatched synchronously with per-handler error isolation.

    The lock guards the subscriber map and the failure log only. Handlers run
    *outside* it, so a slow listener never stalls the checkout that published the
    event, and a listener that raises is recorded rather than propagated -- an
    order must not fail because a low-stock alert did.

    What is deliberately **not** on this bus: reserving and committing stock.
    Those must be synchronous and ordered with the payment, so they are direct
    calls inside the checkout. The bus carries the effects you can afford to
    retry later: restock alerts, notifications, shipment creation.
    """

    def __init__(self) -> None:
        self._lock = threading.Lock()
        self._handlers: dict[str, list[Handler]] = {}
        self._failures: list[tuple[str, str]] = []

    def subscribe(self, topic: str, handler: Handler) -> None:
        with self._lock:
            self._handlers.setdefault(topic, []).append(handler)

    def publish(self, event: Event) -> int:
        with self._lock:
            handlers = list(self._handlers.get(event.topic, ()))
        delivered = 0
        for handler in handlers:
            try:
                handler(event)
                delivered += 1
            except Exception as exc:  # isolation is the whole point of a bus
                with self._lock:
                    self._failures.append((event.topic, str(exc)))
        return delivered

    def failures(self) -> list[tuple[str, str]]:
        with self._lock:
            return list(self._failures)
code/lld/ecommerce_order_inventory/events.py — the subscribers
class LowStockMonitor:
    """Inventory's ear on the bus: every placed order re-checks the reorder point.

    This is the part of inventory that *is* event-driven. Reserving stock has to
    happen inside the checkout; noticing that a SKU dropped below its reorder
    point does not, and running it here keeps the checkout path short.
    """

    def __init__(self, bus: EventBus, inventory: InventoryService, threshold: int = 3) -> None:
        self._bus = bus
        self._inventory = inventory
        self._threshold = threshold
        self._lock = threading.Lock()
        self._alerts: list[tuple[str, int]] = []
        bus.subscribe(ORDER_PLACED, self._on_order_placed)

    def _on_order_placed(self, event: Event) -> None:
        for sku_id in event.payload.get("sku_ids", "").split(","):
            if not sku_id:
                continue
            free = self._inventory.available(sku_id)
            if free <= self._threshold:
                with self._lock:
                    self._alerts.append((sku_id, free))
                self._bus.publish(Event(STOCK_LOW, event.at, {"sku_id": sku_id, "available": str(free)}))

    def alerts(self) -> list[tuple[str, int]]:
        with self._lock:
            return list(self._alerts)


class NotificationService:
    """One inbox per customer, filled by whatever the bus carries."""

    TEMPLATES: dict[str, str] = {
        ORDER_PLACED: "order {order_id} received, total {total}",
        ORDER_PAID: "payment taken for {order_id}",
        ORDER_SHIPPED: "{order_id} is on its way, tracking {tracking}",
        ORDER_CANCELLED: "{order_id} was cancelled",
    }

    def __init__(self, bus: EventBus) -> None:
        self._lock = threading.Lock()
        self._inboxes: dict[str, list[str]] = {}
        for topic in self.TEMPLATES:
            bus.subscribe(topic, self._on_event)

    def _on_event(self, event: Event) -> None:
        customer = event.payload.get("customer_id")
        if not customer:
            return
        message = self.TEMPLATES[event.topic].format_map(_Blank(event.payload))
        with self._lock:
            self._inboxes.setdefault(customer, []).append(message)

    def inbox(self, customer_id: str) -> list[str]:
        with self._lock:
            return list(self._inboxes.get(customer_id, ()))


class _Blank(dict):
    """``format_map`` helper: a missing key renders as ``-`` instead of raising."""

    def __missing__(self, key: str) -> str:
        return "-"

The facade last. Every line of checkout and pay is about ordering and recovery:

code/lld/ecommerce_order_inventory/checkout.py — CheckoutFacade
class CheckoutFacade:
    """Turn a cart into a paid order, or leave nothing behind.

    Checkout is deliberately **two calls**, because a real customer sits on the
    payment page for minutes:

    * ``checkout`` claims an idempotency key, prices the basket, reserves stock
      all-or-nothing, and writes an order in ``CREATED``. The units are now
      *held*, with a TTL running.
    * ``pay`` authorises the card, commits the held units and moves the order to
      ``PAID`` -- or, if the customer took too long and the sweeper already gave
      the units back, fails cleanly and cancels the order.

    Two rules run through both. **Compensate, do not roll back, across services:**
    stock lives in ``InventoryService``, so undoing a hold is a release call, not
    a database rollback. **Publish after committing:** events go out once the
    unit of work has closed, because publishing inside it is how stores send
    "order received" emails for orders that never existed.
    """

    def __init__(
        self,
        catalog: CatalogService,
        carts: CartService,
        inventory: InventoryService,
        orders: OrderService,
        bus: EventBus,
        discount: DiscountStrategy | None = None,
        tax: TaxCalculator | None = None,
        shipping: ShippingStrategy | None = None,
        gateway: PaymentGateway | None = None,
        clock: Clock | None = None,
        ids: IdGenerator | None = None,
    ) -> None:
        self.catalog = catalog
        self.carts = carts
        self.inventory = inventory
        self.orders = orders
        self.bus = bus
        self.payments = InMemoryRepository("payment")
        self._discount = discount or NoDiscount()
        self._tax = tax or RegionTax({})
        self._shipping = shipping or WeightBandShipping()
        self._gateway = gateway or ApprovingGateway()
        self._clock = clock or SystemClock()
        self._ids = ids or SequentialIdGenerator("PAY")
        self._uow = UnitOfWork(orders=orders.repository, payments=self.payments)
        self._checkouts: dict[str, CheckoutRecord] = {}
        self._lock = threading.Lock()

    def checkout(
        self,
        cart_id: str,
        customer_id: str,
        ship_to: Address,
        idempotency_key: str,
        speed: ShippingSpeed = ShippingSpeed.STANDARD,
        expected_total: Money | None = None,
    ) -> Order:
        """Price, hold stock, and create the order. Safe to call twice with one key."""
        replay = self._claim_key(idempotency_key)
        if replay is not None:
            return replay  # the same request arrived twice; nothing is bought twice

        hold = None
        try:
            items = snapshot_items(self.catalog, self.carts.cart(cart_id))
            discount = self._discount.discount(items)
            subtotal = Money(sum(item.line_total.cents for item in items))
            tax = self._tax.tax(subtotal - discount, ship_to)
            shipping = self._shipping.cost(items, ship_to, speed)
            total = subtotal - discount + tax + shipping
            if expected_total is not None and expected_total != total:
                raise PriceChangedError(f"basket is now {total}, you were shown {expected_total}")

            hold = self.inventory.reserve({i.sku_id: i.quantity for i in items}, owner=idempotency_key)
            order = Order(
                id=self.orders.next_id(),
                customer_id=customer_id,
                items=items,
                ship_to=ship_to,
                discount=discount,
                tax=tax,
                shipping=shipping,
                hold_id=hold.id,
                idempotency_key=idempotency_key,
                placed_at=self._clock.now(),
            )
            self.orders.repository.add(order)
        except Exception:
            if hold is not None:
                self._safe_release(hold.id)  # compensating call, not a rollback
            self._abandon_key(idempotency_key)
            raise

        self._complete_key(idempotency_key, order.id)
        self._publish(ORDER_PLACED, order)
        return order

    def pay(self, order_id: str) -> Payment:
        """Authorise, commit the held units, and move to PAID -- all or none of it.

        The unit of work earns its place here and not in ``checkout``: two
        repositories change together, and a decline must leave neither behind.
        """
        order = self.orders.order(order_id)
        if order.status is not OrderStatus.CREATED:
            raise OrderStateError(f"order {order_id} is {order.status}, not awaiting payment")
        payment = Payment(self._ids.next_id(), order.id, order.total, PaymentStatus.CAPTURED)
        try:
            with self._uow:
                if not self._gateway.authorize(order.total, order.id):
                    raise PaymentDeclinedError(f"card declined for {order.total}")
                self._uow.payments.add(payment)
                self.inventory.commit(order.hold_id)  # raises if the TTL sweeper won the race
                self.orders.transition(order.id, OrderStatus.PAID)
        except (PaymentDeclinedError, HoldExpiredError):
            self._safe_release(order.hold_id)
            self.orders.transition(order.id, OrderStatus.CANCELLED)
            self._publish(ORDER_CANCELLED, order)
            raise
        self._publish(ORDER_PAID, order)
        return payment

    # -- fulfilment --------------------------------------------------------------
    def pack(self, order_id: str) -> Order:
        return self.orders.transition(order_id, OrderStatus.PACKED)

    def ship(self, order_id: str) -> Order:
        order = self.orders.transition(order_id, OrderStatus.SHIPPED)
        self._publish(ORDER_SHIPPED, order)
        return order

    def deliver(self, order_id: str) -> Order:
        return self.orders.transition(order_id, OrderStatus.DELIVERED)

    def cancel(self, order_id: str) -> Order:
        """Give the units back the right way: release a hold, restock a commit."""
        previous = self.orders.order(order_id).status
        order = self.orders.transition(order_id, OrderStatus.CANCELLED)
        if previous is OrderStatus.CREATED:
            self._safe_release(order.hold_id)
        elif previous in RESTOCK_ON_CANCEL_FROM:
            self._restock(order)
            self._refund(order)
        self._publish(ORDER_CANCELLED, order)
        return order

    def accept_return(self, order_id: str) -> Order:
        order = self.orders.transition(order_id, OrderStatus.RETURNED)
        self._restock(order)
        self._refund(order)
        return order

    def sweep_expired_holds(self) -> list[str]:
        """Abandoned checkouts give their units back and their orders are cancelled."""
        cancelled = []
        for expired in self.inventory.expire_holds():
            for order in self.orders.repository.all():
                if order.hold_id == expired.id and order.status is OrderStatus.CREATED:
                    self.orders.transition(order.id, OrderStatus.CANCELLED)
                    cancelled.append(order.id)
        return cancelled

    # -- internals ---------------------------------------------------------------
    def _claim_key(self, key: str) -> Order | None:
        """In-progress or completed: the two states an idempotency record can be in."""
        with self._lock:
            record = self._checkouts.get(key)
            if record is None:
                self._checkouts[key] = CheckoutRecord(key, self._clock.now())
                return None
            order_id = record.order_id
        if order_id is None:
            raise CheckoutInProgressError(f"checkout {key} is already in flight")
        return self.orders.order(order_id)

    def _complete_key(self, key: str, order_id: str) -> None:
        with self._lock:
            self._checkouts[key].order_id = order_id

    def _abandon_key(self, key: str) -> None:
        with self._lock:
            self._checkouts.pop(key, None)  # a failed attempt may be retried

    def _safe_release(self, hold_id: str) -> None:
        try:
            self.inventory.release(hold_id)
        except HoldExpiredError:
            pass  # already released, expired or committed; nothing to give back

    def _restock(self, order: Order) -> None:
        for line in self.inventory.hold(order.hold_id).lines:
            self.inventory.add_stock(line.sku_id, line.warehouse_id, line.quantity)

    def _refund(self, order: Order) -> None:
        for payment in self.payments.all():
            if payment.order_id == order.id and payment.status is PaymentStatus.CAPTURED:
                payment.status = PaymentStatus.REFUNDED

    def _publish(self, topic: str, order: Order) -> None:
        payload = {
            "order_id": order.id,
            "customer_id": order.customer_id,
            "total": str(order.total),
            "sku_ids": ",".join(item.sku_id for item in order.items),
            "tracking": f"TRK-{order.id}",
        }
        self.bus.publish(Event(topic, self._clock.now(), payload))

Running python -m lld.ecommerce_order_inventory.demo walks a multi-warehouse hold, a retry, an oversell rejection and an abandoned cart:

search kitchen, in stock, under 50.00: ['sku-kettle', 'sku-mug']
kettle: 5 available across two warehouses
ORD-1 created: 175.00 USD - 15.00 USD + 32.00 USD tax + 0.00 USD ship = 192.00 USD
held ['2xkettle@w-east', '2xkettle@w-west', '2xmug@w-east']; kettles now 1 available, 4 reserved
retrying with key idem-1 returns ORD-1 again, not a second order
second buyer rejected: sku-kettle: short by 1 of 2
low-stock alerts raised on order.placed: [('sku-kettle', 1)]
PAY-1 captured 192.00 USD; kettles on hand 1
after the 15 min TTL the sweeper cancels ['ORD-2'], grinder back to 1
ORD-1 shipped via TRK-ORD-1
    cust-1: order ORD-1 received, total 192.00 USD
    cust-1: payment taken for ORD-1
    cust-1: ORD-1 is on its way, tracking TRK-ORD-1

Concurrency and edge cases

Which lock protects what. Four, and no method holds two of the interesting ones at once:

  1. InventoryService._sku_locks[sku_id] guards every warehouse row of that SKU: the oversell guard. One lock per SKU is the right granularity — a global lock puts every buyer behind whoever wants the popular item, and a lock per warehouse row makes a five-line basket take an unbounded set of locks in an order nobody has reasoned about.
  2. InventoryService._holds_lock guards the hold registry and the status flip, which makes commit, release and expire mutually exclusive per hold.
  3. OrderService._lock guards the order repository and every transition.
  4. CheckoutFacade._lock guards the idempotency store, held just long enough to read and write one key.

Why all-or-nothing works. reserve takes every SKU lock it needs in sorted id order, then plans all lines, then writes. Two baskets holding the same pair of SKUs in opposite order still acquire them in the same order, so there is no lock cycle. Because the plan is computed while holding every lock, either all lines succeed or nothing is written — no partly reserved basket, so no compensation to design. The database version of that sentence is "one transaction, rows locked in primary-key order".

Sizing it. A store doing 2M orders a day averages 2,000,000 / 86,400 ≈ 23 orders/s, and roughly 70/s at the usual 3x peak — trivially served. A flash sale is what breaks you, because every attempt contends on one SKU lock. An uncontended lock is about 17 ns, so the lock is never the cost; the critical section is, and it is a handful of integer updates. The real constraint is downstream: a single relational primary sustains roughly 5k-20k writes/s in total, which is why the production answer for a flash sale is an atomic decrement in Redis (about 100k ops/s on one instance) with the relational row reconciled behind it.

The reserve/commit/release cycle. available and reserved are two numbers, not one: reserve moves units between them, commit removes reserved units for good, release puts them back. The invariant the tests assert is the sum — units physically on hand — which only changes when goods really move. Stating that invariant is the difference between a candidate who has decremented a counter and one who has run a warehouse.

The TTL, and the one thing it blocks. expire_holds returns the units of abandoned checkouts and sweep_expired_holds cancels the orders waiting on them. The deadline blocks exactly one operation — commit — because taking units the shop has already re-offered is the only unsafe thing an overdue hold can do. Releasing an overdue hold is always allowed, and that asymmetry is what lets pay recover when the sweeper won the race.

The idempotency key. _claim_key implements the two states a real idempotency record has. No order id means in progress, so a concurrent duplicate is rejected with CheckoutInProgressError rather than allowed to reserve a second basket. An order id means completed, so the original order is replayed. A failed attempt deletes its record so a genuine retry can proceed. Those three behaviours are what "idempotent" means on a write endpoint.

Compensate across services, roll back inside one. The unit of work rolls the order and payment repositories back together because they are the same store; inventory is a different service, so undoing a hold is a release call — a compensating action, which is a saga in two steps. The same reasoning governs publishing: events go out after the unit of work closes, because publishing inside it is how stores send "order received" emails for orders that never existed. A transactional outbox is the production form of that fix.

Edge cases handled: one line of a basket short, a SKU split across two warehouses, an empty cart, a guest cart merging into an existing one, a repriced SKU after the order was placed, a total that moved between cart and checkout, a declined card, a hold that expired before payment, cancelling before and after payment, a return, a stale row version, and a subscriber that throws.

Common mistake

Modelling stock as a single quantity integer and decrementing it at checkout. It cannot answer any of the questions that follow: what happens when the payment fails, how much could you still sell, and what stops a bot holding your whole stock in a cart? Two counters plus a hold with a deadline answers all three, and it is the same shape as SELECT ... FOR UPDATE plus a reservations table.

Extensibility and follow-ups

  • Flash sales: shorten the TTL to about two minutes and move the hot SKU's counter to an atomic decrement in a cache, reconciled behind. InventoryService keeps its interface; only four method bodies change.
  • Returns as a saga: with a real gateway the refund is a network call that can fail, so accept_return becomes a compensating step with retries and a RETURN_PENDING status between DELIVERED and RETURNED.
  • Multi-warehouse allocation: _plan walks warehouses in id order. Sorting by distance or cost is an AllocationStrategy injected into InventoryService — one interface, no other change.
  • Backorders: a partly satisfiable basket fails today. Allowing it means a promised_at per hold line, an AWAITING_STOCK status, and a subscriber on the restock event that retries.
  • Persistence and scale: InMemoryRepository becomes a SQL repository, UnitOfWork.__enter__ becomes BEGIN, and the per-SKU lock becomes SELECT ... FOR UPDATE ordered by SKU id — or the optimistic WHERE version = ? the version field already anticipates. Beyond that it is a system design question, and the HLD case study is this problem with those constraints.

Interview tip

When they say "now make sure we never oversell", do not reach for a lock. Say the invariant first: "available + reserved is the physical stock and only changes when goods move; overselling means available went negative." Then choose the mechanism — a per-SKU lock here, SELECT ... FOR UPDATE in Postgres, an atomic decrement in Redis — and say why the granularity is a SKU. Invariant before mechanism is the clearest senior signal in this problem.

Tests

tests/test_ecommerce_order_inventory.py has 25 cases. The flagship is the oversell race: forty buyers, twelve units, sixteen threads, exactly twelve winners — plus the physical-stock invariant, which catches bugs the winner count alone would miss.

code/lld/ecommerce_order_inventory/tests/test_ecommerce_order_inventory.py — oversell and idempotency races
def test_forty_buyers_race_for_twelve_units_and_exactly_twelve_win(clock: FakeClock) -> None:
    shop = build(clock, stock={("sku-kettle", EAST.id): 12})

    def buy(i: int) -> bool:
        cart_id = cart_with(shop, f"cust-{i}", kettle=1)
        try:
            shop.checkout(cart_id, f"cust-{i}", SHIP_TO, f"key-{i}")
        except OutOfStockError:
            return False
        return True

    with ThreadPoolExecutor(max_workers=16) as pool:
        results = list(pool.map(buy, range(40)))

    assert results.count(True) == 12  # never 13, never 11
    assert shop.inventory.available("sku-kettle") == 0
    assert shop.inventory.reserved("sku-kettle") == 12
    assert shop.inventory.on_hand("sku-kettle") == 12  # nothing was created or destroyed


def test_concurrent_duplicate_keys_produce_exactly_one_order(clock: FakeClock) -> None:
    shop = build(clock)
    cart_id = cart_with(shop, "cust-1", kettle=1)

    def submit(_: int) -> str | None:
        try:
            return shop.checkout(cart_id, "cust-1", SHIP_TO, "same-key").id
        except CheckoutInProgressError:
            return None

    with ThreadPoolExecutor(max_workers=10) as pool:
        ids = list(pool.map(submit, range(10)))

    created = [i for i in ids if i is not None]
    assert len(set(created)) == 1  # every caller that got an answer got the same order
    assert len(shop.orders.repository.all()) == 1
    assert shop.inventory.reserved("sku-kettle") == 1  # one hold, not ten

The second half of that snippet fires ten concurrent checkouts with the same idempotency key and asserts one order, one hold, and one answer shared by every caller that got one.

The rest cover: checkout to payment with the pricing arithmetic and a repricing the order ignores; all-or-nothing reserve leaving the other SKU untouched; a SKU split across warehouses; the TTL expiring and the sweeper cancelling the order; paying after expiry with no payment row; a declined card releasing the hold; the transition table; a return restocking and refunding; price drift rejected before anything is held; the guest-cart merge; composed specifications; a stale version; a throwing subscriber; and five discount strategies. Run them with uv run pytest code/lld/ecommerce_order_inventory -q.

45-minute pacing

Minutes What to do What to say or write
0–5 Clarify One checkout call or two? All-or-nothing baskets? Multi-warehouse? What happens on a retry? Out of scope: search ranking, recommendations, real gateway.
5–10 The invariant Draw available and reserved as two boxes and the arrows between them: "physical stock is the sum, and it only changes when goods move".
10–18 Entities and class diagram Sku, Cart with no prices, InventoryItem, StockHold with a TTL, Order with a snapshot. Then the facade over three services.
18–34 Code InventoryItem.hold → InventoryService.reserve with sorted locks → CheckoutFacade.checkout → pay with the unit of work and the compensating release.
34–40 Concurrency Per-SKU locks and why that granularity, the sorted order, the TTL asymmetry, and the idempotency record's two states.
40–45 Extensions Flash sale with an atomic decrement, returns as a saga, allocation strategy, the outbox — then hand off to the HLD version.