Design a stock brokerage system¶
TL;DR¶
- You build the broker, not the exchange: accounts with cash and holdings, orders that reserve before they trade, and settlement driven by execution reports from a venue you do not control.
- Three decisions carry the interview: reserve funds or shares atomically before the order leaves the building, settle every fill idempotently on its fill id, and never hold the account lock across the call to the exchange.
- Observer drives the price feed into resting limit orders and alerts; the order lifecycle is an explicit transition table; Unit of Work makes cash, holding, order and trade commit together.
Problem statement¶
"Design the backend of a retail brokerage. Users have an account with cash and a portfolio, keep watchlists, and place market and limit orders to buy and sell. Orders are validated against available funds and holdings, sent to an exchange, and may come back filled, partially filled or not at all. Trades update cash and holdings; users see order history and get price alerts. Focus on the classes, the order lifecycle, and what happens when the exchange sends the same execution report twice."
Requirements¶
Functional
- Accounts holding cash and a
PortfolioofHoldingrows (quantity plus average cost). - A market data feed of quotes; watchlists that follow it; price alerts that fire once when a threshold is crossed.
- Market and limit orders, buy and sell, validated against available cash and available shares.
- An order lifecycle: new, submitted, partially filled, filled, cancelled, rejected — with only legal transitions allowed.
- Fills arrive from the exchange as execution reports; each one becomes a
Tradethat updates cash and holdings. - Cancel an open order and get the unused reservation back.
- Order and trade history per account.
Non-functional and constraints
- Money is
common.Money(integer cents).Decimalwould also be defensible; a float is not. - Reserving funds must be atomic: 20 concurrent orders against a balance that covers five must accept exactly five.
- Execution reports are at-least-once. Settlement must be effectively-once: the same fill id may arrive many times and must move the account once.
- No network call happens while an account lock is held.
- In-memory, single process; the exchange is behind a
Protocolso a real venue drops in.
Out of scope: the matching engine itself (that is the exchange), margin and leverage, options, tax lots, market hours and halts, regulatory reporting.
Clarifying questions and assumptions¶
| Question to ask | Assumption taken here |
|---|---|
| Are we the broker or the exchange? | The broker. We reserve, submit, and settle what comes back. The venue is a Protocol with submit and cancel. |
| Do market orders reserve cash, and at what price? | Yes, at the last print plus 5% headroom, because the price moves between reservation and fill. A limit order reserves exactly limit x quantity. |
| Can an order fill in pieces? | Yes. Each slice releases its share of the reservation; the remainder is released when the order reaches a terminal state. |
| Can the exchange send the same fill twice? | Assume yes. Fill.fill_id is the idempotency key and the account stores the trade it produced. |
| What if a fill arrives after we cancelled? | It is refused. A production broker treats the venue as the authority and reconciles; say so, then show the guard. |
| Decimal or integer cents? | Integer cents. Share counts are ints, prices are cents, and price * quantity is exact — no quantisation rules to argue about. |
| Do we simulate the whole book? | No. SimulatedExchange rests orders and fills them when a tick makes them marketable, with a knob for partial fills. |
Core entities and relationships¶
- Account — cash,
reserved_cashand aPortfolio.available_cash()iscash - reserved_cash, and it is the only number order validation looks at. - Portfolio and Holding — quantity,
reserved(claimed by open sell orders) and weightedaverage_cost.available()mirrorsavailable_cash(). - Order (base) with MarketOrder and LimitOrder, built by OrderFactory. The subclasses answer exactly three questions: would you trade at this price, at what price, and what price sizes the reservation.
- OrderStatus plus the
ORDER_TRANSITIONStable — the state machine, enforced byOrder.transition_to. - Quote, Fill, Trade — all frozen.
Fillis what the venue says happened;Tradeis what the account actually booked. - MarketDataFeed — the subject. SimulatedExchange, AlertService and Watchlist are its observers;
RestingOrderis the venue's own copy of a working order. - BrokerageStore holds one
AccountStateand one lock per account; AccountUnitOfWork is the transaction boundary; OrderService is the facade and the only writer.
Multiplicities: account 1 -> 1 portfolio, portfolio 1 -> * holdings, account 1 -> * orders, order 1 -> * trades, feed 1 -> * listeners.
Class diagram¶
The domain: an account, its holdings, and the two kinds of order.
classDiagram
class Account {
+id: str
+cash: Money
+reserved_cash: Money
+available_cash() Money
+reserve_cash(amount)
+release_cash(amount)
+debit(amount)
+credit(amount)
}
class Portfolio {
+holdings: Dict~str,Holding~
+holding(symbol) Holding
+value(prices) Money
}
class Holding {
+symbol: str
+quantity: int
+reserved: int
+average_cost: Money
+available() int
+add(quantity, price)
+remove(quantity)
}
class Stock {
+symbol: str
+name: str
}
class Quote {
+symbol: str
+price: Money
+at: float
}
class Fill {
+fill_id: str
+order_id: str
+quantity: int
+price: Money
}
class Trade {
+id: str
+symbol: str
+quantity: int
+price: Money
+notional: Money
}
class Order {
+id: str
+side: OrderSide
+quantity: int
+filled_quantity: int
+unit_reserve: Money
+status: OrderStatus
+remaining() int
+is_marketable(quote) bool
+execution_price(quote) Money
+reference_price(quote) Money
+transition_to(status)
+apply_fill(quantity, price)
}
class MarketOrder
class LimitOrder {
+limit_price: Money
}
class OrderFactory {
+create(order_type, ...) Order
}
class Watchlist {
+symbols: set
+on_quote(quote)
+render() str
}
class PriceAlert {
+threshold: Money
+matches(quote) bool
+trigger(at)
}
Account "1" *-- "1" Portfolio
Portfolio "1" *-- "many" Holding
Order <|-- MarketOrder
Order <|-- LimitOrder
OrderFactory ..> Order : creates
Order "1" --> "many" Trade : settles into
Fill ..> Trade : becomes
Holding --> Stock
Watchlist ..> Quote
PriceAlert ..> Quote
The services: the feed and its observers, the venue, and the transaction boundary.
classDiagram
class QuoteListener {
<<interface>>
+on_quote(quote)
}
class MarketDataFeed {
-_lock: Lock
+subscribe(symbol, listener)
+subscribe_all(listener)
+publish(quote)
+last(symbol) Quote
}
class AlertService {
+register(alert) PriceAlert
+on_quote(quote)
+triggered() List~PriceAlert~
}
class ExchangeGateway {
<<interface>>
+submit(order) str
+cancel(exchange_order_id) bool
}
class SimulatedExchange {
-_book: Dict~str,RestingOrder~
+connect(on_fill)
+submit(order) str
+cancel(exchange_order_id) bool
+on_quote(quote)
}
class RestingOrder {
+exchange_order_id: str
+remaining: int
}
class OrderService {
+place_order(...) Order
+cancel_order(account_id, order_id) Order
+on_fill(fill) Trade
+open_orders(account_id) List~Order~
}
class BrokerageStore {
+account_lock(account_id) Lock
+snapshot(account_id) AccountState
+publish(state)
+account_for_order(order_id) str
}
class AccountState {
+account: Account
+orders: Dict~str,Order~
+trades_by_fill: Dict~str,Trade~
+copy() AccountState
}
class UnitOfWork {
<<interface>>
+commit()
+rollback()
}
class AccountUnitOfWork {
+state: AccountState
+commit()
+rollback()
}
class TradeListener {
<<interface>>
+on_trade(trade)
}
class TradeLog
class OrderCommand {
<<interface>>
+execute() Order
+undo() Order
}
class PlaceOrderCommand
QuoteListener <|.. SimulatedExchange
QuoteListener <|.. AlertService
MarketDataFeed --> "many" QuoteListener : notifies
ExchangeGateway <|.. SimulatedExchange
SimulatedExchange "1" o-- "many" RestingOrder
OrderService --> ExchangeGateway
OrderService --> MarketDataFeed
OrderService --> AccountUnitOfWork
OrderService --> "many" TradeListener : notifies
UnitOfWork <|.. AccountUnitOfWork
AccountUnitOfWork --> BrokerageStore
AccountUnitOfWork --> AccountState
TradeListener <|.. TradeLog
OrderCommand <|.. PlaceOrderCommand
Design patterns applied¶
| Pattern | Where | Why it earns its place |
|---|---|---|
| Observer | MarketDataFeed -> SimulatedExchange, AlertService, Watchlist |
One tick drives three unrelated reactions. Adding trailing stops or a chart service is a new listener, and the feed never learns their names. |
| State (transition table) | OrderStatus + ORDER_TRANSITIONS + Order.transition_to |
Six states, one table, every illegal move rejected in one place. Six State classes would be ceremony; say that and show the parametrized test. |
| Template Method / polymorphism | Order.is_marketable, execution_price, reference_price |
The service never asks "is this a limit order?". Adding stop-loss is a third subclass and one registry branch. |
| Factory Method | OrderFactory.create |
Turns ("limit", 395.00) from the API into the right class, and rejects a market order carrying a limit price. |
| Unit of Work | AccountUnitOfWork |
A settled fill touches cash, reservation, holding, order status and the trade row. All five commit together or none do. |
| Adapter behind a Protocol | ExchangeGateway / SimulatedExchange |
The venue is someone else's system. Tests inject a simulator with a partial-fill knob; production injects FIX or REST. |
| Command | PlaceOrderCommand |
Placement and cancellation are one request-object pair, which is what an order-entry queue or a "cancel my last order" button needs. |
| Facade | OrderService |
The app calls four methods. Reservation, transaction, venue and notification are all behind them. |
What was deliberately not used: a Singleton feed or store. Both are created in main and injected, so tests build a dozen independent brokerages. Also no Memento for order history — the trade rows already are the history, and duplicating them into snapshots would create two truths.
Key flows¶
A limit buy from reservation to settlement. Note where the account lock is not held.
sequenceDiagram
autonumber
actor U as User
participant S as OrderService
participant W as AccountUnitOfWork
participant A as Account
participant X as SimulatedExchange
participant F as MarketDataFeed
U->>S: place_order(acc-1, MSFT, BUY, 30, LIMIT 395.00)
S->>W: open (snapshot under the account lock)
S->>A: reserve_cash(30 x 395.00)
A-->>S: ok, available_cash reduced
S->>W: store order NEW, commit
Note over S,X: the account lock is released before any network call
S->>X: submit(order)
X-->>S: exchange_order_id
S->>W: acknowledge, NEW to SUBMITTED
F-)X: on_quote(MSFT 394.00)
X-)S: on_fill(fill_id, 20 shares at 394.00)
S->>W: open, check fill_id not seen
S->>A: release_cash(20 x 395.00), debit(20 x 394.00), holding.add(20)
S->>W: order to PARTIALLY_FILLED, store trade by fill_id, commit
S-)U: on_trade notification
Order lifecycle. The transition table is the code; this diagram is the picture of it.
stateDiagram-v2
[*] --> NEW : validated, funds or shares reserved
NEW --> SUBMITTED : venue acknowledged
NEW --> REJECTED : venue refused
NEW --> CANCELLED : pulled before acknowledgement
SUBMITTED --> PARTIALLY_FILLED : first slice executes
SUBMITTED --> FILLED : executed in one go
SUBMITTED --> CANCELLED : client cancels
SUBMITTED --> REJECTED : venue rejects late
PARTIALLY_FILLED --> PARTIALLY_FILLED : another slice
PARTIALLY_FILLED --> FILLED : last slice
PARTIALLY_FILLED --> CANCELLED : client cancels the rest
FILLED --> [*]
CANCELLED --> [*]
REJECTED --> [*]
Implementation¶
Write the vocabulary first, then the account, then the order hierarchy, then the feed, and only then the service that ties them together.
The enums include the transition table. Putting it next to OrderStatus is deliberate: the legal moves are part of the vocabulary, not scattered through the service.
class OrderSide(StrEnum):
BUY = "buy"
SELL = "sell"
class OrderType(StrEnum):
MARKET = "market" # execute at whatever the feed says now
LIMIT = "limit" # execute only at the limit price or better
class OrderStatus(StrEnum):
NEW = "new" # validated, funds or shares reserved, not yet acknowledged
SUBMITTED = "submitted" # the exchange acknowledged it and it rests on the book
PARTIALLY_FILLED = "partially_filled" # some shares traded, the rest still resting
FILLED = "filled" # fully executed
CANCELLED = "cancelled" # pulled by the client, reservation released
REJECTED = "rejected" # the exchange refused it, reservation released
class AlertDirection(StrEnum):
ABOVE = "above"
BELOW = "below"
TERMINAL_STATUSES = frozenset({OrderStatus.FILLED, OrderStatus.CANCELLED, OrderStatus.REJECTED})
ORDER_TRANSITIONS: Mapping[OrderStatus, frozenset[OrderStatus]] = {
OrderStatus.NEW: frozenset({OrderStatus.SUBMITTED, OrderStatus.CANCELLED, OrderStatus.REJECTED}),
OrderStatus.SUBMITTED: frozenset(
{OrderStatus.PARTIALLY_FILLED, OrderStatus.FILLED, OrderStatus.CANCELLED, OrderStatus.REJECTED}
),
OrderStatus.PARTIALLY_FILLED: frozenset(
{OrderStatus.PARTIALLY_FILLED, OrderStatus.FILLED, OrderStatus.CANCELLED}
),
OrderStatus.FILLED: frozenset(),
OrderStatus.CANCELLED: frozenset(),
OrderStatus.REJECTED: frozenset(),
}
class InsufficientFundsError(ConflictError):
"""Available cash (balance minus reservations) cannot cover the order."""
class InsufficientHoldingsError(ConflictError):
"""The account does not hold enough unreserved shares to sell."""
class OrderStateError(InvalidStateError):
"""The order is not in a state that allows this transition."""
class UnknownEntityError(NotFoundError):
"""No such account, order or symbol."""
Quote, Fill and Trade are frozen. Fill is the venue's claim; Trade is what your ledger booked. Keeping them as separate types is what makes the idempotency check obvious later.
@dataclass(frozen=True, slots=True)
class Stock:
symbol: str
name: str
@dataclass(frozen=True, slots=True)
class Quote:
"""One price tick from the feed. Immutable, so a listener cannot rewrite history."""
symbol: str
price: Money
at: float
@dataclass(frozen=True, slots=True)
class Fill:
"""An execution report from the exchange. ``fill_id`` is the idempotency key."""
fill_id: str
order_id: str
quantity: int
price: Money
at: float
@dataclass(frozen=True, slots=True)
class Trade:
"""The settled result of one fill: what actually moved in the account."""
id: str
order_id: str
account_id: str
symbol: str
side: OrderSide
quantity: int
price: Money
notional: Money
at: float
The account is where "available" is defined. Every validation in the system reduces to available_cash() or Holding.available(), which is why a reservation is enough to make concurrent orders safe.
@dataclass(slots=True)
class Holding:
"""Shares of one symbol. ``reserved`` is what open sell orders have claimed."""
symbol: str
quantity: int = 0
reserved: int = 0
average_cost: Money = Money(0)
def available(self) -> int:
return self.quantity - self.reserved
def reserve(self, quantity: int) -> None:
if quantity > self.available():
raise InsufficientHoldingsError(
f"{self.symbol}: {self.available()} shares available, {quantity} requested"
)
self.reserved += quantity
def release(self, quantity: int) -> None:
self.reserved = max(0, self.reserved - quantity)
def add(self, quantity: int, price: Money) -> None:
"""Weighted average cost; the rounding here is for display, never for cash."""
total = self.average_cost * self.quantity + price * quantity
self.quantity += quantity
self.average_cost = Money(total.cents // self.quantity, price.currency)
def remove(self, quantity: int) -> None:
if quantity > self.quantity:
raise InsufficientHoldingsError(f"{self.symbol}: cannot remove {quantity} of {self.quantity}")
self.quantity -= quantity
self.reserved = max(0, self.reserved - quantity)
if self.quantity == 0:
self.average_cost = Money(0, self.average_cost.currency)
def copy(self) -> Holding:
return Holding(self.symbol, self.quantity, self.reserved, self.average_cost)
@dataclass(slots=True)
class Portfolio:
account_id: str
holdings: dict[str, Holding] = field(default_factory=dict)
def holding(self, symbol: str) -> Holding:
return self.holdings.setdefault(symbol, Holding(symbol))
def value(self, prices: Mapping[str, Money]) -> Money:
total = Money(0)
for symbol, holding in self.holdings.items():
if symbol in prices:
total = total + prices[symbol] * holding.quantity
return total
def copy(self) -> Portfolio:
return Portfolio(self.account_id, {s: h.copy() for s, h in self.holdings.items()})
@dataclass(slots=True)
class Account:
"""Cash plus a portfolio. ``reserved_cash`` is what open buy orders have claimed."""
id: str
owner_id: str
cash: Money
portfolio: Portfolio
reserved_cash: Money = Money(0)
@classmethod
def open(cls, account_id: str, owner_id: str, cash: Money) -> Account:
return cls(account_id, owner_id, cash, Portfolio(account_id))
def available_cash(self) -> Money:
return self.cash - self.reserved_cash
def reserve_cash(self, amount: Money) -> None:
if amount > self.available_cash():
raise InsufficientFundsError(f"{self.available_cash()} available, {amount} requested")
self.reserved_cash = self.reserved_cash + amount
def release_cash(self, amount: Money) -> None:
self.reserved_cash = Money(max(0, (self.reserved_cash - amount).cents), amount.currency)
def debit(self, amount: Money) -> None:
if amount > self.cash:
raise InsufficientFundsError(f"balance {self.cash} cannot cover {amount}")
self.cash = self.cash - amount
def credit(self, amount: Money) -> None:
self.cash = self.cash + amount
def copy(self) -> Account:
return Account(self.id, self.owner_id, self.cash, self.portfolio.copy(), self.reserved_cash)
Watchlists and alerts are listeners with three lines of logic each — the whole point of Observer is that they cost this little.
@dataclass(slots=True)
class Watchlist:
"""A named set of symbols that caches the last price it heard. Observer of the feed."""
id: str
owner_id: str
symbols: set[str] = field(default_factory=set)
last_price: dict[str, Money] = field(default_factory=dict)
def add(self, symbol: str) -> None:
self.symbols.add(symbol)
def on_quote(self, quote: Quote) -> None:
if quote.symbol in self.symbols:
self.last_price[quote.symbol] = quote.price
def render(self) -> str:
return ", ".join(f"{s} {self.last_price[s]}" for s in sorted(self.symbols) if s in self.last_price)
@dataclass(slots=True)
class PriceAlert:
"""Fires once when the price crosses the threshold, then stays triggered."""
id: str
owner_id: str
symbol: str
direction: AlertDirection
threshold: Money
triggered_at: float | None = None
def matches(self, quote: Quote) -> bool:
if self.triggered_at is not None or quote.symbol != self.symbol:
return False
if self.direction is AlertDirection.ABOVE:
return quote.price >= self.threshold
return quote.price <= self.threshold
def trigger(self, at: float) -> None:
if self.triggered_at is not None:
raise ValidationError(f"alert {self.id} already fired")
self.triggered_at = at
The order hierarchy carries unit_reserve, the cash held per share. Releasing a partial fill is then a multiplication, never a division, so no cent is ever stranded.
@dataclass(slots=True, kw_only=True)
class Order:
"""An intention to trade, plus everything needed to unwind its reservation.
``unit_reserve`` is the cash reserved per share for a buy; releasing a
partial fill is then ``unit_reserve * filled_shares``, which is exact in
cents and needs no division.
"""
id: str
account_id: str
symbol: str
side: OrderSide
quantity: int
created_at: float
status: OrderStatus = OrderStatus.NEW
filled_quantity: int = 0
filled_notional: Money = Money(0)
unit_reserve: Money = Money(0)
exchange_order_id: str | None = None
order_type: ClassVar[OrderType]
def __post_init__(self) -> None:
if self.quantity <= 0:
raise ValidationError("order quantity must be positive")
def remaining(self) -> int:
return self.quantity - self.filled_quantity
def is_open(self) -> bool:
return self.status in (OrderStatus.NEW, OrderStatus.SUBMITTED, OrderStatus.PARTIALLY_FILLED)
def is_marketable(self, quote: Quote) -> bool:
raise NotImplementedError
def execution_price(self, quote: Quote) -> Money:
raise NotImplementedError
def reference_price(self, quote: Quote) -> Money:
"""The price used to size the cash reservation before anything trades."""
raise NotImplementedError
def average_price(self) -> Money:
if self.filled_quantity == 0:
return Money(0, self.filled_notional.currency)
cents = self.filled_notional.cents
return Money((cents + self.filled_quantity // 2) // self.filled_quantity, self.filled_notional.currency)
def transition_to(self, status: OrderStatus) -> None:
if status not in ORDER_TRANSITIONS[self.status]:
raise OrderStateError(f"order {self.id}: {self.status} cannot become {status}")
self.status = status
def apply_fill(self, quantity: int, price: Money) -> None:
"""Book shares against the order and move it to PARTIALLY_FILLED or FILLED."""
if quantity <= 0 or quantity > self.remaining():
raise ValidationError(f"order {self.id}: cannot fill {quantity} of {self.remaining()} remaining")
self.filled_quantity += quantity
self.filled_notional = self.filled_notional + price * quantity
self.transition_to(OrderStatus.FILLED if self.remaining() == 0 else OrderStatus.PARTIALLY_FILLED)
def copy(self) -> Order:
return type(self)(**{f.name: getattr(self, f.name) for f in fields(self)})
# 5% headroom: a market buy reserves slightly more than the last print, because
# the price can move between the reservation and the fill.
MARKET_RESERVE_BASIS_POINTS = 10_500
@dataclass(slots=True, kw_only=True)
class MarketOrder(Order):
"""Trades at whatever the feed prints next. Always marketable."""
order_type: ClassVar[OrderType] = OrderType.MARKET
def is_marketable(self, quote: Quote) -> bool:
return True
def execution_price(self, quote: Quote) -> Money:
return quote.price
def reference_price(self, quote: Quote) -> Money:
return Money(quote.price.cents * MARKET_RESERVE_BASIS_POINTS // 10_000, quote.price.currency)
@dataclass(slots=True, kw_only=True)
class LimitOrder(Order):
"""Trades only at the limit or better, so the reservation is exact."""
order_type: ClassVar[OrderType] = OrderType.LIMIT
limit_price: Money
def __post_init__(self) -> None:
# Named base call, not ``super()``: ``slots=True`` rebuilds the class object
# and the zero-argument ``super()`` cell would still point at the old one.
Order.__post_init__(self)
if self.limit_price.cents <= 0:
raise ValidationError("limit price must be positive")
def is_marketable(self, quote: Quote) -> bool:
if self.side is OrderSide.BUY:
return quote.price <= self.limit_price
return quote.price >= self.limit_price
def execution_price(self, quote: Quote) -> Money:
if self.side is OrderSide.BUY:
return min(quote.price, self.limit_price)
return max(quote.price, self.limit_price)
def reference_price(self, quote: Quote) -> Money:
return self.limit_price
class OrderFactory:
"""Factory Method: the API sends ``"limit"`` plus a price, the registry picks the class."""
@staticmethod
def create(
order_type: OrderType | str,
*,
order_id: str,
account_id: str,
symbol: str,
side: OrderSide,
quantity: int,
created_at: float,
limit_price: Money | None = None,
) -> Order:
kind = OrderType(order_type)
common = {
"id": order_id,
"account_id": account_id,
"symbol": symbol,
"side": side,
"quantity": quantity,
"created_at": created_at,
}
if kind is OrderType.MARKET:
if limit_price is not None:
raise ValidationError("a market order cannot carry a limit price")
return MarketOrder(**common)
if limit_price is None:
raise ValidationError("a limit order needs a limit price")
return LimitOrder(**common, limit_price=limit_price)
The feed fans out under a lock it does not hold while calling listeners; the venue keeps its own remaining counter because the broker replaces its Order object on every commit.
class QuoteListener(Protocol):
"""Observer interface: anything that reacts to a price tick."""
def on_quote(self, quote: Quote) -> None: ...
class MarketDataFeed:
"""Fans a tick out to every listener for that symbol, then to the wildcards.
``_lock`` guards the subscription map and the last-price cache only.
Listeners are called *outside* it: a slow alert must not stall the feed, and
a listener that publishes back into the feed must not deadlock.
"""
def __init__(self) -> None:
self._lock = threading.Lock()
self._by_symbol: dict[str, list[QuoteListener]] = {}
self._all: list[QuoteListener] = []
self._last: dict[str, Quote] = {}
def subscribe(self, symbol: str, listener: QuoteListener) -> None:
with self._lock:
self._by_symbol.setdefault(symbol, []).append(listener)
def subscribe_all(self, listener: QuoteListener) -> None:
with self._lock:
self._all.append(listener)
def publish(self, quote: Quote) -> None:
with self._lock:
self._last[quote.symbol] = quote
listeners = [*self._by_symbol.get(quote.symbol, ()), *self._all]
for listener in listeners:
listener.on_quote(quote)
def last(self, symbol: str) -> Quote:
with self._lock:
try:
return self._last[symbol]
except KeyError:
raise UnknownEntityError(f"no price yet for {symbol}") from None
class ExchangeGateway(Protocol):
"""The venue, as the broker sees it. Everything below this line is someone else's system."""
def submit(self, order: Order) -> str: ...
def cancel(self, exchange_order_id: str) -> bool: ...
@dataclass(slots=True)
class RestingOrder:
"""The venue's own copy of an order. It owns the remaining quantity, not the broker."""
exchange_order_id: str
order: Order
remaining: int
class SimulatedExchange:
"""A stand-in venue: it rests orders and fills them when a tick makes them marketable.
The venue keeps its own ``remaining`` counter -- the broker's ``Order`` object
is replaced on every commit, so reading its state from here would be a bug.
``max_fill_quantity`` caps how many shares one tick can execute, which is how
the tests and the demo reproduce partial fills deterministically. Fills are
pushed to ``on_fill``: the broker's inbound execution report, in effect.
"""
def __init__(
self,
on_fill: Callable[[Fill], None] | None = None,
clock: Clock | None = None,
ids: IdGenerator | None = None,
max_fill_quantity: int | None = None,
) -> None:
self._on_fill = on_fill
self._clock = clock or SystemClock()
self._ids = ids or SequentialIdGenerator("F")
self._max_fill = max_fill_quantity
self._lock = threading.Lock()
self._book: dict[str, RestingOrder] = {}
def connect(self, on_fill: Callable[[Fill], None]) -> None:
"""Wire the broker's execution-report handler once both objects exist."""
self._on_fill = on_fill
def submit(self, order: Order) -> str:
exchange_order_id = self._ids.next_id()
with self._lock:
self._book[exchange_order_id] = RestingOrder(exchange_order_id, order, order.remaining())
return exchange_order_id
def cancel(self, exchange_order_id: str) -> bool:
with self._lock:
return self._book.pop(exchange_order_id, None) is not None
def on_quote(self, quote: Quote) -> None:
"""Fill one slice of every resting order this tick makes marketable, in submission order."""
with self._lock:
exchange_order_ids = list(self._book)
for exchange_order_id in exchange_order_ids:
fill = self._slice(exchange_order_id, quote)
if fill is not None and self._on_fill is not None:
self._on_fill(fill) # outside the lock: the broker calls back into us
def _slice(self, exchange_order_id: str, quote: Quote) -> Fill | None:
with self._lock:
resting = self._book.get(exchange_order_id)
if resting is None or resting.order.symbol != quote.symbol:
return None
if not resting.order.is_marketable(quote) or resting.remaining <= 0:
return None
quantity = resting.remaining if self._max_fill is None else min(resting.remaining, self._max_fill)
resting.remaining -= quantity
if resting.remaining == 0:
self._book.pop(exchange_order_id, None)
price = resting.order.execution_price(quote)
return Fill(self._ids.next_id(), resting.order.id, quantity, price, self._clock.now())
def resting(self) -> Iterable[str]:
with self._lock:
return list(self._book)
The store hands out snapshots and takes back whole states; the Unit of Work is the five-things-at-once boundary.
class UnitOfWork(Protocol):
"""One transaction boundary over one account. ``state`` is the working copy."""
state: AccountState
def commit(self) -> None: ...
def rollback(self) -> None: ...
class AccountUnitOfWork:
"""Cash, reservation, holding, order status and the trade row commit together.
A settled fill touches five things. If the process died between the debit
and the holding update, the account would have paid for shares it does not
own -- so all five happen on a private copy that ``commit`` publishes in one
assignment, and any exception leaves the stored state untouched.
"""
def __init__(self, store: BrokerageStore, account_id: str) -> None:
self._store = store
self._account_id = account_id
self._committed = False
self.state = store.snapshot(account_id)
def __enter__(self) -> Self:
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
if not self._committed:
self.rollback()
def commit(self) -> None:
self._store.publish(self.state)
self._committed = True
def rollback(self) -> None:
self.state = self._store.snapshot(self._account_id)
place_order and on_fill are the two methods to write on the whiteboard. Read the docstrings: the lock is released before the venue call, and the fill id short-circuits a redelivery before anything moves.
class OrderService:
"""Place, cancel and settle. Every method that moves money takes the account lock."""
def __init__(
self,
store: BrokerageStore,
feed: MarketDataFeed,
exchange: ExchangeGateway,
clock: Clock | None = None,
ids: IdGenerator | None = None,
listeners: Iterable[TradeListener] = (),
) -> None:
self._store = store
self._feed = feed
self._exchange = exchange
self._clock = clock or SystemClock()
self._ids = ids or SequentialIdGenerator("O")
self._listeners = list(listeners)
def place_order(
self,
account_id: str,
symbol: str,
side: OrderSide,
quantity: int,
order_type: OrderType | str = OrderType.MARKET,
limit_price: Money | None = None,
) -> Order:
"""Reserve funds or shares, persist, then hand the order to the venue.
The exchange call happens *after* the transaction commits and *outside*
the account lock: never hold a lock across a network call. The cost is
that a fill can arrive before the acknowledgement lands, which
``_acknowledge`` handles by leaving an already-progressed order alone.
"""
self._store.stock(symbol)
quote = self._feed.last(symbol)
with self._store.account_lock(account_id):
with AccountUnitOfWork(self._store, account_id) as uow:
order = OrderFactory.create(
order_type,
order_id=self._ids.next_id(),
account_id=account_id,
symbol=symbol,
side=side,
quantity=quantity,
created_at=self._clock.now(),
limit_price=limit_price,
)
self._reserve(uow.state, order, quote_price=order.reference_price(quote))
uow.state.orders[order.id] = order
uow.commit()
self._store.index_order(order.id, account_id)
try:
exchange_order_id = self._exchange.submit(order)
except Exception:
self._finish(account_id, order.id, OrderStatus.REJECTED)
raise
return self._acknowledge(account_id, order.id, exchange_order_id)
def cancel_order(self, account_id: str, order_id: str) -> Order:
"""Release whatever is still reserved, then pull the order off the book."""
order = self._finish(account_id, order_id, OrderStatus.CANCELLED)
if order.exchange_order_id is not None:
self._exchange.cancel(order.exchange_order_id)
return order
def on_fill(self, fill: Fill) -> Trade:
"""Settle one execution report. Idempotent: a repeated fill id returns the first trade."""
account_id = self._store.account_for_order(fill.order_id)
with self._store.account_lock(account_id):
with AccountUnitOfWork(self._store, account_id) as uow:
seen = uow.state.trades_by_fill.get(fill.fill_id)
if seen is not None:
return seen # duplicate delivery: no second debit, no second trade
order = uow.state.orders[fill.order_id]
if order.status in TERMINAL_STATUSES:
raise OrderStateError(f"order {order.id} is {order.status}; fill {fill.fill_id} arrived too late")
trade = self._settle(uow.state, order, fill)
uow.state.trades.append(trade)
uow.state.trades_by_fill[fill.fill_id] = trade
uow.commit()
for listener in self._listeners:
listener.on_trade(trade) # outside the lock
return trade
# -- queries ---------------------------------------------------------------------
def account(self, account_id: str) -> Account:
return self._store.snapshot(account_id).account
def order(self, account_id: str, order_id: str) -> Order:
try:
return self._store.snapshot(account_id).orders[order_id]
except KeyError:
raise UnknownEntityError(f"unknown order {order_id}") from None
def trades(self, account_id: str) -> list[Trade]:
return self._store.snapshot(account_id).trades
def open_orders(self, account_id: str) -> list[Order]:
return [o for o in self._store.snapshot(account_id).orders.values() if o.is_open()]
# -- internals -------------------------------------------------------------------
@staticmethod
def _reserve(state: AccountState, order: Order, quote_price: Money) -> None:
"""Buy: hold cash at the reference price. Sell: hold the shares themselves."""
if order.side is OrderSide.BUY:
order.unit_reserve = quote_price
state.account.reserve_cash(quote_price * order.quantity)
else:
state.account.portfolio.holding(order.symbol).reserve(order.quantity)
def _settle(self, state: AccountState, order: Order, fill: Fill) -> Trade:
notional = fill.price * fill.quantity
holding = state.account.portfolio.holding(order.symbol)
if order.side is OrderSide.BUY:
state.account.release_cash(order.unit_reserve * fill.quantity)
state.account.debit(notional)
holding.add(fill.quantity, fill.price)
else:
holding.remove(fill.quantity)
state.account.credit(notional)
order.apply_fill(fill.quantity, fill.price)
if order.status is OrderStatus.FILLED:
self._release_remainder(state, order)
return Trade(
id=self._ids.next_id(),
order_id=order.id,
account_id=order.account_id,
symbol=order.symbol,
side=order.side,
quantity=fill.quantity,
price=fill.price,
notional=notional,
at=fill.at,
)
@staticmethod
def _release_remainder(state: AccountState, order: Order) -> None:
"""Give back what the unfilled part of the order was still holding."""
if order.side is OrderSide.BUY:
state.account.release_cash(order.unit_reserve * order.remaining())
else:
state.account.portfolio.holding(order.symbol).release(order.remaining())
def _acknowledge(self, account_id: str, order_id: str, exchange_order_id: str) -> Order:
with self._store.account_lock(account_id):
with AccountUnitOfWork(self._store, account_id) as uow:
order = uow.state.orders[order_id]
order.exchange_order_id = exchange_order_id
if order.status is OrderStatus.NEW:
order.transition_to(OrderStatus.SUBMITTED) # a fill may already have moved it on
uow.commit()
return order
def _finish(self, account_id: str, order_id: str, status: OrderStatus) -> Order:
with self._store.account_lock(account_id):
with AccountUnitOfWork(self._store, account_id) as uow:
order = uow.state.orders.get(order_id)
if order is None:
raise UnknownEntityError(f"unknown order {order_id}")
if not order.is_open():
raise OrderStateError(f"order {order_id} is already {order.status}")
self._release_remainder(uow.state, order)
order.transition_to(status)
uow.commit()
return order
The demo runs a full session: a market buy, a limit buy that fills in two slices, a redelivered report, a sell that trips an alert, and a cancellation.
"""One session: a market buy, a limit buy that fills in two slices, a duplicate report, a sell."""
from common import FakeClock, Money, SequentialIdGenerator
from lld.stock_brokerage.market import AlertService, MarketDataFeed, SimulatedExchange
from lld.stock_brokerage.models import (
Account,
AlertDirection,
Fill,
OrderSide,
OrderType,
PriceAlert,
Quote,
Stock,
Watchlist,
)
from lld.stock_brokerage.services import OrderService
from lld.stock_brokerage.store import BrokerageStore
def tick(feed: MarketDataFeed, clock: FakeClock, symbol: str, price: str) -> None:
clock.advance(60)
feed.publish(Quote(symbol, Money.of(price), clock.now()))
def main() -> None:
clock = FakeClock(start=1_700_000_000)
store = BrokerageStore()
store.list_stock(Stock("AAPL", "Apple"))
store.list_stock(Stock("MSFT", "Microsoft"))
store.open_account(Account.open("acc-1", "ada", Money.of("50000.00")))
feed = MarketDataFeed()
exchange = SimulatedExchange(clock=clock, ids=SequentialIdGenerator("F"), max_fill_quantity=20)
service = OrderService(store, feed, exchange, clock=clock, ids=SequentialIdGenerator("O"))
fills: list[Fill] = []
def settle(fill: Fill) -> None:
fills.append(fill)
service.on_fill(fill)
exchange.connect(settle)
alerts = AlertService(clock=clock)
alerts.register(PriceAlert("al-1", "ada", "AAPL", AlertDirection.ABOVE, Money.of("190.00")))
watchlist = Watchlist("wl-1", "ada", {"AAPL", "MSFT"})
for listener in (exchange, alerts, watchlist):
feed.subscribe_all(listener)
tick(feed, clock, "AAPL", "185.00")
tick(feed, clock, "MSFT", "400.00")
buy = service.place_order("acc-1", "AAPL", OrderSide.BUY, 20, OrderType.MARKET)
print(f"{buy.id} market BUY 20 AAPL -> {buy.status}, reserved {service.account('acc-1').reserved_cash}")
tick(feed, clock, "AAPL", "186.50")
account = service.account("acc-1")
print(f"filled at {service.order('acc-1', buy.id).average_price()}, cash {account.cash}, reserved {account.reserved_cash}")
limit = service.place_order("acc-1", "MSFT", OrderSide.BUY, 30, OrderType.LIMIT, Money.of("395.00"))
print(f"{limit.id} limit BUY 30 MSFT at 395.00 -> {limit.status}, reserved {service.account('acc-1').reserved_cash}")
tick(feed, clock, "MSFT", "398.00")
print(f"tick 398.00: still {service.order('acc-1', limit.id).status}, 0 shares")
tick(feed, clock, "MSFT", "394.00")
partial = service.order("acc-1", limit.id)
print(f"tick 394.00: {partial.status} {partial.filled_quantity}/{partial.quantity}, cash {service.account('acc-1').cash}")
service.on_fill(fills[-1])
print(f"duplicate report {fills[-1].fill_id} replayed: cash still {service.account('acc-1').cash}")
tick(feed, clock, "MSFT", "393.00")
done = service.order("acc-1", limit.id)
print(f"tick 393.00: {done.status} at average {done.average_price()}, cash {service.account('acc-1').cash}")
sell = service.place_order("acc-1", "AAPL", OrderSide.SELL, 10, OrderType.LIMIT, Money.of("190.00"))
tick(feed, clock, "AAPL", "191.00")
print(f"{sell.id} limit SELL 10 AAPL at 190.00 -> {service.order('acc-1', sell.id).status} at 191.00")
print(f"alerts fired: {[a.id for a in alerts.triggered()]}")
stale = service.place_order("acc-1", "AAPL", OrderSide.BUY, 5, OrderType.LIMIT, Money.of("150.00"))
print(f"{stale.id} rests off-market, reserving {service.account('acc-1').reserved_cash}")
service.cancel_order("acc-1", stale.id)
account = service.account("acc-1")
print(f"cancelled: reserved back to {account.reserved_cash}, cash {account.cash}")
prices = {"AAPL": Money.of("191.00"), "MSFT": Money.of("393.00")}
print(f"portfolio {account.portfolio.value(prices)} across {len(service.trades('acc-1'))} trades")
print(f"watchlist: {watchlist.render()}")
if __name__ == "__main__":
main()
Running python -m lld.stock_brokerage.demo prints:
O-1 market BUY 20 AAPL -> submitted, reserved 3885.00 USD
filled at 186.50 USD, cash 46270.00 USD, reserved 0.00 USD
O-3 limit BUY 30 MSFT at 395.00 -> submitted, reserved 11850.00 USD
tick 398.00: still submitted, 0 shares
tick 394.00: partially_filled 20/30, cash 38390.00 USD
duplicate report F-4 replayed: cash still 38390.00 USD
tick 393.00: filled at average 393.67 USD, cash 34460.00 USD
O-6 limit SELL 10 AAPL at 190.00 -> filled at 191.00
alerts fired: ['al-1']
O-8 rests off-market, reserving 750.00 USD
cancelled: reserved back to 0.00 USD, cash 36370.00 USD
portfolio 13700.00 USD across 4 trades
watchlist: AAPL 191.00 USD, MSFT 393.00 USD
Trace the reservation: 20 shares at the last print of 185.00 plus 5% is 194.25 x 20 = 3885.00; the fill at 186.50 costs 3730.00, and the difference goes straight back to available cash.
Concurrency and edge cases¶
Which lock protects what.
BrokerageStore.account_lock(account_id)guards cash, reservations, holdings, order status and the trade index of one account. Two accounts trading the same symbol never contend, which is the granularity you want: contention follows users, not symbols.BrokerageStore._registry_lockguards the account, symbol and order-to-account dictionaries, and is held for a lookup only.MarketDataFeed._lockguards the subscription map, andSimulatedExchange._lockguards the book. Both are released before any callback fires.
Lock ordering. The only nesting is account lock then registry lock, always in that direction, and nothing else nests. That is why there is no deadlock to reason about: locks are leaves or a single two-level chain.
Why the venue call is outside the lock. An uncontended mutex costs about 17 ns and a same-datacenter round trip about 500 µs (see the latency cheatsheet); the network call is roughly 500 000 / 17, near 30 000 times more expensive. Holding an account lock across it would serialise a user's entire session behind one slow venue. The price you pay is that a fill can arrive before the acknowledgement commits, so _acknowledge only moves NEW -> SUBMITTED if the order is still NEW.
Idempotent settlement. AccountState.trades_by_fill maps fill id to the trade it produced. on_fill checks it first, inside the transaction, and returns the stored trade on a repeat. That is effectively-once delivery in three lines: at-least-once transport plus an idempotent consumer.
Partial fills. Every slice releases unit_reserve x slice_quantity and debits the actual traded notional. When the order reaches a terminal state, _release_remainder gives back what the unfilled part was still holding. The arithmetic is exact because unit_reserve is a per-share Money and quantities are ints.
Common mistake
Checking the balance and then debiting it at fill time, with nothing in between. Two orders both pass the check, both get sent, and both come back filled — and now the account is overdrawn by a real amount of real money. Reserve at placement, release on settlement or cancellation, and validate against available_cash(), never cash.
Other edge cases handled: a market order carrying a limit price (rejected by the factory); a sell of shares an open sell order has already claimed; a fill larger than the remaining quantity; cancelling a filled or already cancelled order; a fill arriving after a cancel (refused, with a note that the venue is really the authority); an alert that would fire on every tick above its threshold (it fires once); an order for an unlisted symbol or one with no price yet.
Extensibility and follow-ups¶
- Stop and stop-limit orders: a third
Ordersubclass whoseis_marketablecompares against a trigger, plus one branch inOrderFactory. Nothing inOrderServicechanges — name that seam when asked. - Time in force (day, good-till-cancelled, immediate-or-cancel): a policy object consulted by the venue adapter at the end of each session; IOC becomes "cancel the remainder after the first slice".
- A real venue: implement
ExchangeGatewayover FIX or a REST API and turnon_fillinto a webhook handler. The idempotency key is already the venue's execution id, so redelivery is free. - Margin:
available_cash()becomescash + buying_power - reserved, computed by aMarginPolicy. The reservation machinery does not change, which is the point of having it in one method. - The matching engine: when the interviewer asks "now build the exchange", you are into Design a stock exchange — price-time priority, sequenced order books, deterministic replay.
- Audit and compliance: trades already carry account, order, price and time. Add an append-only event stream fed by
TradeListener, and reconstruct any account by replay.
Interview tip
Say "reserve, submit, settle, release" out loud before you write any code, and draw the four boxes. Interviewers grade this problem mostly on whether reservations exist at all; candidates who only debit on fill lose the round in the first ten minutes regardless of how good the class diagram is.
Tests¶
tests/test_stock_brokerage.py has 13 cases. The three to walk an interviewer through are partial fills, the duplicate execution report, and the concurrency test that proves reservations are atomic:
def test_partial_fills_accumulate_and_cancelling_releases_the_remainder(clock: FakeClock) -> None:
service, feed, _, _ = build(clock, max_fill=4)
quote(feed, clock, "100.00")
order = service.place_order("acc-1", "AAPL", OrderSide.BUY, 10, OrderType.LIMIT, Money.of("100.00"))
assert service.account("acc-1").reserved_cash == Money.of("1000.00") # 10 x the limit
quote(feed, clock, "99.00") # one slice of 4 shares
resting = service.order("acc-1", order.id)
assert resting.status is OrderStatus.PARTIALLY_FILLED and resting.filled_quantity == 4
assert service.account("acc-1").reserved_cash == Money.of("600.00") # 6 shares still held
cancelled = service.cancel_order("acc-1", order.id)
assert cancelled.status is OrderStatus.CANCELLED
account = service.account("acc-1")
assert account.reserved_cash == Money(0)
assert account.cash == Money.of("9604.00") # 10000 - 4 x 99.00
assert account.portfolio.holding("AAPL").quantity == 4
def test_a_duplicate_execution_report_settles_exactly_once(clock: FakeClock) -> None:
service, feed, exchange, _ = build(clock)
seen: list[Fill] = []
def record_and_settle(fill: Fill) -> None:
seen.append(fill)
service.on_fill(fill)
exchange.connect(record_and_settle)
quote(feed, clock, "100.00")
service.place_order("acc-1", "AAPL", OrderSide.BUY, 10, OrderType.MARKET)
quote(feed, clock, "100.00")
first = service.trades("acc-1")[0]
cash_after_first = service.account("acc-1").cash
replayed = service.on_fill(seen[-1]) # the venue redelivers the same report
assert replayed is first # the stored trade comes back, nothing new is booked
assert service.account("acc-1").cash == cash_after_first
assert len(service.trades("acc-1")) == 1
assert service.account("acc-1").portfolio.holding("AAPL").quantity == 10
def test_concurrent_orders_never_reserve_more_cash_than_the_account_holds(clock: FakeClock) -> None:
service, feed, _, _ = build(clock, cash="5000.00")
quote(feed, clock, "100.00") # limit far below market, so nothing fills
def place(i: int) -> bool:
try:
service.place_order("acc-1", "AAPL", OrderSide.BUY, 10, OrderType.LIMIT, Money.of("100.00"))
except InsufficientFundsError:
return False
return True
with ThreadPoolExecutor(max_workers=10) as pool:
accepted = list(pool.map(place, range(20)))
account = service.account("acc-1")
assert accepted.count(True) == 5 # 5 x 10 shares x 100.00 = the whole balance
assert account.reserved_cash == Money.of("5000.00")
assert account.available_cash() == Money(0)
assert len(service.open_orders("acc-1")) == 5
The rest cover: a market buy reserving with headroom and settling at the traded price; orders beyond the available balance or holding never reaching the venue; a limit order resting through a non-crossing tick and then filling at the better price; the transition table refusing four illegal moves via parametrize; a fill delivered after a cancel; alerts firing once while the watchlist keeps following the feed; and PlaceOrderCommand undoing itself. Run them with uv run pytest code/lld/stock_brokerage -q.
45-minute pacing¶
| Minutes | What to do | What to say or write |
|---|---|---|
| 0-5 | Clarify | Broker or exchange? Order types? Partial fills? Duplicate execution reports? Out of scope: matching engine, margin, options. |
| 5-10 | Entities | Account, Portfolio, Holding, Order, Fill, Trade, Quote. Say "available cash equals cash minus reserved" while drawing the account. |
| 10-16 | Lifecycle | Draw the six-state diagram and the transition table before any other code. It anchors the rest of the discussion. |
| 16-32 | Code | Account.reserve_cash, the Order subclasses, place_order (reserve, commit, submit, acknowledge), then on_fill with the fill-id check. |
| 32-39 | Concurrency | The per-account lock, why the venue call sits outside it, and the 20-orders-five-succeed test. |
| 39-45 | Extensions | Stop orders as a subclass, time in force, a real gateway, and the hand-off to the exchange design. |
Related¶
- Observer — the price feed driving limit orders and alerts
- State — the order lifecycle, and when a table beats classes
- Command — placement and cancellation as request objects
- Unit of Work — settling five things in one commit
- Design a stock exchange — the venue on the other side of the gateway
- Concurrency for LLD in Python — lock granularity and never locking across I/O