Scalability:
The system must support horizontal scaling to handle high QPS and traffic spikes (e.g., viral videos).
This includes:
Idempotency:
Duplicate events (e.g., retries) must not lead to over-counting.
Latency:
Video
GET /v1/video/{id}/views
GET /v1/video/{id}
→ Returns current aggregated view count
Report
GET /v1/report/video/{id}/views?start={}&end={}&breakdown=age,device,country&channel=web,app&granularity=[hourly/daily/monthly]
When a user view the video
The frontend batches raw events and sends them periodically using regular HTTP requests.
Additionally, it can use navigator.sendBeacon() to flush any remaining events when the user leaves the page.
POST /v1/events, request: {eventObj}
{
events:[
{event}
]
}
In this design, I separate the system into an online serving path and an analytics path.
For the online path, user requests go through the API Gateway, which handles routing, rate limiting, and fraud detection. Then the request goes to the view service, which updates or reads the latest view counts from View DB. This path is optimized for low-latency access to current counts.
For the analytics path, I use a separate report service. View events are streamed through Kafka, processed by Flink, and written into Druid through an aggregated pipeline. This supports near-real-time reporting queries. For longer-term analytics such as hourly, daily, or monthly trends, a Batch service writes results into a dedicated report DB.
Finally, I add Redis with a Cache Writer to accelerate common real-time report queries, especially for low-cardinality dimensions. This reduces query latency and protects the analytical backend from repeated hot queries.
To control ingestion latency and throughput, I would tune:
Hot video: The real hot-key mitigation should happen in the counting pipeline by sharding very hot videos into multiple logical subkeys, for example (videoId, shardId), where shardId may be derived from a user or session hash.
If a view count is not found in the cache, I would fall back to the serving source of truth instead of recomputing from raw events.
Deduplication and idempotency are related but separate concerns.
They should rely on a stable event identity, such as an eventId or a deterministic composite key, so retries or replayed events do not cause double counting.
For real-time view count aggregation, events can be partitioned by videoId or (videoId, shardId) in Kafka.
Each partition is owned by only one consumer instance in the consumer group.
This ensures all events for the same aggregation key are handled sequentially by the same consumer, which avoids concurrent updates to the same partial counter.