Coding problem · Medium · Design & Data Structures

Sharded Counter for a Hot Row

Medium Practical tasks

A viral post's like_count row is updated thousands of times per second; every update holds the row lock for a while, so requests queue. Model a sharded counter: each key has slots counter rows, and an increment uses whichever slot becomes free earliest (the lowest slot index on ties).

ShardedCounter(slots, holdMs):

  • increment(key, nowMs) — choose the slot as above; the update starts at max(nowMs, slotFreeAt), holds that slot for holdMs, and adds 1 to the key's count. Return the wait in ms (start − nowMs). Calls arrive with non-decreasing nowMs.
  • get(key) — the total count for the key (sum of all slots); 0 for unknown keys.

Each key has its own slots. See Hot Rows and High-Traffic Counters.

Example 1

Input: ["ShardedCounter","increment","increment","increment","increment","get","increment"]
[[2,10],["p",0],["p",0],["p",0],["p",5],["p"],["q",0]]
Output: [null,0,0,10,5,4,0]
Explanation: Two slots absorb the first two likes immediately; the third waits for slot 0 (free at 10); the fourth gets slot 1 (free at 10) after waiting 5 ms.

Hints

Hint 1

Per key, keep an array of 'free at' times and a total count.

Hint 2

Pick the slot with the smallest free-at time (first one on ties).

Write and run your solution

Loading the editor… (JavaScript is required to run code in your browser.)

Solution and explanation

Show the solution

Time O(slots) per increment, O(1) per get · Space O(keys × slots)

A single row can only absorb about 1 / holdMs updates per unit of time; anything faster queues. With slots rows, increments run in parallel on different rows, multiplying capacity by roughly the number of slots. Reads pay by summing the slots. In SQL: INSERT … ON CONFLICT (post_id, slot) DO UPDATE SET n = n + 1 with a random slot, and SELECT sum(n) … WHERE post_id = ? to read.

Reference solution

class ShardedCounter {
  constructor(slots, holdMs) {
    this.slots = slots;
    this.holdMs = holdMs;
    this.state = new Map(); // key -> { freeAt: number[], count }
  }

  increment(key, nowMs) {
    if (!this.state.has(key)) this.state.set(key, { freeAt: new Array(this.slots).fill(0), count: 0 });
    const s = this.state.get(key);
    let best = 0;
    for (let i = 1; i < this.slots; i++) if (s.freeAt[i] < s.freeAt[best]) best = i;
    const start = Math.max(nowMs, s.freeAt[best]);
    s.freeAt[best] = start + this.holdMs;
    s.count += 1;
    return start - nowMs;
  }

  get(key) {
    return this.state.get(key)?.count ?? 0;
  }
}

Common mistakes

  • Sharing slots between keys.
  • Not starting at max(now, freeAt) — negative waits.
  • Choosing the last instead of the first slot on ties.

Follow-up questions

  • Adapt the number of slots to how hot a key is.
  • Replace the sum on read with a periodically refreshed cached total.