Active DAU = 100M
Concurrent user = 10% = 10M
Assuming we have an activity recorded every 5 sec / user
Writes/sec = 10*10^6/5= 2M writes/sec
Peak writes = 6M writes/sec
Reads/sec = 3M/sec
Peak reads = 9M reads/sec
Storage: The counter will be 1 counter / entity.
The counter + metadata could be roughly 250bytes.
Total storage for a given day = 100M * 250 bytes = 25GB (however, this is not the right number to scale, for estimating monthly and yearly storage, as it will be dependent of repeat user). We expect this number to grow relatively slowly.
The storage requirement is trivial
POST /v1/increment
POST /v1/decrement
[Both have same body]
Headers:
Authentication: Bearer
Content: "application/json"
Accept: "application/json"
Idempotency-Key: uuid
Body: {
user_id: uuid,
content_id: uuid
}
Response:
Headers:
RateLimit-Limit: 100
RateLimit-RetryAfter: 30
{
user_id: uuid,
content_id: uuid,
curr_count: int
}
Status: 200 (Success), 429 (Rate Limited), 500 (Server Error)
GET /v1/counter/{content_id}
Authentication: Bearer
Content: "application/json"
Accept: "application/json"
Idempotency-Key: uuid
If-Not-Match: uuid
Response:
{
curr_count: int
}
Status: 200 (Success), 429 (Rate Limited), 500 (Server Error)
The clients request is routed through Load Balancer and Gateway (we also check the Rate Balancing here)
Write:
Write Services dumps the request into a Kafka and return 202 Accepted status to the user.
An Aggregator service reads the Kafka topics, aggregates the count and updates the DB.
Kafka topic is partitioned by content_id, so the activities of a particular content is consumed parallel
Read:
Read is primarily served from Redis and then fallsback to Cassandra as a source of truth (Cache aside method)
We can use LRU as a cache invalidation method
Hot Key Handling:
For hot keys, we can partition the kafka topics using content_id + salt. Use locks to upgrade the Cassandra.
Cassandra:
{
content_id: uuid, [PK]
counter: int,
last_updated_at: timestamp
}
Partition Key: (content_id, timestamp)
Redis:
Key: counter:{content_id}
Value: int
Aggregator Table:
{
content_id: uuid,
shard_id: uuid,
bucket: timestamp,
batch: uuid,
delta: int
}
Failure Scenarios:
Handling Hot Keys:
To handle hot keys, we will partition the Kafka by content_id + salt, this way, events can be processed parallelly. In the aggregation table, we can store the current aggregate as content_id:{shard_1}... content_id:{shard_n} -> aggregate and then flush the final data to cassandra using locks on the content id