LLD: Design Hit Counter(Multithreaded)
A fixed-size sliding window with per-second buckets and fine-grained locking to ensure thread safety, bounded memory, and O(1) updates under high concurrency
A Hit Counter looks simple, but it’s a foundational building block used in many real-world backend systems.
1. API Rate Limiting (Most Common Use)
Use case
Limit number of requests per user / IP / API key
Example
“Max 100 requests per user in last 5 minutes”
How hit counter helps
Each API call →
hit(userId)Before serving request →
getHits(userId)Reject if threshold exceeded
Real-world systems
API Gateways (AWS API Gateway, Kong, NGINX)
Auth services
Payment APIs
2. Traffic Monitoring & Analytics
Use case
Track traffic volume over time
Detect traffic spikes
Example
Hits per endpoint per minute
Requests per region
Why needed
Capacity planning
Auto-scaling triggers
Alerting (DDoS detection)
3. Distributed Systems Health Checks
Use case
Detect unhealthy services
Example
If
hits < Xin last N minutes → service may be down
Used in
Load balancers
Service mesh
Monitoring systems
4. Ad Impression & Click Tracking
Use case
Count ad impressions / clicks in time windows
Example
“Ad shown 1M times in last hour”
Why hit counter
Needs to be fast
Needs sliding window semantics
Needs deduplication support
5. Feature Usage Tracking
Use case
Track how often a feature is used
Example
“Export button clicked 500 times today”
Used by
Product analytics
Feature flag systems
6. Security & Abuse Detection
Use case
Detect suspicious behavior
Examples
Too many login attempts
Brute force detection
Credential stuffing
Flow
Increment hit counter per IP / user
Lock account if threshold crossed
7. Caching & Throttling Decisions
Use case
Decide what to cache
Decide eviction priority
Example
Cache objects with high hit counts
Evict cold keys
Used in
CDN
In-memory caches (Redis LFU)
8. Load Balancing & Traffic Shaping
Use case
Route traffic based on recent load
Example
Don’t route new requests to servers exceeding hit threshold
Systems
Reverse proxies
Service mesh sidecars
9. Leader Election / Coordination (Indirect)
Use case
Track heartbeats or task executions
Example
“Node sent heartbeat 300 times in last 5 minutes”
Used in
Zookeeper / etcd-like systems
Distributed schedulers
FR
Records hits at a given timestamp (in seconds)
Returns number of hits in the last 5 minutes (300 seconds)
Is thread-safe
Handles high concurrency
Uses bounded memory
Works in real time
Core Design Idea
Sliding Window with Buckets
We maintain 300 buckets, one per second
Each bucket stores:
timestampcountof hits in that second
On a hit:
Map timestamp → index =
timestamp % 300If bucket timestamp != current timestamp → reset bucket
On
getHits(ts):Sum counts where
ts - bucket.timestamp < 300
Why this works well
| Property | Value |
| ------------------- | -------------------- |
| Time complexity | O(1) hit, O(300) get |
| Space complexity | O(300) |
| Thread safety | Locking / atomic |
| Scalable | Yes |
| Production-friendly | Yes |
Concurrency Strategy
We have multiple writers (hits) and multiple readers (getHits).
| Approach | Pros | Cons |
| ------------------------ | ------------------ | -------------------------------- |
| `synchronized` | Simple | Lower concurrency |
| `ReentrantReadWriteLock` | Better parallelism | Slightly complex |
| `AtomicIntegerArray` | Lock-free counters | Timestamp reset still needs sync |
Chosen Approach (Interview-Preferred)
👉 Fine-grained locking per bucket
Each bucket has its own lock
Avoids global lock bottleneck
Class Design (LLD)
HitCounter
├── Bucket[300]
├── hit(timestamp)
└── getHits(timestamp)
Bucket
├── int timestamp
├── int count
└── ReentrantLock
import java.util.concurrent.locks.ReentrantLock;
class HitCounter {
private static final int WINDOW_SIZE = 300;
private final Bucket[] buckets;
public HitCounter() {
buckets = new Bucket[WINDOW_SIZE];
for (int i = 0; i < WINDOW_SIZE; i++) {
buckets[i] = new Bucket();
}
}
// Record a hit at given timestamp (seconds)
public void hit(int timestamp) {
int index = timestamp % WINDOW_SIZE;
Bucket bucket = buckets[index];
bucket.lock.lock();
try {
if (bucket.timestamp != timestamp) {
bucket.timestamp = timestamp;
bucket.count = 1;
} else {
bucket.count++;
}
} finally {
bucket.lock.unlock();
}
}
// Get hits in last 5 minutes
public int getHits(int timestamp) {
int total = 0;
for (Bucket bucket : buckets) {
bucket.lock.lock();
try {
if (timestamp - bucket.timestamp < WINDOW_SIZE) {
total += bucket.count;
}
} finally {
bucket.lock.unlock();
}
}
return total;
}
private static class Bucket {
int timestamp;
int count;
ReentrantLock lock = new ReentrantLock();
}
}
Thread Safety Guarantees
✔ No race conditions
✔ Independent bucket locking
✔ Readers and writers don’t block entire structure
✔ Works under high QPS
Edge Cases Handled
Multiple hits in same second
Old buckets automatically evicted
Timestamp jumps (system restarts, time skew)
Concurrent hit + getHits
Optimizations (Discuss in Interview)
1. Read-Heavy Optimization
Use StampedLock or ReadWriteLock
2. Distributed Counter
Shard by machine
Aggregate via:
Kafka + stream aggregation
Redis INCR + TTL
Time-series DB
3. Millisecond Precision
Increase window size
Bucket per 100ms or 10ms
9. Follow-up Questions You’ll Likely Get
| Question | Expected Direction |
| ----------------------------- | ------------------------ |
| How to scale to millions QPS? | Sharding + aggregation |
| How to persist? | WAL / async batch writes |
| How to make it distributed? | Event streaming |
| How to avoid locks? | Atomic arrays |
10. Interview One-Liner Summary
“I used a fixed-size sliding window with per-second buckets and fine-grained locking to ensure thread safety, bounded memory, and O(1) updates under high concurrency.”
Convert this to lock-free Atomic implementation
Design Redis-backed / distributed hit counter
Write JUnit concurrency tests
Extend to per-user / per-API hit counters

