For a normal application, a like may look like a very simple operation:
User clicks Like
↓
API
↓
Database
↓
Like Saved
Simple, right?
But imagine a celebrity uploads a post and millions of users start liking it within a few seconds.
Now the problem becomes completely different.
If every like directly hits the database, we could end up with:
1 Like
↓
1 Database Request
1 Million Likes
↓
1 Million Database Requests
At this scale, the database can quickly become the bottleneck.
So how would we design a system that can handle this kind of traffic?
Let's understand it step by step.
1. First, Understand What Happens When You Like a Post
Let's start with the simplest flow.
A user clicks the Like button.
The application sends something like:
POST /posts/123/like
The request goes through the backend:
User
↓
API Gateway
↓
Like Service
↓
Database
The database stores the relationship:
user_id | post_id
101 | 123
102 | 123
103 | 123
This works perfectly when traffic is small.
But now imagine:
Post 123
↓
10 users
↓
1,000 users
↓
100,000 users
↓
1,000,000 users
Suddenly, millions of requests are trying to update the same piece of information.
This is where system design becomes important.
2. Don't Make Every Like a Heavy Database Operation
One of the biggest mistakes would be treating every like as a synchronous database transaction.
Imagine:
User 1 → Database
User 2 → Database
User 3 → Database
User 4 → Database
User 1,000,000 → Database
The database now has to process an enormous number of operations.
Instead, we can separate the user's immediate action from the heavy background processing.
For example:
User
↓
Like API
↓
Redis
↓
Response
↓
Kafka
↓
Background Workers
↓
Database
Now the API doesn't necessarily have to wait for every database operation to finish before responding.
This is where asynchronous processing becomes useful.
3. Use a Message Queue for Massive Traffic
Let's say a post suddenly becomes viral.
Thousands or millions of users may click Like almost simultaneously.
Instead of processing everything immediately:
Like Request
↓
Database
we can use a message broker:
Like Request
↓
Like Service
↓
Kafka
↓
Consumers
↓
Database
A like event could look conceptually like:
{
"userId": 101,
"postId": 123,
"action": "LIKE"
}
Kafka can act as a buffer between the API and the consumers.
So if traffic suddenly increases:
Normal Traffic
↓
Kafka
↓
Consumer
During a traffic spike:
Huge Traffic
↓
Kafka
↓
Consumer
↓
Consumer
↓
Consumer
We can add more consumers to process the events in parallel.
The important idea is:
Don't force the database to process every request at exactly the same moment.»
4. Store Likes Efficiently
Now let's think about the database.
We need to answer questions like:
- Did this user already like the post?
- How many likes does this post have?
- Which users liked this post?
A basic relational design could look like:
POST
----------------
id
caption
created_at
POST_LIKE
----------------
user_id
post_id
created_at
We can create a unique constraint:
(user_id, post_id)
This prevents the same user from liking the same post multiple times.
For example:
user_id | post_id
101 | 123
If user "101" tries to like post "123" again, the system can detect that the relationship already exists.
This is much safer than simply incrementing a counter every time.
5. Don't Count Millions of Rows Every Time
Imagine a post has:
50,000,000 likes
Now someone opens the post.
Would we run:
SELECT COUNT(*)
FROM post_like
WHERE post_id = 123;
for every request?
That can become expensive at huge scale.
Instead, we can maintain a counter:
Post
----------------
id: 123
like_count: 50000000
When a like is processed:
like_count = like_count + 1
But there's another problem.
If millions of users update the same counter simultaneously, we can create contention around that hot record.
This is known as a hot key / hot row problem.
So we need a better strategy.
6. The Hot Post Problem
This is one of the most interesting parts of the design.
Imagine a normal post:
Post A
100 likes
No problem.
But a celebrity posts something:
Post B
10,000,000 likes
Now millions of requests are related to the same "post_id".
That post becomes a hot key.
Instead of having:
Post 123
like_count = 10,000,000
we can distribute the counter.
For example:
Post 123
Counter 1 → 2,500,000
Counter 2 → 2,500,000
Counter 3 → 2,500,000
Counter 4 → 2,500,000
Then:
Total Likes =
Counter 1
+ Counter 2
+ Counter 3
+ Counter 4
This spreads the write load instead of forcing every request to update the same database row.
This technique is often called counter sharding or sharded counters.
7. Use Redis for Fast Reads
Now think about the Instagram feed.
Users don't just like posts.
They continuously open posts and feeds.
For every post, the application may need to display:
❤️ 2.4M likes
We don't necessarily want to query the database every time.
This is where a distributed cache such as Redis can help.
A simplified architecture:
Request
↓
Redis
/ \
Hit Miss
↓ ↓
Return Database
↓
Redis
For example:
post:123:likes = 2400000
Now the application can retrieve the count quickly.
The database remains the source of durable data, while Redis can handle many frequent reads.
8. What Happens When a User Likes a Post?
Let's put the pieces together.
A simplified flow could be:
User
↓
Load Balancer
↓
API Gateway
↓
Like Service
↓
Redis / Fast Path
↓
Kafka
↓
Like Consumers
↓
Database
The user gets a quick response:
❤️ Liked
Meanwhile, background workers process the event.
This is a classic example of event-driven architecture.
The API handles the user interaction.
The event-processing system handles the work that doesn't need to block the user.
9. But What If the User Likes the Post Twice?
This is an important problem.
Imagine:
User clicks Like
↓
Request 1
User clicks Like again quickly
↓
Request 2
If both requests are processed independently, we might accidentally count:
1 User
↓
2 Likes
That's obviously wrong.
So the system needs idempotency.
The idea is simple:
Processing the same operation multiple times should not produce an incorrect final result.
For example, the database can enforce:
UNIQUE(user_id, post_id)
So only one like relationship can exist.
This protects the system even if duplicate requests arrive.
10. What About Unlike?
Now the user presses Unlike.
We have another event:
{
"userId": 101,
"postId": 123,
"action": "UNLIKE"
}
The system processes it accordingly.
Conceptually:
LIKE
↓
Create relationship
↓
Increase counter
UNLIKE
↓
Remove relationship
↓
Decrease counter
But remember:
At large scale, we should carefully design how counters and events are updated so that duplicate or out-of-order events don't corrupt the final count.
This is one reason event processing often needs:
- idempotency
- ordering considerations
- retries
- dead-letter handling
- reconciliation jobs
11. What If Kafka or a Consumer Goes Down?
Distributed systems fail.
We shouldn't design assuming:
“Everything will always work.”
Suppose:
Like API
↓
Kafka
↓
Consumer
The consumer crashes.
Does the like disappear?
It shouldn't.
The event can remain in Kafka until a consumer successfully processes it, depending on the configured delivery and retention strategy.
When the consumer comes back:
Kafka
↓
Consumer
↓
Process pending events
This gives the system much better resilience than directly coupling every request to immediate database processing.
12. Scale the Like Service Horizontally
One Like Service instance won't be enough for massive traffic.
So we run multiple instances:
Load Balancer
↓
┌────────────┼────────────┐
↓ ↓ ↓
Like Service Like Service Like Service
Instance 1 Instance 2 Instance 3
If traffic increases:
3 instances
↓
10 instances
↓
50 instances
This is horizontal scaling.
The service itself should ideally be stateless, so any instance can process a request.
13. Authentication Is Also Important
Before processing a Like request, the system needs to know:
Who is sending this request?
A typical request might contain an authentication token.
For example:
User
↓
API Gateway
↓
Authentication
↓
Like Service
The Like Service can then determine:
userId = 101
postId = 123
and create the relationship.
Authentication also prevents users from simply sending arbitrary requests pretending to be someone else.
14. Monitoring Becomes Critical at This Scale
Imagine you're handling millions of likes.
Something goes wrong.
How do you know?
You need metrics such as:
Likes / Second
API Latency
Error Rate
Kafka Consumer Lag
Redis Hit Rate
Database Load
Queue Size
Failed Events
For example:
Kafka Consumer Lag
↓
Increasing
↓
Consumers are too slow
↓
Add more consumers
Monitoring isn't just about looking at graphs.
It helps engineers understand where the system is becoming a bottleneck.
15. The Complete Architecture
Now let's combine everything.
A simplified Instagram-scale Like System could look like this:
USERS
↓
Load Balancer
↓
API Gateway
↓
Like Service
/ \
↓ ↓
Redis Kafka
↓
┌───────┼───────┐
↓ ↓ ↓
Consumer Consumer Consumer
↓ ↓ ↓
└───────┼───────┘
↓
Database
For the like count:
User
↓
Like API
↓
Counter / Redis
↓
Background Processing
↓
Durable Storage
And for a viral post:
Millions of Likes
↓
Load Balancer
↓
Multiple Like Service Instances
↓
Kafka
↓
Multiple Consumers
↓
Distributed / Sharded Storage
Now the workload is distributed across multiple components instead of hitting one database directly.
So, How Does Instagram Handle Millions of Likes?
The important thing is that there is no single magic technology.
You don't solve millions of likes by simply adding Redis.
You don't solve it by simply adding Kafka.
You don't solve it by simply adding more servers.
The solution comes from combining multiple concepts:
Horizontal Scaling
+
Caching
+
Message Queues
+
Asynchronous Processing
+
Database Optimization
+
Counter Sharding
+
Idempotency
+
Load Balancing
+
Monitoring
Each component solves a different problem.
Load Balancer
Distributes incoming traffic.
API Gateway
Provides a controlled entry point to backend services.
Like Service
Handles the business logic for likes.
Redis
Provides fast access to frequently requested data.
Kafka
Buffers and distributes high-volume events.
Consumers
Process events asynchronously.
Database
Stores durable like relationships.
Sharded Counters
Help distribute updates for extremely popular posts.
Monitoring
Helps engineers detect bottlenecks and failures.
💡 The Biggest Lesson
When you're designing a system for millions of users, don't ask:
“How can I make my database handle millions of requests?"
Ask:
“How can I prevent millions of requests from reaching my database in the first place?"
That's the mindset behind scalable system design.
Instead of:
1 Million Requests
↓
1 Database
we build:
1 Million Requests
↓
Load Balancer
↓
Multiple Services
/ \
↓ ↓
Redis Kafka
↓
Consumers
↓
Database
The goal isn't to make one component infinitely powerful.
The goal is to distribute the workload across the system.