Design a Stock Trading App — System Design Interview Question
How would you design a Groww/Zerodha-like platform that handles live stock prices, thousands of orders per second, failures, and massive traffic?

Design a Stock Trading App — System Design Interview Question
“Design a stock trading app.”
Sounds like a straightforward system design interview question.
You might start with an API, a database, and a couple of services. Users can see stock prices and place buy or sell orders.
But an interviewer can break that design very quickly.
How will you handle millions of price updates?
Would you use polling, WebSockets, or Server-Sent Events?
Why do you need your own order database when the exchange already stores orders?
What happens if the exchange accepts an order but our response never reaches us?
How do you prevent our system from overwhelming the exchange during a market spike?
These questions are where the real system design begins.
So instead of jumping straight into a giant architecture diagram, let’s build the system step by step — starting with the requirements and traffic estimates, and then figuring out how price updates and order processing should be designed differently.
First, What Are We Actually Building?
Let’s imagine we are building something similar to a stock broker such as Groww or Zerodha.
One important clarification first:
We are not building the stock exchange.
The exchange already exists.
It could be NSE, BSE, or another exchange.
The exchange is responsible for things like matching buy and sell orders.
Our responsibility is to build the broker platform sitting between the user and the exchange.
So the simplified flow looks like this:
User
↓
Stock Broker
↓
Stock ExchangeThe interesting question is:
What happens inside the Stock Broker?
That’s what we need to design.
Step 1: Define the Requirements
Before drawing boxes and arrows, let’s decide what our system actually needs to do.
This is one of the biggest mistakes people make in system design interviews.
They start drawing:
API Gateway → Kafka → Redis → Database → Kuberneteswithin the first two minutes.
But why are those components needed?
We don’t know yet.
So let’s start with requirements.
Functional Requirements
Our application should allow users to:
See the current stock price.
See some historical/intraday price data.
Buy stocks.
Sell stocks.
Place market orders.
Place limit orders.
See their order history and status.
That’s enough for our first version.
We can leave things like advanced charts, portfolio analytics, mutual funds, options, and notifications for another discussion.
Step 2: What About Non-Functional Requirements?
This is where the problem becomes interesting.
A stock broker deals with financial transactions.
So we need to think carefully about consistency, availability, latency and scale.
But here’s the catch:
Not every part of our system needs the same guarantees.
Consider these two situations.
Situation 1
You open the app and look at the price of Infosys.
The price displayed is:
₹1,742
But the actual latest price is:
₹1,743
Is that ideal?
No.
Is the entire system broken?
Probably not.
We can tolerate a small amount of delay in price updates.
Situation 2
You place an order.
Your account is charged.
But the order disappears.
That’s unacceptable.
So we can make an important distinction:
Price Data
→ High availability
→ Small amount of staleness is acceptable
Order Processing
→ Strong consistency
→ Losing an order is NOT acceptableThis distinction will influence almost every architectural decision we make later.
Step 3: How Much Traffic Are We Talking About?
Now let’s do a quick back-of-the-envelope calculation.
Suppose our platform has:
100 million users.
Let’s assume around 10% are active on a typical day.
That gives us:
100M × 10%
= 10M daily active usersNow assume each active user checks around 10 stocks and does that around 20 times a day.
That gives:
10M users
× 10 stocks
× 20 checks
= 2 billion price-related requests/dayThat’s a lot.
Now let’s assume peak traffic can be around 5× the average.
We don’t need perfect mathematical precision in an interview. We’re trying to understand the order of magnitude.
The important observation is this:
Looking at prices creates much more traffic than actually placing orders.
People may continuously watch stocks.
They don’t continuously buy and sell every second.
So our system is largely read-heavy.
The rough numbers used in this design come to around:
Price traffic:
~20K QPS at peak
Order traffic:
~2K QPS at peakThese are interview assumptions, not actual traffic numbers for any particular broker.
And this gives us our first major architectural insight:
The price system and order system should not be designed exactly the same way.
Step 4: Start With the API Gateway
Every request from our mobile/web application can first pass through an API Gateway.
Client
↓
API Gateway
↓
Internal ServicesWhy?
Because this is a financial application.
We need to handle things like:
Authentication
Rate limiting
Request validation
Fraud/security checks
Routing
We don’t want every internal service to implement authentication independently.
The gateway gives us a common entry point.
For example:
GET /api/v1/stock/price?symbol=INFYor:
POST /api/v1/orderBoth can first pass through the gateway.
Step 5: Designing the Stock Price System
Let’s start with the easier-looking problem.
A user opens the Infosys page.
They want to see:
INFY
₹1,742.50Where does that price come from?
The stock exchange.
So we might initially think:
Client
↓
API Gateway
↓
Price Service
↓
ExchangeBut there’s a problem.
What if one million users ask:
“What’s the current price of INFY?”
Do we send one million requests to the exchange?
Absolutely not.
The exchange itself is a heavily loaded external system.
We need to protect it.
Step 6: Create an Exchange Gateway
Let’s introduce a dedicated component:
Exchange Gateway Processor
Its responsibility is to communicate with the external exchange.
Now our architecture becomes:
┌───────────────┐
│ Stock Exchange│
└───────┬───────┘
│
↓
┌─────────────────────┐
│ Exchange Gateway │
│ Processor │
└──────────┬──────────┘
│
↓
Price Service
│
↓
ClientThis gives us an important boundary.
Our internal services don’t need to directly communicate with an external exchange.
The exchange gateway handles that communication.
Step 7: Don’t Keep Polling the Exchange
Here’s another problem.
We need updated stock prices.
One simple solution is polling.
For example:
Every 1 second:
"Hey exchange,
what is the current price of INFY?"Then one second later:
"Hey exchange,
what is the current price now?"And again.
And again.
At scale, this becomes wasteful.
Instead, if the exchange provides a streaming mechanism, our exchange gateway can establish a persistent connection and subscribe to stock-price updates.
Conceptually:
Stock Exchange
│
│ Price Updates
↓
Exchange GatewayNow when the price changes, the exchange can push the update to us.
We don’t need to keep asking:
“Anything new?”
This is much more efficient.
Step 8: Where Do We Store Price History?
Now imagine a user opens the app at 11 AM.
The market opened at 9:15 AM.
They don’t want only the latest price.
They may want to see:
9:15 ₹1,720
9:30 ₹1,725
10:00 ₹1,730
10:30 ₹1,738
11:00 ₹1,742So our exchange gateway can store incoming price updates.
For this type of data, a time-series database is a reasonable choice.
For example, something based on PostgreSQL such as TimescaleDB.
The basic idea is:
timestamp | symbol | price
----------|--------|------
09:15:01 | INFY | 1720
09:15:03 | INFY | 1721
09:15:05 | INFY | 1720
...The important part is that the data is naturally organized around time.
That makes queries such as:
“Give me INFY’s price history between 9:15 and 11:00”
much more natural.
Step 9: How Does the Client Get Live Prices?
Now we have another question.
Suppose I am already looking at the Infosys page.
The price changes from:
₹1,742 → ₹1,743.
How does my browser/app know?
One option is polling.
Client → "Price?"
Server → ₹1742
Client → "Price?"
Server → ₹1743
Client → "Price?"
Server → ₹1744Again, that’s wasteful.
We can push updates from the server.
This is where Server-Sent Events (SSE) become useful.
The client establishes a persistent connection:
Client
│
│ Subscribe to INFY
↓
Price ServiceThen the server can continuously send updates:
INFY ₹1742
INFY ₹1743
INFY ₹1741
INFY ₹1745The client doesn’t need to repeatedly ask.
SSE vs WebSockets
At this point, someone will probably ask:
“Why not WebSockets?”
That’s a perfectly valid question.
WebSockets could work.
But think about our requirement.
The client mainly wants to say:
“Give me updates for this stock.”
After that, most of the communication is:
Server → ClientThat’s exactly the type of communication SSE is designed to handle.
WebSockets are especially useful when we need continuous two-way communication:
Client ↔ ServerFor example, a chat application is a natural WebSocket use case.
For our price feed, SSE can be simpler.
But this is not a rule saying:
“Never use WebSockets for stock prices.”
The better engineering principle is:
Choose the communication mechanism based on the communication pattern.
Step 10: The Complete Price Flow
Now let’s combine what we have.
Stock Exchange
│
Price Updates
↓
Exchange Gateway Processor
│
┌───────┴───────┐
↓ ↓
Time-Series DB Redis Pub/Sub
│ │
↓ ↓
Price History Price Service
│
↓
SSE
│
↓
ClientThere are still details we can optimize, but the basic flow is becoming clear.
Step 11: Now Let’s Handle the Important Part — Orders
Price viewing is mostly a read-heavy problem.
Orders are different.
Suppose I send:
POST /api/v1/orderwith:
{
"symbol": "INFY",
"side": "BUY",
"orderType": "LIMIT",
"quantity": 10,
"price": 1740
}We need to make sure this order is not lost.
So let’s create another service:
Order Management Service (OMS).
Now:
Client
↓
API Gateway
↓
Order Management ServiceThe OMS becomes responsible for handling buy/sell orders.
Step 12: Why Do We Need Our Own Order Database?
A natural question is:
“The exchange already stores orders. Why do we need another database?”
There are several reasons.
Reason 1: Reduce calls to the exchange
Suppose a user wants to see their last 30 orders.
Why should our application ask the exchange every time?
The exchange is already handling massive amounts of traffic.
We can maintain our own copy.
User
↓
Broker
↓
Order DBMuch easier.
Reason 2: Fast order history
Our database is optimized for our application’s queries.
Reason 3: Analytics
We may later want to answer questions like:
How many orders did users place?
What percentage were cancelled?
Which stocks are most frequently traded?
How many orders were fulfilled?
Reason 4: Reconciliation
This is a very important one.
We need to compare:
What our system thinks happened
with
What the exchange says happened.
This process is called reconciliation.
Step 13: What Should an Order Look Like?
A simple order table could contain:
order_id
user_id
symbol
side
order_type
quantity
price
status
exchange_order_id
created_at
updated_atFor example:
order_id: ORD123
user_id: U456
symbol: INFY
side: BUY
order_type: LIMIT
quantity: 10
price: 1740
status: PENDING
exchange_order_id: NULL
created_at: ...Notice something important:
We have two IDs.
Our Order ID
ORD123This belongs to our system.
Exchange Order ID
Something like:
EX987654This belongs to the exchange.
Initially:
exchange_order_id = NULLbecause the exchange hasn’t accepted the order yet.
Once the exchange accepts it, we store the exchange’s ID.
That ID becomes extremely useful later.
Step 14: Why Use a Relational Database?
Order processing needs strong transactional guarantees.
Imagine an order involves multiple database operations.
We don’t want this:
Operation 1 → SUCCESS
Operation 2 → SUCCESS
Operation 3 → FAILEDand our database is left in some half-completed state.
We want transactional behavior:
Everything succeeds
OR
Everything rolls backThis is where an ACID-compliant relational database such as PostgreSQL is a reasonable choice.
The goal isn’t:
“PostgreSQL is the only database that works.”
The goal is:
Use a storage system that gives us the consistency and transactional guarantees our financial workflow requires.
Step 15: Don’t Send Every Order Directly to the Exchange
Now suppose thousands of users place orders at the same time.
If our OMS directly calls the exchange for every order:
OMS
│
├──→ Exchange
├──→ Exchange
├──→ Exchange
├──→ Exchange
├──→ Exchange
└──→ ...we have very little control over the traffic.
Instead, we can introduce a streaming platform such as Kafka.
Order Management Service
↓
Kafka
↓
Exchange Gateway
↓
Stock ExchangeNow the system is asynchronous.
The OMS can accept and persist the order, then publish an event.
The exchange gateway consumes those events and sends them to the exchange.
Step 16: Kafka Gives Us Something Very Important — Backpressure
Imagine the exchange can safely handle 1,000 requests per second from us.
But suddenly our application receives 5,000 orders per second.
We don’t want to blindly send all 5,000 requests immediately.
Kafka can act as a buffer.
Incoming Orders
↓
Kafka
↓
Controlled Consumption
↓
ExchangeIf the exchange gateway can process only a certain number of orders, the remaining orders can wait in the queue.
This is called backpressure.
And it becomes extremely useful when traffic suddenly spikes.
Step 17: What Happens After We Send the Order?
Suppose Kafka gives this order to the exchange gateway.
The gateway sends it to the exchange.
The exchange responds with:
Exchange Order ID:
EX987654We then update our database:
order_id: ORD123
exchange_order_id: EX987654
status: PENDINGThe order has now been accepted by the exchange.
But wait.
Accepted does not necessarily mean executed.
A limit order might remain pending for minutes or even longer.
For example:
“Buy INFY only when the price reaches ₹1,700.”
If the price never reaches ₹1,700, the order may remain pending.
So how do we know when the status changes?
Step 18: Exchange → Webhook → Our System
Instead of continuously polling the exchange:
"Is my order complete?"
"Is my order complete?"
"Is my order complete?"we can ask the exchange to notify us when the order changes.
For example:
Stock Exchange
↓
Webhook
↓
Exchange Gateway
↓
Order DBThe exchange can call our webhook whenever the order status changes.
For example:
PENDING
↓
PARTIALLY_FILLED
↓
FILLEDor:
PENDING
↓
CANCELLEDThis avoids maintaining unnecessary persistent connections or repeatedly polling for every order.
Step 19: But What If the Order Never Gets Accepted?
Now we hit one of the more interesting failure cases.
Suppose:
Order DB
↓
Kafka
↓
Exchange Gateway
↓
ExchangeEverything looks fine.
But the exchange never returns an exchange order ID.
What do we do?
We can’t simply leave the order sitting there forever.
We need failure handling.
Step 20: Retry Failed Orders
Because we’re using Kafka, we can retry processing.
For example:
Attempt 1 → Failed
Attempt 2 → Failed
Attempt 3 → Failed
Attempt 4 → Failed
Attempt 5 → FailedAfter some number of retries, we can stop retrying automatically.
The event can move to a:
Dead Letter Queue (DLQ)
Kafka
↓
Exchange Gateway
↓
Failed
↓
Retry
↓
Retry
↓
Retry
↓
Dead Letter QueueNow someone or some automated recovery process can investigate it.
The DLQ is not a magical “fix the problem” queue.
It is basically saying:
“We tried. Something went wrong. Don’t keep hammering the same failure forever. Put it somewhere we can investigate and recover safely.”
Step 21: The Exchange Order ID Becomes Very Important
Remember the exchange order ID?
This is one of the reasons we store it.
If we have:
exchange_order_id = EX987654we know that the exchange accepted our order.
If we don’t have one, we need to determine whether:
The request never reached the exchange.
The exchange received it but our response was lost.
The request failed.
Our system failed while processing the response.
This is where reconciliation becomes important.
Because in distributed systems, the hardest problems often happen when:
System A thinks one thing happened, while System B thinks something else happened.
Step 22: Now Let’s Fix the Price-Service Bottleneck
Our price system currently looks something like:
Exchange
↓
Exchange Gateway
↓
Price Service
↓
ClientsBut remember our earlier calculation?
We may have around 20K QPS at peak.
And stock prices can change very frequently.
So the Price Service could become a bottleneck.
One option is to introduce Kafka:
Exchange Gateway
↓
Kafka
↓
Price ServiceBut there’s another problem.
Kafka consumers generally pull messages.
For extremely frequent real-time price updates, we may want a more direct push mechanism.
This is where something like Redis Pub/Sub can help.
Exchange Gateway
↓
Redis Pub/Sub
↓
Price Service
↓
SSE
↓
ClientsThe idea is simple:
When a new price arrives:
INFY → ₹1743the update is published.
Price-service instances subscribed to that symbol can receive it immediately.
This reduces the need for every component to repeatedly pull updates.
Step 23: What About Hot Stocks?
Here’s another interesting problem.
Suppose something major happens to a company.
Millions of users suddenly open that stock.
We now have a hot symbol.
For example:
Normal:
INFY → 5K requests/sec
Suddenly:
INFY → 100K requests/secIf all of those requests land on one server, that server becomes a bottleneck.
So we need to think about load distribution.
We can horizontally scale our services:
Load Balancer
/ | \
↓ ↓ ↓
Price-1 Price-2 Price-3But simply distributing everything equally isn’t always enough.
A very popular symbol may need dedicated capacity.
We can route particularly hot symbols to larger or dedicated instances.
This is one reason a hybrid scaling strategy can make sense.
Step 24: Sharding the Order Database
Our order database can also become huge.
Imagine billions of historical orders.
A single database will eventually become difficult to scale.
We can shard the data.
One possible strategy is to distribute orders based on user ID or order ID.
For example:
Shard 1 → Users A–F
Shard 2 → Users G–M
Shard 3 → Users N–S
Shard 4 → Users T–ZWithin each shard, we can index by timestamp.
Then a query such as:
“Give me this user’s latest 30 orders”
can be served efficiently.
Again, the exact sharding strategy depends on the workload.
The important system-design principle is:
Choose a partition key that matches the queries you expect to run.
Step 25: Don’t Rely Only on Autoscaling
Stock markets have a very interesting traffic pattern.
Traffic isn’t completely random.
There are predictable spikes.
For example:
Market Opens
↓
Huge traffic spikeand:
Market Nears Closing
↓
Another traffic spikeSo blindly relying on autoscaling may not be enough.
We can keep some infrastructure pre-provisioned.
Think of it like keeping extra counters open before a huge crowd arrives.
You don’t wait for the crowd to arrive and then start building the counters.
You prepare beforehand.
For predictable peak periods, having warm capacity can reduce latency and improve reliability.
Step 26: Reduce Latency
There is one more thing we care about heavily:
Latency.
For a trading platform, every extra network hop matters.
One strategy is to keep our infrastructure geographically close to the exchange infrastructure.
Conceptually:
Our Servers
│
│ Low-latency connection
↓
Exchange InfrastructureThe closer the systems are, the less network latency we generally introduce.
For latency-sensitive systems, infrastructure placement becomes part of the architecture.
The Final Architecture
Let’s put everything together.
┌────────────────────┐
│ Stock Exchange │
└─────────┬──────────┘
│
Price / Orders
│
↓
┌──────────────────────────┐
│ Exchange Gateway │
│ Processor │
└────────────┬─────────────┘
│
┌───────────────┴────────────────┐
│ │
↓ ↓
Time-Series DB Kafka
│ │
↓ ↓
Price History Order Processing
│
↓
Order Management
│
↓
Order DB
Client
│
↓
API Gateway
│
┌────┴───────────────┐
│ │
↓ ↓
Price Service Order Management
│
↓
Redis Pub/Sub
│
↓
SSE
│
↓
Client
The architecture may look complicated now.
But notice something important.
We didn’t start with this diagram.
We arrived here by asking simple questions.
Let’s Trace Two Requests
Request 1: “Show Me the Latest Price”
Client
↓
API Gateway
↓
Price Service
↓
Redis Pub/Sub
↓
SSE
↓
ClientMeanwhile, price updates are coming from:
Exchange
↓
Exchange Gateway
↓
Redis Pub/Sub
↓
Price Service
↓
SSE
↓
ClientHistorical data can come from:
Price Service
↓
Time-Series DBRequest 2: “Buy 10 INFY”
Client
↓
API Gateway
↓
Order Management Service
↓
Order DB
↓
Kafka
↓
Exchange Gateway
↓
Stock ExchangeThen:
Stock Exchange
↓
Webhook
↓
Exchange Gateway
↓
Order DB
↓
Updated StatusNow the two flows make sense.
The Most Important Design Decision
If you remember only one thing from this entire system design, remember this:
Don’t design every part of the system with the same consistency and availability requirements.
For price viewing:
Availability > Perfect consistencyA slightly stale price may be acceptable.
For order processing:
Consistency > AvailabilityA missing or incorrectly processed order is not acceptable.
This one decision explains why the architecture has different components and different strategies for different flows.
What Would Happen During a Huge Market Spike?
Let’s say the market suddenly crashes.
Millions of users open their apps.
Everyone wants to see the same few stocks.
At the same time, users start placing orders.
Our architecture needs to handle both situations differently.
Price traffic
We can:
Use horizontally scaled price services.
Use Redis Pub/Sub for pushing updates.
Keep historical data in time-series storage.
Allow slightly stale data when necessary.
Prioritize important/high-volume symbols.
Use dedicated capacity for hot symbols.
Order traffic
We can:
Persist orders in a transactional database.
Put orders through Kafka.
Control the rate at which orders reach the exchange.
Retry failed processing.
Use a DLQ for persistent failures.
Track exchange order IDs.
Reconcile our data with the exchange.
This is much safer than simply throwing more servers at the problem.
A Few Interview Questions You Should Expect
Once you present a design like this, an interviewer can easily start digging deeper.
For example:
“Why Kafka?”
Because we need asynchronous processing, buffering and backpressure between our system and the external exchange.
“Why not directly call the exchange?”
Because sudden traffic spikes from our users could overload our integration and because asynchronous processing gives us better control.
“Why SSE instead of WebSockets?”
Because the price-feed use case is primarily server-to-client communication. WebSockets could still work, but may be unnecessary if two-way communication isn’t required.
“Why PostgreSQL?”
Because order processing benefits from transactional and consistency guarantees.
“Why a time-series database?”
Because price data is naturally organized around timestamps and historical time-range queries are common.
“Why store orders locally if the exchange already stores them?”
For faster queries, analytics, reduced exchange dependency and reconciliation.
“What happens if the exchange doesn’t respond?”
Retry, track the state carefully, and eventually move failed events to a DLQ for recovery/investigation.
“What if one stock becomes extremely popular?”
Scale horizontally, use appropriate partitioning and give hot symbols dedicated capacity when necessary.
And This Is Where System Design Gets Interesting
At first, designing a stock trading application sounds like a simple problem.
You might think:
User
↓
API
↓
DatabaseDone.
But real systems aren’t difficult because of the number of boxes.
They’re difficult because things go wrong.
What if the exchange is slow?
What if Kafka is full?
What if our database is unavailable?
What if the response from the exchange is lost?
What if we send the same order twice?
What if millions of users suddenly request the same stock?
What if our system says an order is pending while the exchange says it was executed?
These are the questions that turn a basic architecture into a real system design problem.
From Tech By Neha Gupta
👏 Enjoyed the article? Don’t forget to leave a clap.
💬 Have thoughts or questions? Share them in the comments.