"Design a stock brokerage system. Users should be able to create an account, deposit and withdraw cash, place buy and sell orders for stocks, and view their portfolio and order history. Assume stock prices come from an external market data feed."
Note what is not specified: whether only market orders are supported or limit orders too, whether an unfilled order sits pending or is rejected outright, and whether execution means "trade against the live feed price" or "match against other users' orders" (a full exchange). A strong candidate surfaces these before writing a single class — the last one in particular determines whether this is an LLD problem at all, or secretly an HLD/distributed-systems one.
B. Difficulty
Advanced, escalating to Advanced + Concurrency/Consistency once the interviewer asks about two orders racing on the same account's cash or holdings (an almost-guaranteed follow-up). It sits a notch above Parking-Lot-style problems because the domain has hard financial invariants — a balance must never go negative, a sell must never exceed held shares — that have to survive concurrent access, not just single-threaded correctness.
C. What This Problem Tests
Modeling money and quantities correctly (no floating point for currency)
State-machine thinking for order lifecycle (PENDING → EXECUTED / REJECTED / CANCELLED)
Strategy pattern for execution logic (market vs. limit) — only once justified by a requirement
Observer pattern for reacting to price changes instead of polling
Enforcing invariants (funds/holdings) at the right layer, not scattered across callers
Concurrency reasoning once multiple orders can race on the same account
Recognizing the LLD/HLD boundary — this is a brokerage placing orders against a price feed, not a matching engine for a stock exchange
D. Requirements
Functional
Create an account with a cash balance
Deposit and withdraw cash from an account
Place a buy or sell order for a stock, as a market or limit order
Validate an order against available funds (buy) or available holdings (sell) before accepting it
Execute an order against the current market price — immediately for market orders, or once the price condition is met for limit orders
Maintain a portfolio of holdings per account
Maintain an order/transaction history per account
Allow cancelling an order that hasn't executed yet
Look up the current price of a stock via a market data feed
Non-Functional
Extensibility — new order types (stop-loss, stop-limit) should not require touching existing order classes
Correctness under concurrency — cash can never be double-spent and shares can never be double-sold, even with concurrent orders on the same account
Interview-scale (a handful of accounts and stocks, not a full exchange's order volume)
Out of scope by default: matching orders against other users' orders (a real order book/exchange), multi-currency, tax reporting, real payment-rail integration — unless the interviewer asks
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
Are both market and limit orders in scope, or market only?
Determines whether execution logic needs to be pluggable at all
ExecutionStrategy abstraction vs. one inline calculation
2
If a limit order's price isn't currently met, does it sit pending or get rejected outright?
Pending orders need a lifecycle and a way to be re-evaluated later
Order status machine, feed subscription
3
Do orders execute against a live price feed, or get matched against other users' orders?
The former is a brokerage (LLD-scale); the latter is a real exchange matching engine (HLD/distributed)
Whether an order book/matching engine is even built
4
Should funds/holdings be reserved at order-placement time, or only checked at execution time?
Without a reservation, two pending orders could both "pass" a check against the same unspent balance
Hold-then-settle model on Account/Portfolio
5
Can a user place multiple concurrent orders, including on the same stock?
The single biggest driver of the concurrency design
Locking granularity, atomicity of reserve/settle
6
Is a single currency assumed?
Avoids an unrequested currency-conversion layer
Money modeling simplicity
7
Can a placed order be cancelled?
Adds a CANCELLED terminal state and a release-reservation path
Order lifecycle, cancelOrder()
8
Are fractional shares allowed?
Changes Quantity from an integer to a decimal type
Order/Portfolio quantity type
9
Are price updates pushed to the system, or does it need to poll?
Push enables an efficient Observer-based re-check of pending limit orders; poll wastes cycles and adds latency
MarketDataFeed interface shape
10
Must account/order state persist across restarts?
Affects ID durability and crash-recovery of in-flight reservations
Persistence section relevance
11
Do we need commission/fees per trade?
If yes, pricing needs a second pluggable piece separate from execution
Optional CommissionStrategy (follow-up, not baked in)
Assumption: execution happens against a pluggable market-data feed's price — this is a brokerage routing orders, not a peer-to-peer order book. Real cross-user matching is called out explicitly as an HLD boundary (see Y), mirroring how a single parking lot's design doesn't try to solve city-wide allocation.
Assumption: market orders execute immediately at the current feed price; limit orders remain PENDING until a feed price update crosses the limit, re-checked via a subscription callback (Observer), never by polling.
Assumption: funds (for buys) and holdings (for sells) are reserved at order-placement time and released on cancel/rejection — this is what prevents two concurrently pending orders from both passing a check against the same unspent balance.
Assumption: single currency, whole-share (integer) quantities in the base design; fractional shares are a stated extension, not built preemptively.
Assumption: in-memory state; persistence discussed as an extension (section X).
Assumption: no Singleton for Broker — same testability reasoning used throughout this handbook (see M).
G. Actors
Actor
Responsibility
Investor / User
Owns an account; deposits/withdraws cash; places and cancels orders
Market Data Provider
External source of current stock prices — feeds the system, isn't a human actor in the flow
System
Validates orders, reserves funds/holdings, executes against the feed price, records history — no human decision in the core flow
H. Core Domain Entities
Class
Responsibility
Key Attributes
Key Methods
Does NOT Handle
Account
Holds cash and a portfolio for one investor
id, cashBalance, reservedCash
reserve(), release(), deposit(), withdraw()
Order validation rules, pricing
Portfolio
Tracks per-stock holdings for one account
holdings map, reserved-quantity map
reserveShares(), releaseShares(), settle()
Pricing, order lifecycle
Stock
Represents a tradable instrument and its last known price
symbol, currentPrice
updatePrice()
Deciding who trades it
Order (abstract)
Represents one order's lifecycle
id, quantity, status, timestamps
cancel()
Fund/holding math, execution decision
BuyOrder/SellOrder
Concrete order — fixes direction and which side (funds vs. holdings) gets reserved/settled
—
—
Any behavior beyond direction
ExecutionStrategy (interface)
Decides whether an order executes at a given price
—
tryExecute(Order, Price)
Reserving/settling funds itself
MarketDataFeed (interface)
Supplies current prices and notifies subscribers on change
Deliberately not created (yet): a peer order-matching engine/order book, a PaymentGateway for real money movement, a multi-currency FxConverter, a ComplianceEngine. The reference GitHub write-up this problem is commonly modeled after (ashishps1/awesome-low-level-design) keeps scope similarly narrow, but wires its central class as a Singleton — this handbook deliberately avoids that (see M) for the same reason it avoids Singleton everywhere else: it isn't a class-design requirement, it's an application-wiring decision, and baking it in makes the class harder to test.
I. Class Relationships
Composition:Broker owns Accounts; Account owns its Portfolio. An account's holdings don't make sense detached from the account — composition, not aggregation.
Inheritance:Order → BuyOrder/SellOrder. Kept shallow — the two subclasses diverge only on direction (which side gets reserved, which side gets settled), not on unrelated behavior, so the split stays justified rather than becoming ceremony.
Dependency:Broker → ExecutionStrategy and Broker → MarketDataFeed — both injected interfaces, so the execution rule and the price source can each be swapped (e.g. a fake feed in tests) without touching Broker.
Association:Order → Account, Order → Stock — an order references both but owns the lifecycle of neither.
Observer:MarketDataFeed notifies subscribed pending orders on a price change, rather than the system polling the feed on a timer — the same "push, don't poll" reasoning used for the Pub-Sub problem elsewhere in this handbook.
Why composition over a standalone Account service? An account's cash and holdings are two views of one financial position that must stay consistent with each other during a trade — splitting them into unrelated services would make the reserve/settle step span a network call for no benefit at this scale.
J. Class Diagram
View Mermaid source
classDiagram
class Broker {
+placeOrder(Account, Stock, Side, Quantity, OrderType, LimitPrice)
+cancelOrder(OrderId)
+deposit(Account, Money)
+withdraw(Account, Money)
}
class Account {
id
cashBalance
reservedCash
+reserve(Money)
+release(Money)
+deposit(Money)
+withdraw(Money)
}
class Portfolio {
+reserveShares(Stock, Quantity)
+releaseShares(Stock, Quantity)
+settle(Stock, Quantity, Side)
}
class ExecutionStrategy {
<<interface>>
+tryExecute(Order, Price)
}
class MarketDataFeed {
<<interface>>
+getPrice(Symbol)
+subscribe(Symbol, PriceListener)
}
class Order {
<<abstract>>
id
quantity
status
+cancel()
}
class Stock {
symbol
currentPrice
}
Broker *-- Account
Account *-- Portfolio
Broker ..> ExecutionStrategy
Broker ..> MarketDataFeed
ExecutionStrategy <|.. MarketExecutionStrategy
ExecutionStrategy <|.. LimitExecutionStrategy
Order <|-- BuyOrder
Order <|-- SellOrder
Order --> Account
Order --> Stock
K. Responsibility Mapping
Class
Responsibility
Does NOT Handle
Broker
Coordinates order placement/cancellation, owns accounts
If the system is trivially small — a facade adds a layer of indirection
State (light)
Order lifecycle: PENDING → EXECUTED / REJECTED / CANCELLED
Order.status
Plain enum + guarded transitions (fine at this scale)
Don't build a full State-pattern class hierarchy for 4 states — an enum with guard checks is enough until per-state behavior gets complex
Explicitly avoided: Singleton for Broker. The reference implementation this problem is commonly based on defaults to a Singleton StockBroker "because there's only one broker in the process" — but that's an application-wiring decision (construct one instance, inject it everywhere), not a class-design constraint. Singleton would make it impossible to spin up two independent brokers side-by-side in a unit test, for no real production benefit.
N. What Is Likely to Change?
Behavior
Current Design
Anticipated Change
Protecting Abstraction
Execution rule
Market (immediate) or limit (price-crossing)
Stop-loss, stop-limit, trailing-stop orders
ExecutionStrategy interface
Price source
Single MarketDataFeed
Best-price-across-multiple-exchanges aggregation
MarketDataFeed interface
Cost model
No commission
Per-trade commission, tiered fee schedules
Future CommissionStrategy interface (not built prematurely)
Account structure
One portfolio per account
Multiple sub-accounts per user (e.g. individual + retirement)
User *-- Account[1..*] composition (not built prematurely)
Notification
Not in scope
Alert user on order fill via push/email
Future Observer/event bus on Order status changes
O. Design Evolution
V1 — Single account, single stock, market orders only:Broker executes directly against the feed price with no reservation step.
V2 — Multiple stocks + real validation: introduce Portfolio; buy/sell must be validated against funds/holdings before accepting.
V3 — Limit orders introduced: extract ExecutionStrategy so Broker doesn't special-case order type with if/else.
V4 — Orders can stay pending: extract MarketDataFeed as an observable source; pending limit orders subscribe instead of being polled.
V5 — Preventing double-spend across pending orders: introduce the reserve-then-settle model on Account/Portfolio instead of a plain balance check.
V6 — Concurrent orders on the same account: reserve/settle become atomic operations (lock or CAS per account); Broker's public API signature doesn't change — only the internals of reservation gain synchronization.
What stays stable across all versions: the Broker.placeOrder()/cancelOrder() facade signature. That stability is the payoff of extracting ExecutionStrategy and MarketDataFeed early — callers never notice the internal evolution.
Ownership/validation notes:placeOrder owns the reserve-then-attempt-execute atomicity — a caller never sees a half-reserved order. cancelOrder must validate the order is still PENDING before cancelling, and must release any funds/shares it reserved — this is the boundary where "cancel after it already executed" bugs occur if skipped.
Q. Pseudocode — Critical Logic
placeOrder(account, stock, side, qty, type, limitPrice):
if side == BUY:
estimatedCost = qty * (type == MARKET ? feed.getPrice(stock) : limitPrice)
account.reserve(estimatedCost) // throws InsufficientFundsException
else:
account.portfolio.reserveShares(stock, qty) // throws InsufficientHoldingsException
order = Order.create(account, stock, side, qty, type, limitPrice)
strategy = strategyFor(type, limitPrice)
price = feed.getPrice(stock)
result = strategy.tryExecute(order, price) // must be atomic w.r.t. account state — see W
if result is present:
settle(order, result)
else:
order.status = PENDING
feed.subscribe(stock, order) // re-checked on future price ticks
return order
onPriceUpdate(stock, newPrice): // Observer callback, not polling
for order in pendingOrdersFor(stock):
result = order.strategy.tryExecute(order, newPrice)
if result is present: settle(order, result)
settle(order, executionPrice):
if order.side == BUY:
account.release(order.reservedEstimate)
account.withdraw(executionPrice * order.quantity)
portfolio.addHolding(order.stock, order.quantity)
else:
portfolio.settle(order.stock, order.quantity, SELL) // removes reserved shares
account.deposit(executionPrice * order.quantity)
order.status = EXECUTED
cancelOrder(orderId):
order = orderRepository.find(orderId)
if order.status != PENDING: raise InvalidOrderStateException
releaseReservation(order) // cash or shares, by side
order.status = CANCELLED
Order placeOrder(Account account, Stock stock, Side side, int qty, OrderType type, Money limitPrice)
throws InsufficientFundsException, InsufficientHoldingsException {
if (side == Side.BUY) {
Money estimatedCost = (type == OrderType.MARKET ? feed.getPrice(stock.getSymbol()) : limitPrice).times(qty);
account.reserve(estimatedCost); // throws InsufficientFundsException
} else {
account.getPortfolio().reserveShares(stock, qty); // throws InsufficientHoldingsException
}
Order order = Order.create(account, stock, side, qty, type, limitPrice);
ExecutionStrategy strategy = strategyFor(type, limitPrice);
Money price = feed.getPrice(stock.getSymbol());
Optional<Money> result = strategy.tryExecute(order, price); // must be atomic w.r.t. account state — see W
if (result.isPresent()) {
settle(order, result.get());
} else {
order.setStatus(OrderStatus.PENDING);
feed.subscribe(stock.getSymbol(), order); // re-checked on future price ticks
}
return order;
}
void onPriceUpdate(Symbol symbol, Money newPrice) { // Observer callback, not polling
for (Order order : pendingOrdersFor(symbol)) {
Optional<Money> result = order.getStrategy().tryExecute(order, newPrice);
result.ifPresent(price -> settle(order, price));
}
}
void settle(Order order, Money executionPrice) {
if (order.getSide() == Side.BUY) {
account.release(order.getReservedEstimate());
account.withdraw(executionPrice.times(order.getQuantity()));
portfolio.addHolding(order.getStock(), order.getQuantity());
} else {
portfolio.settle(order.getStock(), order.getQuantity(), Side.SELL); // removes reserved shares
account.deposit(executionPrice.times(order.getQuantity()));
}
order.setStatus(OrderStatus.EXECUTED);
}
void cancelOrder(OrderId orderId) throws InvalidOrderStateException {
Order order = orderRepository.find(orderId);
if (order.getStatus() != OrderStatus.PENDING) throw new InvalidOrderStateException();
releaseReservation(order); // cash or shares, by side
order.setStatus(OrderStatus.CANCELLED);
}
std::shared_ptr<Order> placeOrder(std::shared_ptr<Account> account, std::shared_ptr<Stock> stock,
Side side, int qty, OrderType type, Money limitPrice) {
if (side == Side::BUY) {
Money estimatedCost = (type == OrderType::MARKET ? feed.getPrice(stock->symbol) : limitPrice) * qty;
account->reserve(estimatedCost); // throws InsufficientFundsException
} else {
account->portfolio.reserveShares(stock, qty); // throws InsufficientHoldingsException
}
auto order = Order::create(account, stock, side, qty, type, limitPrice);
auto strategy = strategyFor(type, limitPrice);
Money price = feed.getPrice(stock->symbol);
std::optional<Money> result = strategy->tryExecute(*order, price); // must be atomic w.r.t. account state — see W
if (result.has_value()) {
settle(order, *result);
} else {
order->status = OrderStatus::PENDING;
feed.subscribe(stock->symbol, order); // re-checked on future price ticks
}
return order;
}
void onPriceUpdate(const Symbol& symbol, Money newPrice) { // Observer callback, not polling
for (auto& order : pendingOrdersFor(symbol)) {
auto result = order->strategy->tryExecute(*order, newPrice);
if (result.has_value()) settle(order, *result);
}
}
void settle(std::shared_ptr<Order> order, Money executionPrice) {
if (order->side == Side::BUY) {
account->release(order->reservedEstimate);
account->withdraw(executionPrice * order->quantity);
portfolio.addHolding(order->stock, order->quantity);
} else {
portfolio.settle(order->stock, order->quantity, Side::SELL); // removes reserved shares
account->deposit(executionPrice * order->quantity);
}
order->status = OrderStatus::EXECUTED;
}
void cancelOrder(OrderId orderId) {
auto order = orderRepository.find(orderId);
if (order->status != OrderStatus::PENDING) throw InvalidOrderStateException();
releaseReservation(order); // cash or shares, by side
order->status = OrderStatus::CANCELLED;
}
function placeOrder(account, stock, side, qty, type, limitPrice) {
if (side === Side.BUY) {
const estimatedCost = (type === OrderType.MARKET ? feed.getPrice(stock.symbol) : limitPrice) * qty;
account.reserve(estimatedCost); // throws InsufficientFundsException
} else {
account.portfolio.reserveShares(stock, qty); // throws InsufficientHoldingsException
}
const order = Order.create(account, stock, side, qty, type, limitPrice);
const strategy = strategyFor(type, limitPrice);
const price = feed.getPrice(stock.symbol);
const result = strategy.tryExecute(order, price); // must be atomic w.r.t. account state — see W
if (result !== null) {
settle(order, result);
} else {
order.status = 'PENDING';
feed.subscribe(stock.symbol, order); // re-checked on future price ticks
}
return order;
}
function onPriceUpdate(symbol, newPrice) { // Observer callback, not polling
for (const order of pendingOrdersFor(symbol)) {
const result = order.strategy.tryExecute(order, newPrice);
if (result !== null) settle(order, result);
}
}
function settle(order, executionPrice) {
if (order.side === Side.BUY) {
account.release(order.reservedEstimate);
account.withdraw(executionPrice * order.quantity);
portfolio.addHolding(order.stock, order.quantity);
} else {
portfolio.settle(order.stock, order.quantity, Side.SELL); // removes reserved shares
account.deposit(executionPrice * order.quantity);
}
order.status = 'EXECUTED';
}
function cancelOrder(orderId) {
const order = orderRepository.find(orderId);
if (order.status !== 'PENDING') throw new Error('InvalidOrderStateException');
releaseReservation(order); // cash or shares, by side
order.status = 'CANCELLED';
}
function placeOrder(account: Account, stock: Stock, side: Side, qty: number,
type: OrderType, limitPrice: Money): Order {
if (side === Side.BUY) {
const estimatedCost: Money = (type === OrderType.MARKET ? feed.getPrice(stock.symbol) : limitPrice) * qty;
account.reserve(estimatedCost); // throws InsufficientFundsException
} else {
account.portfolio.reserveShares(stock, qty); // throws InsufficientHoldingsException
}
const order: Order = Order.create(account, stock, side, qty, type, limitPrice);
const strategy: ExecutionStrategy = strategyFor(type, limitPrice);
const price: Money = feed.getPrice(stock.symbol);
const result: Money | null = strategy.tryExecute(order, price); // must be atomic w.r.t. account state — see W
if (result !== null) {
settle(order, result);
} else {
order.status = OrderStatus.PENDING;
feed.subscribe(stock.symbol, order); // re-checked on future price ticks
}
return order;
}
function onPriceUpdate(symbol: Symbol, newPrice: Money): void { // Observer callback, not polling
for (const order of pendingOrdersFor(symbol)) {
const result: Money | null = order.strategy.tryExecute(order, newPrice);
if (result !== null) settle(order, result);
}
}
function settle(order: Order, executionPrice: Money): void {
if (order.side === Side.BUY) {
account.release(order.reservedEstimate);
account.withdraw(executionPrice * order.quantity);
portfolio.addHolding(order.stock, order.quantity);
} else {
portfolio.settle(order.stock, order.quantity, Side.SELL); // removes reserved shares
account.deposit(executionPrice * order.quantity);
}
order.status = OrderStatus.EXECUTED;
}
function cancelOrder(orderId: OrderId): void {
const order: Order = orderRepository.find(orderId);
if (order.status !== OrderStatus.PENDING) throw new Error('InvalidOrderStateException');
releaseReservation(order); // cash or shares, by side
order.status = OrderStatus.CANCELLED;
}
R. End-to-End Use Case
Investor
│ places a limit buy order
▼
Broker.placeOrder(account, stock, BUY, qty, LIMIT, limitPrice)
│
▼
Account.reserve(estimatedCost) // funds held, not yet spent
│
▼
LimitExecutionStrategy.tryExecute(order, feed.getPrice(stock))
│ price hasn't crossed the limit yet → returns empty
▼
Order.status = PENDING
│
▼
MarketDataFeed.subscribe(stock, order)
...time passes, price ticks arrive...
MarketDataFeed → onPriceUpdate(stock, newPrice)
│
▼
LimitExecutionStrategy.tryExecute(order, newPrice) // now crosses the limit
│
▼
settle(order, executionPrice)
│ Account.withdraw(), Portfolio.addHolding()
▼
Order.status = EXECUTED // investor sees the fill in history
S. Data Structures
Structure
Used For
Why
Alternative & Trade-off
Map<OrderId, Order>
Order lookup for cancel/status checks
O(1) lookup by order ID
DB-backed lookup if persistence is required (see X)
Map<Symbol, List<Order>>
Pending limit orders subscribed per stock
O(k) re-check on a price tick, where k = pending orders for that stock only — not all orders
A single global pending-order list — simpler, but forces scanning irrelevant orders on every tick
Integer cents (not float) for Money
All cash and price arithmetic
Avoids floating-point rounding errors compounding across trades
Arbitrary-precision decimal type — safer at odd fractions, slightly more overhead than plain integers
T. Complexity Analysis
Operation
Time
Space
placeOrder (market, immediate execution)
O(1)
O(1) extra per order
placeOrder (limit, goes pending)
O(1) to enqueue for the subscribed stock
O(1) per pending order
onPriceUpdate re-check
O(k), k = pending orders for that one stock
O(1) extra
cancelOrder
O(1) with an order-ID map
O(1)
Overall order/account storage
—
O(accounts + orders)
U. Testing Strategy
Happy path: deposit funds, place a market buy, portfolio reflects the new holding, cash balance decreases correctly.
Edge cases: buy that spends the balance down to exactly zero; sell of exactly all held shares; limit order placed exactly at the current price.
Invalid input: buy exceeding available funds; sell exceeding held shares; cancel of an unknown or already-executed order.
Failure scenarios: feed returns a stale or non-positive price (see Z); a pending order's stock is delisted mid-wait.
Concurrency tests: N concurrent buy orders on the same account that individually fit the balance but collectively exceed it — exactly the affordable prefix (by arrival or a defined tie-break) must succeed, the rest must get InsufficientFundsException, never an overdraft.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Classes Unchanged
Pattern/Trade-off
"Add stop-loss orders."
OCP under a new requirement
New execution rule triggered by price falling below a threshold
New StopLossExecutionStrategy only
Broker, Order
Validates the Strategy investment made in V3
"Two sell orders for the same stock race, but holdings only cover one."
Concurrency / race conditions
Share reservation on Portfolio must be atomic
Portfolio.reserveShares()
External API signature
Optimistic (CAS) vs. pessimistic (lock) per account — see W
"Add a commission per trade."
Recognizing a second, independent variability axis
New pluggable fee calculation consulted during settle()
New CommissionStrategy only
Order, Broker API
Don't fold this into ExecutionStrategy — pricing and fees vary independently
"Support real matching against other users' orders."
LLD → HLD boundary awareness
This crosses into an exchange matching engine / order book design
None in-process — this is a new subsystem
All core classes — the account/order model doesn't change, only how execution is sourced
Recognize the boundary rather than trying to build a matching engine inline
"The price feed goes down mid-trade."
Failure handling
Broker must reject/hold rather than execute against a stale price
MarketDataFeed gains a staleness/health check
Order, Account
Fail closed, not silently trade on old data
"A cancel request arrives just as the order is executing."
Concurrency / race conditions
Cancel and execute must both go through an atomic status transition on the order
Order.status transition (CAS)
External API signature
Whichever wins the CAS is final — no double-outcome
"Support margin / short-selling."
Business-rule modeling
Account gains a borrowing limit; Portfolio allows constrained negative holdings
Account, Portfolio validation logic
Broker, Order API
Push the new rule into validation, not into the allocator/execution path
"Millions of users trading simultaneously."
LLD → HLD boundary awareness
Sharding accounts by user ID, a message bus fanning out price ticks, a database instead of in-memory maps
None in-process
All core classes — the object model doesn't change, only where it runs
Recognize the boundary rather than trying to distributed-system-ify the domain classes
"How would you test this without a real market feed?"
Testability awareness
Inject a fake MarketDataFeed with a scriptable price sequence
None — this is exactly why MarketDataFeed is an interface
—
Dependency injection over a hardcoded live feed call
W. Concurrency
Shared mutable state: an account's cashBalance/reservedCash and a portfolio's holdings/reserved-quantity. Two orders racing to spend or sell against the same account is the canonical hazard here.
Critical section: "check available balance/holdings" + "reserve it" must be one atomic step — never two steps a thread could be pre-empted between.
Pessimistic approach: a mutex per account guarding reserve/release/settle. Simple, safe, and cheap in practice because accounts are naturally independent — locking Account A never blocks an order on Account B.
Optimistic approach: model the balance as a versioned/CAS-based field and retry the reserve on a failed compare-and-swap — avoids holding a lock, at the cost of retry logic.
Lock granularity trade-off: per-account locking is the natural boundary here (unlike a parking lot's shared spot pool) — different users' orders never need to contend, so granularity is close to free parallelism. A single global lock would be simpler but would serialize unrelated users' trades for no reason.
Idempotency: if a client retries a placeOrder call after a timeout (not knowing if it succeeded), the system must not double-reserve — this argues for an idempotency key on order placement.
Deadlock avoidance: a feature this base design doesn't need (transferring funds between two accounts) would require locking two accounts at once — always acquire such locks in a consistent global order (e.g., by account ID) to avoid circular wait.
X. Persistence
If state must survive a restart: Account, Portfolio, and Order become persistent entities with durable IDs, not in-memory object references. An AccountRepository/OrderRepository abstraction replaces direct in-memory maps — same interface shape, different backing store. The reserve-then-settle step becomes a database transaction (or an optimistic-locking update with a version column), so a crash between "reserve" and "settle" can't leave cash permanently held or shares double-counted. Order IDs should be generated in a way that's safe across restarts (UUID, not an in-memory counter).
Y. Scaling Boundary
Layer
What Belongs Here
LLD (this document)
Object relationships, responsibilities, APIs, in-process state, patterns for one brokerage instance
HLD
A BrokerService behind a load balancer, a database sharded by account/user ID, a message bus fanning out price ticks to interested pending orders, a cache for "current price" reads
Distributed Systems
Only relevant if building a real exchange: a matching engine/order book that pairs buyers with sellers directly, consensus on trade sequencing, and fairness guarantees across data centers — a fundamentally different problem than a brokerage that trades against a price feed
Z. Failure Scenarios
Duplicate order-placement retry: a client retries placeOrder after a timeout — must not double-reserve funds/shares (idempotency key on the request).
Crash between "reserve" and "settle": reconciliation on restart should treat "reserved but never settled or released" as an anomaly to flag, not silently trust as normal pending state.
Stale or invalid feed price: a price feed delivering a stale, zero, or negative price must be rejected by validation before it's ever used to execute a trade — never trade on unchecked external input.
Withdrawal racing a pending buy's reservation: a withdraw request must check against the available balance (total minus reserved), never the raw total, or a pending order's funds could be withdrawn out from under it.
AA. Common Bad Designs
One giant Broker god class doing validation + execution + portfolio updates + history logging inline. Looks reasonable early (fewer files!) but every new order type or fee rule means editing the same massive class — violates SRP and OCP simultaneously. Fix: extract ExecutionStrategy and repositories as soon as either shows signs of variability.
Singleton StockBroker. Common in reference implementations "because there's only one broker" — but that's an application-wiring decision, not a class-design constraint, and it makes side-by-side testing awkward. Fix: a single instance via dependency injection at the wiring layer.
Using floating-point for money. Looks fine in a demo, quietly accumulates rounding errors across many trades. Fix: integer cents (or a fixed-point/decimal Money type) for all currency arithmetic.
Polling the price feed on a timer for every pending order. Looks simple to write, wastes cycles and adds latency at any real scale. Fix: subscribe pending orders to price-change notifications (Observer) so only actual changes trigger a re-check.
Type-string switch statements (if order.type == "limit") instead of polymorphism. Every new order type means hunting down every switch statement across the codebase. Fix: polymorphic ExecutionStrategy selected once at order creation.
AB. Design Trade-Offs
Decision
Option A
Option B
Trade-off
Money representation
Integer cents
Floating-point
Integer cents is exact but needs care converting for display; float is convenient but silently accumulates rounding errors across many trades
Fund/holding validation timing
Reserve at order-placement time
Check only at execution time
Reserve-then-settle prevents overselling/overspending under concurrent pending orders, at the cost of extra bookkeeping (release paths on cancel/failure)
Limit-order price re-check
Push (Observer on feed updates)
Poll on a timer
Push is real-time and efficient but needs pub-sub wiring; poll is simpler to write but wastes cycles and adds latency proportional to the polling interval
Lock granularity
One lock per account
One global lock
Per-account is naturally parallel since accounts are independent; a global lock is simpler to reason about but serializes unrelated users' trades for no reason
AC. Interviewer Evaluation
Weak candidate: jumps straight to code; one big Broker class; uses floats for money; never asks whether execution is against a feed or a real order book.
Average candidate: asks 1–2 clarifying questions; produces a reasonable class split (Account, Order, Portfolio, Broker) but reservation and execution logic are still inline inside Broker.
Strong candidate: asks clarifying questions up front, including the order-book-vs-feed question; extracts ExecutionStrategy proactively because they identify execution rules as "likely to change"; models money as integers without being prompted.
Exceptional candidate: does all of the above, then anticipates the concurrency follow-up before being asked ("reserve needs to be atomic per account once orders can race"), and can clearly draw the line between this brokerage LLD and a full exchange's matching engine if pushed on scale.
AD. Interview Simulation
Interviewer: Design a stock brokerage system.
Candidate: Before I start — when I place an order, does it execute against a live market price feed, or does it get matched against other users' orders directly?
Interviewer: Assume it executes against a market data feed — you're not building an exchange.
Candidate: Good, that keeps this in LLD territory. I'll model a MarketDataFeed interface the broker depends on, so the price source is swappable and testable.
Interviewer: We need both market and limit orders, and pricing rules like commissions may come later.
Candidate: Then I'll isolate execution behind an ExecutionStrategy interface — market executes immediately, limit checks the price condition — and keep commission as a separate, future strategy rather than bolting it onto the same interface, since they vary independently.
Interviewer: What stops a user from placing two orders that together overdraw their balance?
Candidate: I'll reserve funds at order-placement time rather than only checking the balance — the reservation itself has to be atomic, guarded by a per-account lock, so two concurrent orders on the same account can't both pass a check against the same unspent cash.
Interviewer: The price feed goes down for thirty seconds. What happens to pending limit orders?
Candidate: They should just stay pending — I wouldn't execute against a stale or missing price. I'd add a staleness check on the feed and fail closed rather than trade on old data.
Interviewer: Last one — how would this look for millions of users trading at once?
Candidate: That crosses from LLD into HLD — I'd keep this exact account/order model, but shard accounts by user ID, put a message bus in front of price-tick fan-out instead of in-process subscriptions, and back the repositories with a real database. The object design here doesn't need to change; only where it runs does.
Abstract Order with BuyOrder/SellOrder, an OrderStatus enum, a Portfolio for holdings, and a Singleton StockBroker as the central coordinator
Clean, minimal entity set that maps closely to the real domain; good baseline vocabulary (Order/Account/Portfolio/Stock)
Singleton on the central coordinator is a questionable default (see M); execution is a same-thread call with no explicit reservation step, so it doesn't address the double-spend race this document calls out in W
The core entity shape (Order/Account/Portfolio/Stock) — adopted and then hardened with reservation semantics and a pluggable execution strategy in this document
This problem is less standardized across public LLD write-ups than something like Parking Lot — most tutorial-style sources converge on a similar entity shape (Account/Order/Portfolio) but rarely address the concurrent-reservation problem directly, which is why section W here goes further than most references do.
AF. Interview Cheat Sheet
Problem
Stock Brokerage System
Difficulty
Advanced (Concurrency & Consistency)
Core requirements
Account with cash + portfolio; buy/sell orders (market/limit); execution against a price feed; order/history tracking
Key clarifications
Feed-execution or real order matching? Reserve-at-placement or check-at-execution? Limit orders pending or rejected?
Core entities
Account, Portfolio, Stock, Order (Buy/Sell), ExecutionStrategy, MarketDataFeed, Broker (facade)
God-class Broker; Singleton by default; float for money; polling instead of Observer
What NOT to over-engineer
A real order-matching engine, multi-currency, tax reporting — unless explicitly asked
AG. Full Implementation
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
// Problem 21: Stock Brokerage System
// Patterns: Strategy (ExecutionStrategy), Observer (MarketDataFeed -> Broker), Facade (Broker)
// Concurrency: per-account lock guards reserve+execute+settle so cash/holdings never corrupt
// under concurrent orders (badge: Advanced, Concurrency & Consistency).
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
public class p21 {
// ---------- Exceptions ----------
static class InsufficientFundsException extends RuntimeException { InsufficientFundsException(String m) { super(m); } }
static class InsufficientHoldingsException extends RuntimeException { InsufficientHoldingsException(String m) { super(m); } }
static class InvalidOrderStateException extends RuntimeException { InvalidOrderStateException(String m) { super(m); } }
static class OrderNotFoundException extends RuntimeException { OrderNotFoundException(String m) { super(m); } }
enum Side { BUY, SELL }
enum OrderType { MARKET, LIMIT }
enum OrderStatus { PENDING, EXECUTED, CANCELLED }
// ---------- Stock ----------
static class Stock {
final String symbol;
volatile double currentPrice;
Stock(String symbol, double currentPrice) { this.symbol = symbol; this.currentPrice = currentPrice; }
void updatePrice(double newPrice) { this.currentPrice = newPrice; }
}
// ---------- Portfolio (owned by Account) ----------
static class Portfolio {
private final Map<String, Integer> holdings = new HashMap<>();
private final Map<String, Integer> reserved = new HashMap<>();
void reserveShares(Stock stock, int qty) {
int owned = holdings.getOrDefault(stock.symbol, 0);
int alreadyReserved = reserved.getOrDefault(stock.symbol, 0);
if (owned - alreadyReserved < qty) throw new InsufficientHoldingsException("Not enough shares of " + stock.symbol);
reserved.merge(stock.symbol, qty, Integer::sum);
}
void releaseShares(Stock stock, int qty) {
reserved.merge(stock.symbol, -qty, Integer::sum);
}
void addHolding(Stock stock, int qty) {
holdings.merge(stock.symbol, qty, Integer::sum);
}
// Removes settled (sold) shares from both holdings and their reservation.
void settle(Stock stock, int qty, Side side) {
if (side == Side.SELL) {
holdings.merge(stock.symbol, -qty, Integer::sum);
reserved.merge(stock.symbol, -qty, Integer::sum);
}
}
int holdingOf(String symbol) { return holdings.getOrDefault(symbol, 0); }
}
// ---------- Account (owns Portfolio) ----------
static class Account {
final String id;
double cashBalance;
double reservedCash;
final Portfolio portfolio = new Portfolio();
final ReentrantLock lock = new ReentrantLock(); // guards reserve/execute/settle atomicity
Account(String id, double initialCash) { this.id = id; this.cashBalance = initialCash; }
void reserve(double amount) {
if (cashBalance - reservedCash < amount) throw new InsufficientFundsException("Insufficient funds in " + id);
reservedCash += amount;
}
void release(double amount) { reservedCash = Math.max(0, reservedCash - amount); }
void deposit(double amount) { cashBalance += amount; }
void withdraw(double amount) {
if (cashBalance < amount) throw new InsufficientFundsException("Insufficient funds in " + id);
cashBalance -= amount;
}
}
// ---------- Order (abstract) ----------
static abstract class Order {
final String id;
final Account account;
final Stock stock;
final int quantity;
final OrderType type;
final double limitPrice; // unused for MARKET
double reservedEstimate;
OrderStatus status = OrderStatus.PENDING;
final long createdAt = System.nanoTime();
Order(String id, Account account, Stock stock, int quantity, OrderType type, double limitPrice) {
this.id = id; this.account = account; this.stock = stock;
this.quantity = quantity; this.type = type; this.limitPrice = limitPrice;
}
abstract Side getSide();
void cancel() {
if (status != OrderStatus.PENDING) throw new InvalidOrderStateException("Order not cancellable: " + status);
status = OrderStatus.CANCELLED;
}
}
static class BuyOrder extends Order {
BuyOrder(String id, Account account, Stock stock, int quantity, OrderType type, double limitPrice) {
super(id, account, stock, quantity, type, limitPrice);
}
@Override Side getSide() { return Side.BUY; }
}
static class SellOrder extends Order {
SellOrder(String id, Account account, Stock stock, int quantity, OrderType type, double limitPrice) {
super(id, account, stock, quantity, type, limitPrice);
}
@Override Side getSide() { return Side.SELL; }
}
// ---------- ExecutionStrategy ----------
interface ExecutionStrategy {
OptionalDouble tryExecute(Order order, double currentPrice);
}
static class MarketExecutionStrategy implements ExecutionStrategy {
@Override public OptionalDouble tryExecute(Order order, double currentPrice) {
return OptionalDouble.of(currentPrice); // market orders always execute immediately
}
}
static class LimitExecutionStrategy implements ExecutionStrategy {
@Override public OptionalDouble tryExecute(Order order, double currentPrice) {
boolean conditionMet = order.getSide() == Side.BUY
? currentPrice <= order.limitPrice
: currentPrice >= order.limitPrice;
return conditionMet ? OptionalDouble.of(currentPrice) : OptionalDouble.empty();
}
}
// ---------- MarketDataFeed ----------
interface PriceListener {
void onPriceUpdate(String symbol, double newPrice);
}
interface MarketDataFeed {
double getPrice(String symbol);
void subscribe(String symbol, PriceListener listener);
}
static class SimpleMarketDataFeed implements MarketDataFeed {
private final Map<String, Stock> stocks;
private final Map<String, List<PriceListener>> listeners = new HashMap<>();
SimpleMarketDataFeed(Map<String, Stock> stocks) { this.stocks = stocks; }
@Override public double getPrice(String symbol) { return stocks.get(symbol).currentPrice; }
@Override public void subscribe(String symbol, PriceListener listener) {
List<PriceListener> ls = listeners.computeIfAbsent(symbol, k -> new ArrayList<>());
if (!ls.contains(listener)) ls.add(listener);
}
void updatePrice(String symbol, double newPrice) {
stocks.get(symbol).updatePrice(newPrice);
for (PriceListener l : listeners.getOrDefault(symbol, Collections.emptyList())) {
l.onPriceUpdate(symbol, newPrice); // push, don't poll
}
}
}
// ---------- Broker (Facade) ----------
static class Broker implements PriceListener {
private final Map<String, Account> accounts = new HashMap<>();
private final Map<String, Order> ordersById = new HashMap<>();
private final Map<String, List<Order>> pendingOrdersBySymbol = new HashMap<>();
private final MarketDataFeed feed;
private final ExecutionStrategy marketStrategy = new MarketExecutionStrategy();
private final ExecutionStrategy limitStrategy = new LimitExecutionStrategy();
private int nextOrderId = 1;
Broker(MarketDataFeed feed) { this.feed = feed; }
void addAccount(Account account) { accounts.put(account.id, account); }
void deposit(Account account, double amount) { account.deposit(amount); }
void withdraw(Account account, double amount) { account.withdraw(amount); }
Order placeOrder(Account account, Stock stock, Side side, int quantity, OrderType type, double limitPrice) {
account.lock.lock(); // guard reserve + execution-attempt + settle atomically
try {
ExecutionStrategy strategy = (type == OrderType.MARKET) ? marketStrategy : limitStrategy;
double currentPrice = feed.getPrice(stock.symbol);
String id = "ORD-" + (nextOrderId++);
Order order;
if (side == Side.BUY) {
double estimatedCost = quantity * (type == OrderType.MARKET ? currentPrice : limitPrice);
account.reserve(estimatedCost); // throws InsufficientFundsException
order = new BuyOrder(id, account, stock, quantity, type, limitPrice);
order.reservedEstimate = estimatedCost;
} else {
account.portfolio.reserveShares(stock, quantity); // throws InsufficientHoldingsException
order = new SellOrder(id, account, stock, quantity, type, limitPrice);
}
ordersById.put(order.id, order);
OptionalDouble result = strategy.tryExecute(order, currentPrice);
if (result.isPresent()) {
settle(order, result.getAsDouble());
} else {
order.status = OrderStatus.PENDING;
pendingOrdersBySymbol.computeIfAbsent(stock.symbol, k -> new ArrayList<>()).add(order);
feed.subscribe(stock.symbol, this); // re-checked on future price ticks
}
return order;
} finally {
account.lock.unlock();
}
}
@Override public void onPriceUpdate(String symbol, double newPrice) {
List<Order> pending = pendingOrdersBySymbol.get(symbol);
if (pending == null) return;
List<Order> stillPending = new ArrayList<>();
for (Order order : new ArrayList<>(pending)) {
if (order.status != OrderStatus.PENDING) continue; // already cancelled/executed
order.account.lock.lock();
try {
ExecutionStrategy strategy = (order.type == OrderType.MARKET) ? marketStrategy : limitStrategy;
OptionalDouble result = strategy.tryExecute(order, newPrice);
if (result.isPresent()) {
settle(order, result.getAsDouble());
} else {
stillPending.add(order);
}
} finally {
order.account.lock.unlock();
}
}
pendingOrdersBySymbol.put(symbol, stillPending);
}
// Must be called while holding order.account.lock.
private void settle(Order order, double executionPrice) {
Account account = order.account;
if (order.getSide() == Side.BUY) {
account.release(order.reservedEstimate);
account.withdraw(executionPrice * order.quantity);
account.portfolio.addHolding(order.stock, order.quantity);
} else {
account.portfolio.settle(order.stock, order.quantity, Side.SELL); // removes reserved shares
account.deposit(executionPrice * order.quantity);
}
order.status = OrderStatus.EXECUTED;
}
void cancelOrder(String orderId) {
Order order = ordersById.get(orderId);
if (order == null) throw new OrderNotFoundException("No such order: " + orderId);
order.account.lock.lock();
try {
if (order.status != OrderStatus.PENDING) throw new InvalidOrderStateException("Order not cancellable: " + order.status);
if (order.getSide() == Side.BUY) order.account.release(order.reservedEstimate);
else order.account.portfolio.releaseShares(order.stock, order.quantity);
order.status = OrderStatus.CANCELLED;
} finally {
order.account.lock.unlock();
}
}
}
// ---------- Demo ----------
public static void main(String[] args) throws InterruptedException {
Stock appl = new Stock("AAPL", 150.0);
Map<String, Stock> stocks = new HashMap<>();
stocks.put("AAPL", appl);
SimpleMarketDataFeed feed = new SimpleMarketDataFeed(stocks);
Broker broker = new Broker(feed);
Account account = new Account("A1", 10000.0);
broker.addAccount(account);
// Market buy: executes immediately.
Order buy = broker.placeOrder(account, appl, Side.BUY, 10, OrderType.MARKET, 0);
System.out.println("Market buy status=" + buy.status + " cash=" + account.cashBalance + " holdings=" + account.portfolio.holdingOf("AAPL"));
// Limit sell above current price: stays pending until price rises.
Order limitSell = broker.placeOrder(account, appl, Side.SELL, 5, OrderType.LIMIT, 160.0);
System.out.println("Limit sell status=" + limitSell.status);
feed.updatePrice("AAPL", 161.0); // Observer push triggers re-check
System.out.println("After price tick, limit sell status=" + limitSell.status + " cash=" + account.cashBalance);
// Place another limit sell, then cancel it before it executes.
Order toCancel = broker.placeOrder(account, appl, Side.SELL, 2, OrderType.LIMIT, 500.0);
broker.cancelOrder(toCancel.id);
System.out.println("Cancelled order status=" + toCancel.status + " holdings=" + account.portfolio.holdingOf("AAPL"));
// Concurrency demo: two threads race to sell the last 3 shares concurrently.
Account raceAccount = new Account("A2", 5000.0);
broker.addAccount(raceAccount);
broker.placeOrder(raceAccount, appl, Side.BUY, 3, OrderType.MARKET, 0); // gives raceAccount exactly 3 shares
Runnable trySell = () -> {
try {
broker.placeOrder(raceAccount, appl, Side.SELL, 3, OrderType.MARKET, 0);
} catch (InsufficientHoldingsException e) {
System.out.println("Thread correctly rejected: " + e.getMessage());
}
};
Thread t1 = new Thread(trySell);
Thread t2 = new Thread(trySell);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("Race account final holdings=" + raceAccount.portfolio.holdingOf("AAPL") + " (never negative)");
}
}
// Problem 21: Stock Brokerage System
// Patterns: Strategy (ExecutionStrategy), Observer (MarketDataFeed -> Broker), Facade (Broker)
// Concurrency: per-account mutex guards reserve+execute+settle so cash/holdings never corrupt
// under concurrent orders (badge: Advanced, Concurrency & Consistency).
#include <iostream>
#include <vector>
#include <map>
#include <memory>
#include <string>
#include <optional>
#include <mutex>
#include <thread>
#include <stdexcept>
#include <algorithm>
using namespace std;
// ---------- Exceptions ----------
struct InsufficientFundsException : runtime_error { InsufficientFundsException(const string& m) : runtime_error(m) {} };
struct InsufficientHoldingsException : runtime_error { InsufficientHoldingsException(const string& m) : runtime_error(m) {} };
struct InvalidOrderStateException : runtime_error { InvalidOrderStateException(const string& m) : runtime_error(m) {} };
struct OrderNotFoundException : runtime_error { OrderNotFoundException(const string& m) : runtime_error(m) {} };
enum class Side { BUY, SELL };
enum class OrderType { MARKET, LIMIT };
enum class OrderStatus { PENDING, EXECUTED, CANCELLED };
// ---------- Stock ----------
struct Stock {
string symbol;
double currentPrice;
Stock(string symbol_, double price) : symbol(move(symbol_)), currentPrice(price) {}
void updatePrice(double newPrice) { currentPrice = newPrice; }
};
// ---------- Portfolio (owned by Account) ----------
class Portfolio {
map<string, int> holdings;
map<string, int> reserved;
public:
void reserveShares(const Stock& stock, int qty) {
int owned = holdings.count(stock.symbol) ? holdings[stock.symbol] : 0;
int alreadyReserved = reserved.count(stock.symbol) ? reserved[stock.symbol] : 0;
if (owned - alreadyReserved < qty) throw InsufficientHoldingsException("Not enough shares of " + stock.symbol);
reserved[stock.symbol] += qty;
}
void releaseShares(const Stock& stock, int qty) { reserved[stock.symbol] -= qty; }
void addHolding(const Stock& stock, int qty) { holdings[stock.symbol] += qty; }
// Removes settled (sold) shares from both holdings and their reservation.
void settle(const Stock& stock, int qty, Side side) {
if (side == Side::SELL) {
holdings[stock.symbol] -= qty;
reserved[stock.symbol] -= qty;
}
}
int holdingOf(const string& symbol) { return holdings.count(symbol) ? holdings[symbol] : 0; }
};
// ---------- Account (owns Portfolio) ----------
class Account {
public:
string id;
double cashBalance;
double reservedCash = 0;
Portfolio portfolio;
mutex lockMutex; // guards reserve/execute/settle atomicity
Account(string id_, double initialCash) : id(move(id_)), cashBalance(initialCash) {}
void reserve(double amount) {
if (cashBalance - reservedCash < amount) throw InsufficientFundsException("Insufficient funds in " + id);
reservedCash += amount;
}
void release(double amount) { reservedCash = max(0.0, reservedCash - amount); }
void deposit(double amount) { cashBalance += amount; }
void withdraw(double amount) {
if (cashBalance < amount) throw InsufficientFundsException("Insufficient funds in " + id);
cashBalance -= amount;
}
};
// ---------- Order (abstract) ----------
class Order {
public:
string id;
shared_ptr<Account> account;
shared_ptr<Stock> stock;
int quantity;
OrderType type;
double limitPrice; // unused for MARKET
double reservedEstimate = 0;
OrderStatus status = OrderStatus::PENDING;
Order(string id_, shared_ptr<Account> account_, shared_ptr<Stock> stock_, int quantity_, OrderType type_, double limitPrice_)
: id(move(id_)), account(account_), stock(stock_), quantity(quantity_), type(type_), limitPrice(limitPrice_) {}
virtual ~Order() = default;
virtual Side getSide() const = 0;
void cancel() {
if (status != OrderStatus::PENDING) throw InvalidOrderStateException("Order not cancellable");
status = OrderStatus::CANCELLED;
}
};
class BuyOrder : public Order {
public:
using Order::Order;
Side getSide() const override { return Side::BUY; }
};
class SellOrder : public Order {
public:
using Order::Order;
Side getSide() const override { return Side::SELL; }
};
// ---------- ExecutionStrategy ----------
class ExecutionStrategy {
public:
virtual ~ExecutionStrategy() = default;
virtual optional<double> tryExecute(Order& order, double currentPrice) = 0;
};
class MarketExecutionStrategy : public ExecutionStrategy {
public:
optional<double> tryExecute(Order&, double currentPrice) override { return currentPrice; } // always executes
};
class LimitExecutionStrategy : public ExecutionStrategy {
public:
optional<double> tryExecute(Order& order, double currentPrice) override {
bool conditionMet = order.getSide() == Side::BUY
? currentPrice <= order.limitPrice
: currentPrice >= order.limitPrice;
return conditionMet ? optional<double>(currentPrice) : nullopt;
}
};
// ---------- MarketDataFeed ----------
class PriceListener {
public:
virtual ~PriceListener() = default;
virtual void onPriceUpdate(const string& symbol, double newPrice) = 0;
};
class MarketDataFeed {
public:
virtual ~MarketDataFeed() = default;
virtual double getPrice(const string& symbol) = 0;
virtual void subscribe(const string& symbol, PriceListener* listener) = 0;
};
class SimpleMarketDataFeed : public MarketDataFeed {
map<string, shared_ptr<Stock>> stocks;
map<string, vector<PriceListener*>> listeners;
public:
SimpleMarketDataFeed(map<string, shared_ptr<Stock>> stocks_) : stocks(move(stocks_)) {}
double getPrice(const string& symbol) override { return stocks[symbol]->currentPrice; }
void subscribe(const string& symbol, PriceListener* listener) override {
auto& ls = listeners[symbol];
if (find(ls.begin(), ls.end(), listener) == ls.end()) ls.push_back(listener);
}
void updatePrice(const string& symbol, double newPrice) {
stocks[symbol]->updatePrice(newPrice);
for (auto* l : listeners[symbol]) l->onPriceUpdate(symbol, newPrice); // push, don't poll
}
};
// ---------- Broker (Facade) ----------
class Broker : public PriceListener {
map<string, shared_ptr<Account>> accounts;
map<string, shared_ptr<Order>> ordersById;
map<string, vector<shared_ptr<Order>>> pendingOrdersBySymbol;
MarketDataFeed* feed;
MarketExecutionStrategy marketStrategy;
LimitExecutionStrategy limitStrategy;
int nextOrderId = 1;
public:
Broker(MarketDataFeed* feed_) : feed(feed_) {}
void addAccount(shared_ptr<Account> account) { accounts[account->id] = account; }
void deposit(shared_ptr<Account> account, double amount) { account->deposit(amount); }
void withdraw(shared_ptr<Account> account, double amount) { account->withdraw(amount); }
shared_ptr<Order> placeOrder(shared_ptr<Account> account, shared_ptr<Stock> stock, Side side,
int quantity, OrderType type, double limitPrice) {
lock_guard<mutex> guard(account->lockMutex); // guard reserve + execution-attempt + settle atomically
ExecutionStrategy* strategy = (type == OrderType::MARKET) ? (ExecutionStrategy*)&marketStrategy : (ExecutionStrategy*)&limitStrategy;
double currentPrice = feed->getPrice(stock->symbol);
string id = "ORD-" + to_string(nextOrderId++);
shared_ptr<Order> order;
if (side == Side::BUY) {
double estimatedCost = quantity * (type == OrderType::MARKET ? currentPrice : limitPrice);
account->reserve(estimatedCost); // throws InsufficientFundsException
order = make_shared<BuyOrder>(id, account, stock, quantity, type, limitPrice);
order->reservedEstimate = estimatedCost;
} else {
account->portfolio.reserveShares(*stock, quantity); // throws InsufficientHoldingsException
order = make_shared<SellOrder>(id, account, stock, quantity, type, limitPrice);
}
ordersById[order->id] = order;
auto result = strategy->tryExecute(*order, currentPrice);
if (result.has_value()) {
settle(*order, *result);
} else {
order->status = OrderStatus::PENDING;
pendingOrdersBySymbol[stock->symbol].push_back(order);
feed->subscribe(stock->symbol, this); // re-checked on future price ticks
}
return order;
}
void onPriceUpdate(const string& symbol, double newPrice) override {
auto it = pendingOrdersBySymbol.find(symbol);
if (it == pendingOrdersBySymbol.end()) return;
vector<shared_ptr<Order>> stillPending;
for (auto& order : it->second) {
if (order->status != OrderStatus::PENDING) continue;
lock_guard<mutex> guard(order->account->lockMutex);
ExecutionStrategy* strategy = (order->type == OrderType::MARKET) ? (ExecutionStrategy*)&marketStrategy : (ExecutionStrategy*)&limitStrategy;
auto result = strategy->tryExecute(*order, newPrice);
if (result.has_value()) settle(*order, *result);
else stillPending.push_back(order);
}
it->second = stillPending;
}
void cancelOrder(const string& orderId) {
auto it = ordersById.find(orderId);
if (it == ordersById.end()) throw OrderNotFoundException("No such order: " + orderId);
auto order = it->second;
lock_guard<mutex> guard(order->account->lockMutex);
if (order->status != OrderStatus::PENDING) throw InvalidOrderStateException("Order not cancellable");
if (order->getSide() == Side::BUY) order->account->release(order->reservedEstimate);
else order->account->portfolio.releaseShares(*order->stock, order->quantity);
order->status = OrderStatus::CANCELLED;
}
private:
// Must be called while holding order.account->lockMutex.
void settle(Order& order, double executionPrice) {
auto account = order.account;
if (order.getSide() == Side::BUY) {
account->release(order.reservedEstimate);
account->withdraw(executionPrice * order.quantity);
account->portfolio.addHolding(*order.stock, order.quantity);
} else {
account->portfolio.settle(*order.stock, order.quantity, Side::SELL); // removes reserved shares
account->deposit(executionPrice * order.quantity);
}
order.status = OrderStatus::EXECUTED;
}
};
string statusName(OrderStatus s) {
switch (s) {
case OrderStatus::PENDING: return "PENDING";
case OrderStatus::EXECUTED: return "EXECUTED";
case OrderStatus::CANCELLED: return "CANCELLED";
}
return "?";
}
// ---------- Demo ----------
int main() {
auto appl = make_shared<Stock>("AAPL", 150.0);
map<string, shared_ptr<Stock>> stocks = {{"AAPL", appl}};
SimpleMarketDataFeed feed(stocks);
Broker broker(&feed);
auto account = make_shared<Account>("A1", 10000.0);
broker.addAccount(account);
auto buy = broker.placeOrder(account, appl, Side::BUY, 10, OrderType::MARKET, 0);
cout << "Market buy status=" << statusName(buy->status) << " cash=" << account->cashBalance
<< " holdings=" << account->portfolio.holdingOf("AAPL") << endl;
auto limitSell = broker.placeOrder(account, appl, Side::SELL, 5, OrderType::LIMIT, 160.0);
cout << "Limit sell status=" << statusName(limitSell->status) << endl;
feed.updatePrice("AAPL", 161.0); // Observer push triggers re-check
cout << "After price tick, limit sell status=" << statusName(limitSell->status) << " cash=" << account->cashBalance << endl;
auto toCancel = broker.placeOrder(account, appl, Side::SELL, 2, OrderType::LIMIT, 500.0);
broker.cancelOrder(toCancel->id);
cout << "Cancelled order status=" << statusName(toCancel->status) << " holdings=" << account->portfolio.holdingOf("AAPL") << endl;
// Concurrency demo: two threads race to sell the last 3 shares concurrently.
auto raceAccount = make_shared<Account>("A2", 5000.0);
broker.addAccount(raceAccount);
broker.placeOrder(raceAccount, appl, Side::BUY, 3, OrderType::MARKET, 0);
auto trySell = [&]() {
try {
broker.placeOrder(raceAccount, appl, Side::SELL, 3, OrderType::MARKET, 0);
} catch (InsufficientHoldingsException& e) {
cout << "Thread correctly rejected: " << e.what() << endl;
}
};
thread t1(trySell), t2(trySell);
t1.join(); t2.join();
cout << "Race account final holdings=" << raceAccount->portfolio.holdingOf("AAPL") << " (never negative)" << endl;
return 0;
}
// Problem 21: Stock Brokerage System
// Patterns: Strategy (ExecutionStrategy), Observer (MarketDataFeed -> Broker), Facade (Broker)
// Concurrency note: Node.js runs this synchronous demo on a single thread, so there is no
// real data race to guard against here -- but we still model a per-account Lock explicitly
// (mirroring the Java/C++ versions' mutex) so that the *shape* of the critical section
// (reserve -> tryExecute -> settle, all atomic w.r.t. one account) is structurally faithful
// and would compose correctly if ported to a worker/thread-based environment.
class InsufficientFundsException extends Error {}
class InsufficientHoldingsException extends Error {}
class InvalidOrderStateException extends Error {}
class OrderNotFoundException extends Error {}
const Side = Object.freeze({ BUY: 'BUY', SELL: 'SELL' });
const OrderType = Object.freeze({ MARKET: 'MARKET', LIMIT: 'LIMIT' });
const OrderStatus = Object.freeze({ PENDING: 'PENDING', EXECUTED: 'EXECUTED', CANCELLED: 'CANCELLED' });
// Trivial synchronous lock: acquire/release bracket the critical section explicitly,
// keeping the code shape identical to the Java/C++ mutex-guarded versions.
class Lock {
acquire() { /* no-op: single-threaded event loop, but marks the critical section */ }
release() { /* no-op */ }
}
// ---------- Stock ----------
class Stock {
constructor(symbol, currentPrice) { this.symbol = symbol; this.currentPrice = currentPrice; }
updatePrice(newPrice) { this.currentPrice = newPrice; }
}
// ---------- Portfolio (owned by Account) ----------
class Portfolio {
constructor() { this.holdings = new Map(); this.reserved = new Map(); }
reserveShares(stock, qty) {
const owned = this.holdings.get(stock.symbol) || 0;
const alreadyReserved = this.reserved.get(stock.symbol) || 0;
if (owned - alreadyReserved < qty) throw new InsufficientHoldingsException(`Not enough shares of ${stock.symbol}`);
this.reserved.set(stock.symbol, alreadyReserved + qty);
}
releaseShares(stock, qty) {
this.reserved.set(stock.symbol, (this.reserved.get(stock.symbol) || 0) - qty);
}
addHolding(stock, qty) {
this.holdings.set(stock.symbol, (this.holdings.get(stock.symbol) || 0) + qty);
}
// Removes settled (sold) shares from both holdings and their reservation.
settle(stock, qty, side) {
if (side === Side.SELL) {
this.holdings.set(stock.symbol, (this.holdings.get(stock.symbol) || 0) - qty);
this.reserved.set(stock.symbol, (this.reserved.get(stock.symbol) || 0) - qty);
}
}
holdingOf(symbol) { return this.holdings.get(symbol) || 0; }
}
// ---------- Account (owns Portfolio) ----------
class Account {
constructor(id, initialCash) {
this.id = id; this.cashBalance = initialCash; this.reservedCash = 0;
this.portfolio = new Portfolio();
this.lock = new Lock(); // guards reserve/execute/settle atomicity
}
reserve(amount) {
if (this.cashBalance - this.reservedCash < amount) throw new InsufficientFundsException(`Insufficient funds in ${this.id}`);
this.reservedCash += amount;
}
release(amount) { this.reservedCash = Math.max(0, this.reservedCash - amount); }
deposit(amount) { this.cashBalance += amount; }
withdraw(amount) {
if (this.cashBalance < amount) throw new InsufficientFundsException(`Insufficient funds in ${this.id}`);
this.cashBalance -= amount;
}
}
// ---------- Order (abstract) ----------
class Order {
constructor(id, account, stock, quantity, type, limitPrice) {
if (new.target === Order) throw new Error('Order is abstract');
this.id = id; this.account = account; this.stock = stock;
this.quantity = quantity; this.type = type; this.limitPrice = limitPrice;
this.reservedEstimate = 0;
this.status = OrderStatus.PENDING;
}
getSide() { throw new Error('abstract method'); }
cancel() {
if (this.status !== OrderStatus.PENDING) throw new InvalidOrderStateException('Order not cancellable');
this.status = OrderStatus.CANCELLED;
}
}
class BuyOrder extends Order { getSide() { return Side.BUY; } }
class SellOrder extends Order { getSide() { return Side.SELL; } }
// ---------- ExecutionStrategy ----------
class MarketExecutionStrategy {
tryExecute(order, currentPrice) { return currentPrice; } // always executes, market orders take the going price
}
class LimitExecutionStrategy {
tryExecute(order, currentPrice) {
const conditionMet = order.getSide() === Side.BUY ? currentPrice <= order.limitPrice : currentPrice >= order.limitPrice;
return conditionMet ? currentPrice : null;
}
}
// ---------- MarketDataFeed ----------
class SimpleMarketDataFeed {
constructor(stocks) { this.stocks = stocks; this.listeners = new Map(); } // symbol -> Stock, symbol -> listeners[]
getPrice(symbol) { return this.stocks.get(symbol).currentPrice; }
subscribe(symbol, listener) {
if (!this.listeners.has(symbol)) this.listeners.set(symbol, []);
const ls = this.listeners.get(symbol);
if (!ls.includes(listener)) ls.push(listener);
}
updatePrice(symbol, newPrice) {
this.stocks.get(symbol).updatePrice(newPrice);
for (const l of this.listeners.get(symbol) || []) l.onPriceUpdate(symbol, newPrice); // push, don't poll
}
}
// ---------- Broker (Facade) ----------
class Broker {
constructor(feed) {
this.feed = feed;
this.accounts = new Map();
this.ordersById = new Map();
this.pendingOrdersBySymbol = new Map();
this.marketStrategy = new MarketExecutionStrategy();
this.limitStrategy = new LimitExecutionStrategy();
this.nextOrderId = 1;
}
addAccount(account) { this.accounts.set(account.id, account); }
deposit(account, amount) { account.deposit(amount); }
withdraw(account, amount) { account.withdraw(amount); }
placeOrder(account, stock, side, quantity, type, limitPrice) {
account.lock.acquire(); // guard reserve + execution-attempt + settle atomically
try {
const strategy = type === OrderType.MARKET ? this.marketStrategy : this.limitStrategy;
const currentPrice = this.feed.getPrice(stock.symbol);
const id = `ORD-${this.nextOrderId++}`;
let order;
if (side === Side.BUY) {
const estimatedCost = quantity * (type === OrderType.MARKET ? currentPrice : limitPrice);
account.reserve(estimatedCost); // throws InsufficientFundsException
order = new BuyOrder(id, account, stock, quantity, type, limitPrice);
order.reservedEstimate = estimatedCost;
} else {
account.portfolio.reserveShares(stock, quantity); // throws InsufficientHoldingsException
order = new SellOrder(id, account, stock, quantity, type, limitPrice);
}
this.ordersById.set(order.id, order);
const result = strategy.tryExecute(order, currentPrice);
if (result !== null) {
this._settle(order, result);
} else {
order.status = OrderStatus.PENDING;
if (!this.pendingOrdersBySymbol.has(stock.symbol)) this.pendingOrdersBySymbol.set(stock.symbol, []);
this.pendingOrdersBySymbol.get(stock.symbol).push(order);
this.feed.subscribe(stock.symbol, this); // re-checked on future price ticks
}
return order;
} finally {
account.lock.release();
}
}
onPriceUpdate(symbol, newPrice) {
const pending = this.pendingOrdersBySymbol.get(symbol);
if (!pending) return;
const stillPending = [];
for (const order of pending) {
if (order.status !== OrderStatus.PENDING) continue;
order.account.lock.acquire();
try {
const strategy = order.type === OrderType.MARKET ? this.marketStrategy : this.limitStrategy;
const result = strategy.tryExecute(order, newPrice);
if (result !== null) this._settle(order, result);
else stillPending.push(order);
} finally {
order.account.lock.release();
}
}
this.pendingOrdersBySymbol.set(symbol, stillPending);
}
cancelOrder(orderId) {
const order = this.ordersById.get(orderId);
if (!order) throw new OrderNotFoundException(`No such order: ${orderId}`);
order.account.lock.acquire();
try {
if (order.status !== OrderStatus.PENDING) throw new InvalidOrderStateException('Order not cancellable');
if (order.getSide() === Side.BUY) order.account.release(order.reservedEstimate);
else order.account.portfolio.releaseShares(order.stock, order.quantity);
order.status = OrderStatus.CANCELLED;
} finally {
order.account.lock.release();
}
}
// Must be called while holding order.account.lock.
_settle(order, executionPrice) {
const account = order.account;
if (order.getSide() === Side.BUY) {
account.release(order.reservedEstimate);
account.withdraw(executionPrice * order.quantity);
account.portfolio.addHolding(order.stock, order.quantity);
} else {
account.portfolio.settle(order.stock, order.quantity, Side.SELL); // removes reserved shares
account.deposit(executionPrice * order.quantity);
}
order.status = OrderStatus.EXECUTED;
}
}
// ---------- Demo ----------
function main() {
const appl = new Stock('AAPL', 150.0);
const stocks = new Map([['AAPL', appl]]);
const feed = new SimpleMarketDataFeed(stocks);
const broker = new Broker(feed);
const account = new Account('A1', 10000.0);
broker.addAccount(account);
const buy = broker.placeOrder(account, appl, Side.BUY, 10, OrderType.MARKET, 0);
console.log(`Market buy status=${buy.status} cash=${account.cashBalance} holdings=${account.portfolio.holdingOf('AAPL')}`);
const limitSell = broker.placeOrder(account, appl, Side.SELL, 5, OrderType.LIMIT, 160.0);
console.log(`Limit sell status=${limitSell.status}`);
feed.updatePrice('AAPL', 161.0); // Observer push triggers re-check
console.log(`After price tick, limit sell status=${limitSell.status} cash=${account.cashBalance}`);
const toCancel = broker.placeOrder(account, appl, Side.SELL, 2, OrderType.LIMIT, 500.0);
broker.cancelOrder(toCancel.id);
console.log(`Cancelled order status=${toCancel.status} holdings=${account.portfolio.holdingOf('AAPL')}`);
// Sequential "race": both attempts guarded by the same lock discipline as Java/C++;
// the second is correctly rejected because the first already consumed all shares.
const raceAccount = new Account('A2', 5000.0);
broker.addAccount(raceAccount);
broker.placeOrder(raceAccount, appl, Side.BUY, 3, OrderType.MARKET, 0);
const trySell = () => {
try {
broker.placeOrder(raceAccount, appl, Side.SELL, 3, OrderType.MARKET, 0);
} catch (e) {
console.log('Rejected as expected:', e.message);
}
};
trySell();
trySell();
console.log(`Race account final holdings=${raceAccount.portfolio.holdingOf('AAPL')} (never negative)`);
}
if (require.main === module) main();
module.exports = {
Account, Portfolio, Stock, Order, BuyOrder, SellOrder,
MarketExecutionStrategy, LimitExecutionStrategy, SimpleMarketDataFeed, Broker,
Side, OrderType, OrderStatus,
};
// Problem 21: Stock Brokerage System
// Patterns: Strategy (ExecutionStrategy), Observer (MarketDataFeed -> Broker), Facade (Broker)
// Concurrency note: this synchronous demo runs on a single thread, so there is no real data
// race to guard against here -- but we still model a per-account Lock explicitly (mirroring
// the Java/C++ versions' mutex) so the *shape* of the critical section (reserve -> tryExecute
// -> settle, all atomic w.r.t. one account) is structurally faithful and would compose
// correctly if ported to a worker/thread-based environment.
class InsufficientFundsException extends Error {}
class InsufficientHoldingsException extends Error {}
class InvalidOrderStateException extends Error {}
class OrderNotFoundException extends Error {}
enum Side { BUY = 'BUY', SELL = 'SELL' }
enum OrderType { MARKET = 'MARKET', LIMIT = 'LIMIT' }
enum OrderStatus { PENDING = 'PENDING', EXECUTED = 'EXECUTED', CANCELLED = 'CANCELLED' }
// Trivial synchronous lock: acquire/release bracket the critical section explicitly,
// keeping the code shape identical to the Java/C++ mutex-guarded versions.
class Lock {
acquire(): void { /* no-op: single-threaded event loop, but marks the critical section */ }
release(): void { /* no-op */ }
}
// ---------- Stock ----------
class Stock {
constructor(public symbol: string, public currentPrice: number) {}
updatePrice(newPrice: number): void { this.currentPrice = newPrice; }
}
// ---------- Portfolio (owned by Account) ----------
class Portfolio {
private holdings = new Map<string, number>();
private reserved = new Map<string, number>();
reserveShares(stock: Stock, qty: number): void {
const owned = this.holdings.get(stock.symbol) || 0;
const alreadyReserved = this.reserved.get(stock.symbol) || 0;
if (owned - alreadyReserved < qty) throw new InsufficientHoldingsException(`Not enough shares of ${stock.symbol}`);
this.reserved.set(stock.symbol, alreadyReserved + qty);
}
releaseShares(stock: Stock, qty: number): void {
this.reserved.set(stock.symbol, (this.reserved.get(stock.symbol) || 0) - qty);
}
addHolding(stock: Stock, qty: number): void {
this.holdings.set(stock.symbol, (this.holdings.get(stock.symbol) || 0) + qty);
}
// Removes settled (sold) shares from both holdings and their reservation.
settle(stock: Stock, qty: number, side: Side): void {
if (side === Side.SELL) {
this.holdings.set(stock.symbol, (this.holdings.get(stock.symbol) || 0) - qty);
this.reserved.set(stock.symbol, (this.reserved.get(stock.symbol) || 0) - qty);
}
}
holdingOf(symbol: string): number { return this.holdings.get(symbol) || 0; }
}
// ---------- Account (owns Portfolio) ----------
class Account {
cashBalance: number;
reservedCash = 0;
readonly portfolio = new Portfolio();
readonly lock = new Lock(); // guards reserve/execute/settle atomicity
constructor(public id: string, initialCash: number) { this.cashBalance = initialCash; }
reserve(amount: number): void {
if (this.cashBalance - this.reservedCash < amount) throw new InsufficientFundsException(`Insufficient funds in ${this.id}`);
this.reservedCash += amount;
}
release(amount: number): void { this.reservedCash = Math.max(0, this.reservedCash - amount); }
deposit(amount: number): void { this.cashBalance += amount; }
withdraw(amount: number): void {
if (this.cashBalance < amount) throw new InsufficientFundsException(`Insufficient funds in ${this.id}`);
this.cashBalance -= amount;
}
}
// ---------- Order (abstract) ----------
abstract class Order {
reservedEstimate = 0;
status: OrderStatus = OrderStatus.PENDING;
constructor(
public id: string, public account: Account, public stock: Stock,
public quantity: number, public type: OrderType, public limitPrice: number,
) {}
abstract getSide(): Side;
cancel(): void {
if (this.status !== OrderStatus.PENDING) throw new InvalidOrderStateException('Order not cancellable');
this.status = OrderStatus.CANCELLED;
}
}
class BuyOrder extends Order { getSide(): Side { return Side.BUY; } }
class SellOrder extends Order { getSide(): Side { return Side.SELL; } }
// ---------- ExecutionStrategy ----------
interface ExecutionStrategy {
tryExecute(order: Order, currentPrice: number): number | null;
}
class MarketExecutionStrategy implements ExecutionStrategy {
tryExecute(_order: Order, currentPrice: number): number | null { return currentPrice; } // always executes
}
class LimitExecutionStrategy implements ExecutionStrategy {
tryExecute(order: Order, currentPrice: number): number | null {
const conditionMet = order.getSide() === Side.BUY ? currentPrice <= order.limitPrice : currentPrice >= order.limitPrice;
return conditionMet ? currentPrice : null;
}
}
// ---------- MarketDataFeed ----------
interface PriceListener {
onPriceUpdate(symbol: string, newPrice: number): void;
}
interface MarketDataFeed {
getPrice(symbol: string): number;
subscribe(symbol: string, listener: PriceListener): void;
}
class SimpleMarketDataFeed implements MarketDataFeed {
private listeners = new Map<string, PriceListener[]>();
constructor(private stocks: Map<string, Stock>) {}
getPrice(symbol: string): number { return this.stocks.get(symbol)!.currentPrice; }
subscribe(symbol: string, listener: PriceListener): void {
if (!this.listeners.has(symbol)) this.listeners.set(symbol, []);
const ls = this.listeners.get(symbol)!;
if (!ls.includes(listener)) ls.push(listener);
}
updatePrice(symbol: string, newPrice: number): void {
this.stocks.get(symbol)!.updatePrice(newPrice);
for (const l of this.listeners.get(symbol) || []) l.onPriceUpdate(symbol, newPrice); // push, don't poll
}
}
// ---------- Broker (Facade) ----------
class Broker implements PriceListener {
private accounts = new Map<string, Account>();
private ordersById = new Map<string, Order>();
private pendingOrdersBySymbol = new Map<string, Order[]>();
private marketStrategy = new MarketExecutionStrategy();
private limitStrategy = new LimitExecutionStrategy();
private nextOrderId = 1;
constructor(private feed: MarketDataFeed) {}
addAccount(account: Account): void { this.accounts.set(account.id, account); }
deposit(account: Account, amount: number): void { account.deposit(amount); }
withdraw(account: Account, amount: number): void { account.withdraw(amount); }
placeOrder(account: Account, stock: Stock, side: Side, quantity: number, type: OrderType, limitPrice: number): Order {
account.lock.acquire(); // guard reserve + execution-attempt + settle atomically
try {
const strategy: ExecutionStrategy = type === OrderType.MARKET ? this.marketStrategy : this.limitStrategy;
const currentPrice = this.feed.getPrice(stock.symbol);
const id = `ORD-${this.nextOrderId++}`;
let order: Order;
if (side === Side.BUY) {
const estimatedCost = quantity * (type === OrderType.MARKET ? currentPrice : limitPrice);
account.reserve(estimatedCost); // throws InsufficientFundsException
order = new BuyOrder(id, account, stock, quantity, type, limitPrice);
order.reservedEstimate = estimatedCost;
} else {
account.portfolio.reserveShares(stock, quantity); // throws InsufficientHoldingsException
order = new SellOrder(id, account, stock, quantity, type, limitPrice);
}
this.ordersById.set(order.id, order);
const result = strategy.tryExecute(order, currentPrice);
if (result !== null) {
this.settle(order, result);
} else {
order.status = OrderStatus.PENDING;
if (!this.pendingOrdersBySymbol.has(stock.symbol)) this.pendingOrdersBySymbol.set(stock.symbol, []);
this.pendingOrdersBySymbol.get(stock.symbol)!.push(order);
this.feed.subscribe(stock.symbol, this); // re-checked on future price ticks
}
return order;
} finally {
account.lock.release();
}
}
onPriceUpdate(symbol: string, newPrice: number): void {
const pending = this.pendingOrdersBySymbol.get(symbol);
if (!pending) return;
const stillPending: Order[] = [];
for (const order of pending) {
if (order.status !== OrderStatus.PENDING) continue;
order.account.lock.acquire();
try {
const strategy: ExecutionStrategy = order.type === OrderType.MARKET ? this.marketStrategy : this.limitStrategy;
const result = strategy.tryExecute(order, newPrice);
if (result !== null) this.settle(order, result);
else stillPending.push(order);
} finally {
order.account.lock.release();
}
}
this.pendingOrdersBySymbol.set(symbol, stillPending);
}
cancelOrder(orderId: string): void {
const order = this.ordersById.get(orderId);
if (!order) throw new OrderNotFoundException(`No such order: ${orderId}`);
order.account.lock.acquire();
try {
if (order.status !== OrderStatus.PENDING) throw new InvalidOrderStateException('Order not cancellable');
if (order.getSide() === Side.BUY) order.account.release(order.reservedEstimate);
else order.account.portfolio.releaseShares(order.stock, order.quantity);
order.status = OrderStatus.CANCELLED;
} finally {
order.account.lock.release();
}
}
// Must be called while holding order.account.lock.
private settle(order: Order, executionPrice: number): void {
const account = order.account;
if (order.getSide() === Side.BUY) {
account.release(order.reservedEstimate);
account.withdraw(executionPrice * order.quantity);
account.portfolio.addHolding(order.stock, order.quantity);
} else {
account.portfolio.settle(order.stock, order.quantity, Side.SELL); // removes reserved shares
account.deposit(executionPrice * order.quantity);
}
order.status = OrderStatus.EXECUTED;
}
}
// ---------- Demo ----------
function main(): void {
const appl = new Stock('AAPL', 150.0);
const stocks = new Map<string, Stock>([['AAPL', appl]]);
const feed = new SimpleMarketDataFeed(stocks);
const broker = new Broker(feed);
const account = new Account('A1', 10000.0);
broker.addAccount(account);
const buy = broker.placeOrder(account, appl, Side.BUY, 10, OrderType.MARKET, 0);
console.log(`Market buy status=${buy.status} cash=${account.cashBalance} holdings=${account.portfolio.holdingOf('AAPL')}`);
const limitSell = broker.placeOrder(account, appl, Side.SELL, 5, OrderType.LIMIT, 160.0);
console.log(`Limit sell status=${limitSell.status}`);
feed.updatePrice('AAPL', 161.0); // Observer push triggers re-check
console.log(`After price tick, limit sell status=${limitSell.status} cash=${account.cashBalance}`);
const toCancel = broker.placeOrder(account, appl, Side.SELL, 2, OrderType.LIMIT, 500.0);
broker.cancelOrder(toCancel.id);
console.log(`Cancelled order status=${toCancel.status} holdings=${account.portfolio.holdingOf('AAPL')}`);
// Sequential "race": both attempts guarded by the same lock discipline as Java/C++;
// the second is correctly rejected because the first already consumed all shares.
const raceAccount = new Account('A2', 5000.0);
broker.addAccount(raceAccount);
broker.placeOrder(raceAccount, appl, Side.BUY, 3, OrderType.MARKET, 0);
const trySell = () => {
try {
broker.placeOrder(raceAccount, appl, Side.SELL, 3, OrderType.MARKET, 0);
} catch (e) {
console.log('Rejected as expected:', (e as Error).message);
}
};
trySell();
trySell();
console.log(`Race account final holdings=${raceAccount.portfolio.holdingOf('AAPL')} (never negative)`);
}
main();
export {
Account, Portfolio, Stock, Order, BuyOrder, SellOrder,
ExecutionStrategy, MarketExecutionStrategy, LimitExecutionStrategy,
MarketDataFeed, SimpleMarketDataFeed, Broker, Side, OrderType, OrderStatus,
};