Stock Trading App System Design
Functional Requirements
- Users can search for and fetch stock price details.
- The system connects with the stock exchange to fetch stock prices.
- Users should see real-time price updates.
- Users can place buy/sell orders.
- Orders are forwarded to the stock exchange.
- The system supports:
- LIMIT orders
- MARKET orders
- The system does not need to handle:
- Order matching
- Trade execution (handled by the exchange)
- Users can view their past order history.
Non-Functional Requirements
- High availability for:
- Price search
- Price fetch APIs
- Strong consistency for:
- Order placement
- High scalability:
- The system must handle millions of users
- A read-heavy system:
- Price views >> Order placements
Scale Estimation
Users
- Total users: 100 million
- Daily Active Users (DAU) (10%): 10 million
Stock Price Views
- Each user:
- Views 10 stocks
- Refreshes 20 times/day
Total Views:
10M users Γ 10 stocks Γ 20 views = 2 billion views/day = views/day
QPS:
Orders
Assume that 10% of views convert into orders:
20K Γ 10% = 2K orders/sec = ~2K TPS
Peak Load
- Assume 5Γ spike
Price QPS: normal = 20K/sec, peak = 100k/sec
Order TPS: normal = 2K/sec, peak = 10k/sec
APIs
View Stock Prices
Fetch the latest or historical stock prices.
GET /stockPrices
GET /stockPrices?symbol={symbol}&timerange={timerange}
- symbol β The stock symbol (e.g., AAPL, TSLA)
- timerange β The time window (e.g., 1d, 1w, 1m)
Place Order
Place a buy or sell order.
POST /orders
Request Body
{
"orderType": "buy | sell",
"type": "LIMIT | MARKET",
"symbol": "string",
"quantity": number,
"amount": number
}
- LIMIT β Executes at the specified price
- MARKET β Executes at the current market price
View Past Orders
Retrieve the order history.
GET /orders
GET /orders?symbol={symbol}
- symbol β Filters orders by stock symbol
High-Level Design
Here weβll walk through a high-level design (HLD) of a stock trading system that allows users to:
- View stock prices
- Place buy/sell orders
- Track order history
We will focus on the core components and system interactions. Improvements will be discussed in later iterations.
At a high level, the system consists of:
- API Gateway (entry point)
- Price Service (read-heavy operations)
- Order Service (write-heavy operations)
- Stock DB and Order DB (data storage)
- Exchange Gateway (external integration)
- Stock Exchange System (external dependency)

API Gateway
The API Gateway is the single entry point for all client requests.
Responsibilities:
- Route requests to the appropriate services
- Handle:
- View prices
- Place orders
- View orders
It simplifies communication between clients and backend services.
Price Service
This service handles all stock priceβrelated operations.
Responsibilities:
- Fetch stock prices from the exchange (via gateway)
- Store/update price data in the Stock DB
- Serve price queries to users
Why separate it?
- The system is read-heavy.
- Price requests are high in volume and need independent scaling.
Stock DB
The Stock DB stores stock price data:
- stockId
- symbol
- timestamp
- price
Purpose:
- Fast access to price data
- Fewer calls to the external exchange
It acts as a cache as well as a historical store.
Order Management Service
This service handles all order-related business logic.
Responsibilities:
- Create new orders
- Store orders in the Order DB
- Forward orders to the exchange
- Update order status
Order DB
The Order DB stores all order information:
- id
- order_type (buy/sell)
- type (LIMIT/MARKET)
- quantity
- amount
- status
- exchange_order_id
It acts as the source of truth for all orders. The stock exchange maintains its own order ID, so we need to store that in our database as well.
Stock Exchange Gateway
This component integrates with the external stock exchange.
Responsibilities:
- Fetch stock prices
- Forward orders to the exchange
- Receive updates such as the order status and exchange order ID
This layer decouples internal services from the external system.
Stock Exchange System
An external system responsible for:
- Order matching
- Trade execution
- Market data
Our system does not handle the matching logic. Refer to our Stock Exchange System Design for more details.
High-Level Flows
1. View Stock Prices
- The client sends a request to the API Gateway.
- The API Gateway forwards the request to the Price Service.
- The Price Service fetches data from the Stock DB.
- (Optional) If the data is stale, it is fetched from the exchange via the gateway.
- The response is returned to the client.

2. Place Order
- The client sends an order request to the API Gateway.
- The API Gateway forwards the request to the Order Service.
- The Order Service creates the order and stores it in the Order DB.
- The Order Service sends the order to the stock exchange via the Exchange Gateway.
- The exchange processes the order.
3. Order Status Update
- The stock exchange sends order updates.
- The Exchange Gateway receives the updates.
- The gateway forwards each update to the Order Service.
- The Order Service updates the Order DB with:
- status
- exchange_order_id
4. View Past Orders
- The client sends a request to the API Gateway.
- The API Gateway routes the request to the Order Service.
- The Order Service fetches data from the Order DB.
- The response is returned to the client.
Deep Dive - View Price
In the initial high-level design, price fetching was handled synchronously. But there are two ways to get prices from the exchange:
- The gateway polls the exchange.
- The exchange pushes updates (WebSocket/Webhook).
Push-based systems are more efficient and have lower latency. Instead of fetching prices per request, the system can continuously stream price updates and push them to clients.
We introduce a message broker (e.g., Kafka) to decouple components:
- Price updates are produced asynchronously.
- Consumers process updates independently.
- Clients receive real-time updates via streaming.

Price Flow (Step-by-Step)
- The Stock Exchange Gateway receives price updates from the exchange.
- The gateway publishes events to the Price Topic.
- The Price Service consumes events from the topic.
- The Price Service updates the Stock DB.
- The Price Service pushes updates via SSE.
- Clients receive real-time stock prices.
Why Event-Driven?
This approach improves the system in multiple ways:
- Decoupling β Producers and consumers are independent
- Scalability β Services can scale horizontally
- Real-time updates β No polling required
- Reliability β Message broker provides durability
Partitioning Strategy
Partition the price topic using symbol as the key:
- This ensures ordering per stock.
- It enables parallel processing.
Consumer Scaling
- Run multiple instances of the Price Service.
- Each instance consumes a subset of partitions.
This allows the system to handle high throughput.
Idempotency
Duplicate events can occur, so:
- Use timestamps or versioning.
- Ensure updates are idempotent.
Real-Time Streaming
SSE is used to push updates:
- It is efficient for one-way streaming.
- It works well for real-time price updates.
At very large scale, consider WebSockets or a Pub/Sub fan-out layer. By introducing an event-driven architecture, the price flow becomes scalable, real-time, and decoupled. This design avoids polling, handles high throughput efficiently, and provides a strong foundation for production systems.
Next, we can apply similar principles to improve the order flow, which involves stricter consistency requirements.
Deep Dive - Order Processing Flow
The initial order processing design looks simple and intuitive:
Client β API Gateway β Order Management Service β Stock Exchange Gateway β Exchange
It works. Itβs easy to reason about. But under real-world conditions β scale, failures, retries β this design starts to break down.
Problems in the Old Design
-
Tight Coupling to Exchange
Order Service β Exchange Gateway β Exchange
- If the exchange is slow β your API becomes slow.
- If the exchange is down β order placement fails.
- There is no failure isolation.
-
Fully Synchronous Flow
- It waits for the exchange's response
- High latency
- Poor UX
- Risk of timeouts
-
No Event-Driven Layer
- No queue between components
- No replay capability
- Tight coupling
- Limited scalability
-
Weak Order State Model
{ "status": "...", "exchange_order_id": "..." }It is missing:
- Lifecycle states
- Partial fills
- Retry states
- Failure reasons
-
No Reliability Guarantees
- The exchange succeeds but the DB fails β inconsistent state
- The API crashes after sending β unknown outcome
Improved Architecture (Conceptual Shift)
Let's improve on the old design.
New Components
- Order Topic β queue for incoming order events
- Order Status Topic β queue for order lifecycle updates
- Stock Exchange Gateway β an asynchronous layer that interacts with the exchange (with retries)

Flow
-
The client sends an order request.
-
The Order Management Service:
- Saves the order as
PENDING - Publishes an event to the Order Topic
- Saves the order as
-
The Stock Exchange Gateway:
- Consumes from the Order Topic
- Sends the order to the exchange
- Handles retries if needed
-
The exchange responds (sync/webhook).
-
The gateway publishes an update to the Order Status Topic.
-
The Order Management Service:
- Consumes the Order Status Topic
- Updates the order state (
PLACED,FILLED, etc.)
-
The client receives real-time updates (SSE/WebSocket).
-
Background processes:
- Retry failures (with backoff + DLQ)
- Run reconciliation with the exchange
The Dual Write Problem
Even with an event-driven flow, a critical issue remains: dual writes. Here is an example of where it can happen.
In the Order Management Service:
- Save the order to the DB
- Publish an event to the Order Topic
We are writing to two separate systems β Database + Message Broker, and this is dangerous, because these operations are not atomic. Consider the following scenarios:
-
The DB write succeeds, but the event publish fails
β The order exists but never reaches the exchange. -
The event publish succeeds, but the DB write fails
β The order gets executed but is not recorded.
Result: inconsistent system state
Common (But Flawed) Fixes
- Try-catch with retries β still unreliable
- Reordering operations β just shifts the problem
- Manual reconciliation β reactive, not preventive
The Right Approach: Transactional Outbox
Instead of writing to the DB and the broker separately:
- Save the order (
PENDING) - Save the event in an Outbox Table (in the same DB transaction)
Write to the Orders Table + Outbox Table β in a single atomic DB transaction.
- A background worker reads from the Outbox table, publishes to the Order Topic, and marks the event as processed.

Why This Works
- The DB + event write is atomic.
- No event is lost.
- Retries are safe (idempotent publishing).
- Eventual delivery is guaranteed.
Potential Considerations for Improvement
While the design is robust, here are a few things a final production system would need to ensure:
Idempotency: Since Kafka guarantees "at-least-once" delivery, the Order Management Service and Stock Gateway must be idempotent. If they consume the same "Create Order" message twice (due to a rebalance or retry), they must not place duplicate orders or double-update statuses.
The fix is simple: generate a unique idempotency_key at the client, store it in Redis before processing, and include it in every Kafka message. If the same key appears again, return the cached response. Your users will never know the difference.
Dead Letter Queue (DLQ): If the Stock Gateway fails to send an order to the exchange repeatedly, the system needs a DLQ to handle "poison pill" messages so they donβt block the rest of the Order Topic.
Database Scaling: The Stock DB (under Price Service) likely experiences high write throughput. In a final design, this would likely be a time-series database or a read-replica setup to handle the high velocity of price ticks without locking up.
CDC Instead of Polling: The current outbox worker polls the database every second for new events. That's a full second of latency per order, plus unnecessary database load. In trading, a second is an eternityβprices change, opportunities vanish. Replace polling with Debezium. It reads the database transaction log (WAL/binlog) and publishes changes to Kafka instantly. Latency drops from 1000ms to < 10ms. No polling, no wasted queries, and your orders hit the exchange while the price is still relevant.
The final design is a scalable, resilient, event-driven microservices system. It prioritizes data consistency (via Outbox), loose coupling (via Kafka Topics), and real-time capabilities (via SSE), making it suitable for a financial trading platform where reliability and speed are critical.