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 Exchange

The 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 → Kubernetes

within 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:

  1. See the current stock price.

  2. See some historical/intraday price data.

  3. Buy stocks.

  4. Sell stocks.

  5. Place market orders.

  6. Place limit orders.

  7. 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 acceptable

This 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 users

Now 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/day

That’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 peak

These 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 Services

Why?

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=INFY

or:

POST /api/v1/order

Both 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.50

Where does that price come from?

The stock exchange.

So we might initially think:

Client
   ↓
API Gateway
   ↓
Price Service
   ↓
Exchange

But 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
                         │
                         ↓
                       Client

This 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 Gateway

Now 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,742

So 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 → ₹1744

Again, 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 Service

Then the server can continuously send updates:

INFY ₹1742
INFY ₹1743
INFY ₹1741
INFY ₹1745

The 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 → Client

That’s exactly the type of communication SSE is designed to handle.

WebSockets are especially useful when we need continuous two-way communication:

Client ↔ Server

For 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
                              │
                              ↓
                           Client

There 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/order

with:

{
  "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 Service

The 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 DB

Much 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_at

For 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

ORD123

This belongs to our system.

Exchange Order ID

Something like:

EX987654

This belongs to the exchange.

Initially:

exchange_order_id = NULL

because 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 → FAILED

and our database is left in some half-completed state.

We want transactional behavior:

Everything succeeds
       OR
Everything rolls back

This 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 Exchange

Now 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
      ↓
Exchange

If 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:
EX987654

We then update our database:

order_id: ORD123
exchange_order_id: EX987654
status: PENDING

The 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 DB

The exchange can call our webhook whenever the order status changes.

For example:

PENDING
   ↓
PARTIALLY_FILLED
   ↓
FILLED

or:

PENDING
   ↓
CANCELLED

This 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
   ↓
Exchange

Everything 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 → Failed

After 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 Queue

Now 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 = EX987654

we 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
   ↓
Clients

But 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 Service

But 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
      ↓
Clients

The idea is simple:

When a new price arrives:

INFY → ₹1743

the 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/sec

If 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-3

But 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–Z

Within 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 spike

and:

Market Nears Closing
     ↓
Another traffic spike

So 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 Infrastructure

The 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
  ↓
Client

Meanwhile, price updates are coming from:

Exchange
   ↓
Exchange Gateway
   ↓
Redis Pub/Sub
   ↓
Price Service
   ↓
SSE
   ↓
Client

Historical data can come from:

Price Service
   ↓
Time-Series DB

Request 2: “Buy 10 INFY”

Client
   ↓
API Gateway
   ↓
Order Management Service
   ↓
Order DB
   ↓
Kafka
   ↓
Exchange Gateway
   ↓
Stock Exchange

Then:

Stock Exchange
      ↓
   Webhook
      ↓
Exchange Gateway
      ↓
Order DB
      ↓
Updated Status

Now 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 consistency

A slightly stale price may be acceptable.

For order processing:

Consistency > Availability

A 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
 ↓
Database

Done.

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.