List the key functional requirements for the system (Ask the AI for hints if stuck)...
List the key non-functional requirements (performance, scalability, reliability, etc.)...
Estimate the scale of the system. Consider daily active users, read/write ratio, storage requirements, bandwidth, and any relevant QPS calculations...
Assuming that we have 1 million new events across several regions each second, and 100 million people are refreshing the counts simultaneously in peak hours.
Peak write QPS = 1 million
Peak read QPS = 100 million
For storage, we don't need to store all the increment/decrement events, so we can later do an audit review, as well as the distributed counters. The major storage is all the events.
Assuming each event takes 1kb to store, and on average throughout the day, each second takes in 100 thousand events.
Storage needed per day = 100 k events per second * 100k seconds per day * 1kb = 10 billion * 1kb = 10TB of additional storage per day. For counters that have expired and stopped, we put the events storage to cold storage like S3.
For network bandwidth, in peak hours, peak read QPS = 100 million. Assuming that we transfer 1kb per request per second, peak network bandwidth is roughly:
1kb * 100 million = 100Gb per second
Define the APIs expected from the system. This is your chance to analyze and define the read and write paths so that you can come up with the high-level design...
The APIs for this system is relatively simple, CRUD on the counts:
GET v1/view_counts
POST v1/increment
POST v1/decrement
Describe the overall system architecture. Identify the main components needed to solve the problem end-to-end. Use the diagramming tool to create a block diagram.
In this design, the write path is the path to update counters.
We take in events from the event ingestion source, and the global load balancers redirect traffic to API gateways in different global regions.
For the API gateways, they do:
Then the requests are forwarded to the counter services in each region. The counter service determines the shard for each event for each video, breaking up the count, and publish event on regional Kafkas. Event is like:
event_id, video_id, shard_id, timestamp, increment, decrement
The counters workers servers in different regions process the events. And do 2 things:
On the read path, after load balancers and API gateway, users read updated global counts from their regional redis cluster.
For the redis cluster, both the regional sharded count and global counts are cached. For hot videos, we use below strategies:
Define the data model. Identify the main entities, their attributes, and relationships. Consider the choice of database type (SQL vs NoSQL) and justify your decision based on access patterns...
In this system, there are three types of data to store:
Each increment/decrement request is first written to Kafka.
Example event:
{
event_id: UUID,
video_id: UUID,
shard_id: int,
delta: +1 / -1,
timestamp: Timestamp
}
Kafka acts as the durable event log.
We partition events using:
(video_id, shard_id)
This guarantees that all events for the same physical counter shard are processed in order by a single consumer within a consumer group.
For very popular videos, we divide the logical counter into multiple shards:
video_123:shard_0
video_123:shard_1
...
video_123:shard_N
This prevents a single Kafka partition or Redis key from becoming a bottleneck.
Shard workers consume events from Kafka and apply them to regional Redis using atomic INCR / DECR operations.
The key can be modeled as:
key:
counter:{video_id}:{shard_id}
value:
count
For example:
counter:video_123:shard_3 = 125034
Redis is used because:
Redis is not the only durable source of truth. Kafka retains the event history required to reconstruct state after failures.
Periodically, for example every 5 seconds, the current Redis shard state is checkpointed into Cassandra.
The Cassandra table can be modeled as:
table counter_checkpoints {
video_id: UUID,
shard_id: int,
count: bigint,
kafka_partition: int,
last_kafka_offset: bigint,
checkpointed_at: Timestamp,
PRIMARY KEY ((video_id, shard_id))
}
The partition key is:
(video_id, shard_id)
This distributes shards of a popular video across Cassandra nodes and avoids creating a hot Cassandra partition.
Cassandra is suitable because:
We use a replication factor of 3 across availability zones.
For example:
RF = 3
Write consistency = LOCAL_QUORUM
Read consistency = LOCAL_ONE
A checkpoint write must therefore be acknowledged by 2 of the 3 replicas.
Reads may return a slightly stale checkpoint, which is acceptable because recovery can replay Kafka events after the stored offset.
Recovery works as:
Load latest Cassandra checkpoint
+
Replay Kafka events after last_kafka_offset
=
Reconstructed Redis state
Each region has an aggregator that receives regional counter updates through the aggregation stream.
The aggregator combines regional shard counts into a global count:
global_count =
sum(all positive / net regional shard contributions)
The result is written to a regional Redis cluster:
key:
global_counter:{video_id}
value:
global_count
For example:
global_counter:video_123 = 9823412
This Redis cluster directly serves read traffic.
Reads are therefore:
Client
|
v
Count View Service
|
v
Redis GET global_counter:{video_id}
This provides very low read latency.
For extremely popular videos, the logical counter is sharded at the write layer.
For example:
video_123:
shard_0
shard_1
shard_2
...
shard_63
Each shard can be processed independently by Kafka workers and stored on different Redis nodes.
The aggregator periodically combines these shard values into one materialized global count.
The global Redis value itself is a single key, but because reads are cheap and can be replicated, this is usually less problematic than a hot write key. If one key becomes exceptionally read-heavy, Redis replicas or application-level read replication can absorb the load.
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
In this system, there are three types of data to store:
Each increment/decrement request is first written to Kafka.
Example event:
{
event_id: UUID,
video_id: UUID,
shard_id: int,
delta: +1 / -1,
timestamp: Timestamp
}
Kafka acts as the durable event log.
We partition events using:
(video_id, shard_id)
This guarantees that all events for the same physical counter shard are processed in order by a single consumer within a consumer group.
For very popular videos, we divide the logical counter into multiple shards:
video_123:shard_0
video_123:shard_1
...
video_123:shard_N
This prevents a single Kafka partition or Redis key from becoming a bottleneck.
Shard workers consume events from Kafka and apply them to regional Redis using atomic INCR / DECR operations.
The key can be modeled as:
key:
counter:{video_id}:{shard_id}
value:
count
For example:
counter:video_123:shard_3 = 125034
Redis is used because:
Redis is not the only durable source of truth. Kafka retains the event history required to reconstruct state after failures.
Periodically, for example every 5 seconds, the current Redis shard state is checkpointed into Cassandra.
The Cassandra table can be modeled as:
table counter_checkpoints {
video_id: UUID,
shard_id: int,
count: bigint,
kafka_partition: int,
last_kafka_offset: bigint,
checkpointed_at: Timestamp,
PRIMARY KEY ((video_id, shard_id))
}
The partition key is:
(video_id, shard_id)
This distributes shards of a popular video across Cassandra nodes and avoids creating a hot Cassandra partition.
Cassandra is suitable because:
We use a replication factor of 3 across availability zones.
For example:
RF = 3
Write consistency = LOCAL_QUORUM
Read consistency = LOCAL_ONE
A checkpoint write must therefore be acknowledged by 2 of the 3 replicas.
Reads may return a slightly stale checkpoint, which is acceptable because recovery can replay Kafka events after the stored offset.
Recovery works as:
Load latest Cassandra checkpoint
+
Replay Kafka events after last_kafka_offset
=
Reconstructed Redis state
Each region has an aggregator that receives regional counter updates through the aggregation stream.
The aggregator combines regional shard counts into a global count:
global_count =
sum(all positive / net regional shard contributions)
The result is written to a regional Redis cluster:
key:
global_counter:{video_id}
value:
global_count
For example:
global_counter:video_123 = 9823412
This Redis cluster directly serves read traffic.
Reads are therefore:
Client
|
v
Count View Service
|
v
Redis GET global_counter:{video_id}
This provides very low read latency.
For extremely popular videos, the logical counter is sharded at the write layer.
For example:
video_123:
shard_0
shard_1
shard_2
...
shard_63
Each shard can be processed independently by Kafka workers and stored on different Redis nodes.
The aggregator periodically combines these shard values into one materialized global count.
The global Redis value itself is a single key, but because reads are cheap and can be replicated, this is usually less problematic than a hot write key. If one key becomes exceptionally read-heavy, Redis replicas or application-level read replication can absorb the load.
In this design, the write path is the path to update counters.
We take in events from the event ingestion source, and the global load balancers redirect traffic to API gateways in different global regions.
For the API gateways, they do:
Then the requests are forwarded to the counter services in each region. The counter service determines the shard for each event for each video, breaking up the count, and publish event on regional Kafkas. Event is like:
event_id, video_id, shard_id, timestamp, increment, decrement
The counters workers servers in different regions process the events. And do 2 things:
On the read path, after load balancers and API gateway, users read updated global counts from their regional redis cluster.
For the redis cluster, both the regional sharded count and global counts are cached. For hot videos, we use below strategies:
For each increment/decrement event, we choose one of the video's counter shards. We don't permanently map a video_id to one shard—that would defeat the purpose of sharding a hot counter.
Suppose video_123 has 64 shards:
video_123
|
+-- shard 0
+-- shard 1
+-- ...
+-- shard 63
A clean strategy is:
shard_id = hash(event_id) % num_shards
For example:
event A: hash(A) % 64 = 17
event B: hash(B) % 64 = 42
event C: hash(C) % 64 = 3
event D: hash(D) % 64 = 17
Then the Counter Service publishes:
{
event_id: A,
video_id: video_123,
shard_id: 17,
delta: +1
}
and uses this as the Kafka key:
(video_id, shard_id)
So the flow is:
video_123 +1
|
generate event_id
|
v
hash(event_id) % 64
|
shard 17
|
v
Kafka key=(video_123,17)
|
v
Counter Worker
|
v
Redis INCR video_123:17