The API
In one line: number_of_shards being an explicit parameter at creation time is the design decision. It means the system must predict how popular something will be, before anyone has interacted with it.
Create counter
createCounter(counter_id, number_of_shards)
| Parameter | Description |
|---|---|
counter_id | The unique ID of the counter. The caller can use a sequencer to get a unique identifier |
number_of_shards | Specifies the number of shards for the counter |
We store metadata — counter identifiers, shard counts, and physical machine mappings — in a data store.
The application calls createCounter when a new item (like a tweet) is created.
To determine the optimal number_of_shards, heuristics are used:
| Heuristic | Why it predicts engagement |
|---|---|
followers_count | Indicates the user's follower count, which predicts potential engagement |
post_type | Public posts may require more shards than protected posts |
Shard count is fixed at creation, before any signal exists — that is the hard part
Read the sequence carefully: createCounter is called when the tweet is posted, and it must decide the shard count then. At that moment zero people have interacted with the content.
So the system is predicting future popularity from proxies for the author rather than evidence about the post. Follower count is a reasonable proxy — an account with ten million followers reliably gets more engagement than one with fifty — but it is only a proxy, and it fails in both directions:
- A celebrity posts something boring. Shards allocated, barely used, read amplification paid for nothing.
- An unknown account goes viral. One shard, and you are back to the original contention problem — which is exactly the question Lesson 4 answers with dynamic resizing.
This is genuinely the same structure as object storage's access tiers: a bet made in advance about future access frequency, where a wrong bet costs real money. The difference is that tiering can be corrected lazily, while a counter under-sharded during a viral burst is failing right now.
Using a sequencer for counter_id connects two building blocks
"The caller can use a sequencer to get a unique identifier" is a small clause that reuses that building block's building block.
The requirement is straightforward: counter IDs must be globally unique across a distributed system with many application servers creating tweets simultaneously. A local counter would collide; a sequencer will not.
Whether these IDs also need happens-before ordering — the sequencer's stronger property — is not required here. Counters do not need to be comparable by creation time. So this is a case of using the building block for uniqueness alone, which is worth noticing: not every user of a sequencer needs its ordering guarantee.
Write counter
writeCounter(counter_id, action_type)
| Parameter | Description |
|---|---|
counter_id | The unique identifier, provided at counter creation |
action_type | Specifies the intended action — increment or decrement. Information about the counter is extracted from the data store |
The service selects a specific shard to update based on load-balancing logic. In the Twitter example, writeCounter handles actions such as likes or replies.
The caller does not choose the shard — and that is the right boundary
Notice what is absent from the signature: there is no shard parameter. The caller says "increment counter 47", and the service decides which shard.
That is the opposite of pub-sub, where the client supplied a partition_ID because only the client knew which messages were semantically related.
Here there is no such relationship — increments are interchangeable. It does not matter which shard receives a like, because all that matters is the sum. So the client has no information the service lacks, and hiding the shard is strictly better: the service can rebalance, resize, or change strategy without any client changing.
Expose the partition when the client has semantic knowledge; hide it when increments are fungible. That is a clean rule covering both chapters.
Decrements complicate sharding in a way increments do not
"Could sharded counters be used for decrement-heavy counters as efficiently as for increment-heavy ones?"
Mostly yes — a decrement is just a negative increment, and it distributes across shards identically. The sum is still correct regardless of which shard absorbs which operation.
Two genuine complications:
Individual shards can go negative. If unlikes land on a shard that received few likes, that shard's value drops below zero. The total is still right, so this is harmless — but only if nothing validates per-shard non-negativity. Code that assumes counters are unsigned will break.
You cannot enforce a floor. "Do not let this counter go below zero" requires knowing the global value at write time, which is exactly what sharding gives up. For likes this never matters — you can only unlike what you liked. For anything resembling inventory or a balance, it matters enormously, and it is another instance of Lesson 2's rule: sharded counters count things; they do not control things.
Read counter
readCounter(counter_id)
| Parameter | Description |
|---|---|
counter_id | The unique identifier. For Twitter, tweet_id is used to get the counter_id for all the counters of the features (likes, retweets, and so on) |
The system queries the data store to aggregate values from all shards. This API is triggered when users view a tweet or their timeline.
One tweet has several counters, and that indirection is deliberate
The counter_id description says tweet_id maps to "the counter_id for all the counters of the features." So a single tweet owns several sharded counters — likes, replies, retweets, and views for video — each independently sharded.
That is the right decomposition. Likes and retweets have very different volumes, so each needs its own shard count. Sharing one counter across features would force the busiest to set the shard count for all of them.
It also explains Lesson 8's storage mapping, which is two levels of indirection:
tweetId -> counterId(s) (like / reply / retweet / view) counterId -> list of shard IDs
Two lookups on every read — a real cost, which is why Lesson 8 keeps this mapping in Redis or Memcache rather than a slower store.
Worth noting a small source inconsistency: the prose mentions a content_type parameter that determines "if a counter is necessary (e.g. a view counter is only needed for videos)" — but content_type does not appear in the createCounter signature. The intent is clear enough; the signature is just incomplete as written.
Key takeaway
Create, write, read — with number_of_shards fixed at creation from proxies for the author, before any engagement signal exists, which is a bet that fails in both directions. The caller never picks a shard, because increments are fungible and the client has no knowledge the service lacks. Decrements shard fine but make per-shard negatives possible and floors unenforceable.
Counting the same event twice
Increments are the least idempotent operation there is: applying one twice changes the answer, permanently, with nothing to detect it afterwards.
Two answers, and the second is the better one when the domain allows it.
An idempotency key per action lets the server recognize a retry and ignore it. That works for any counter and costs a lookup and some retention.
Or model the underlying fact rather than the delta. "This user likes this post" is set membership, and adding an element twice is naturally idempotent. The counter then becomes a derived cardinality rather than an authoritative number, and double-submission stops being possible by construction.
The reason this belongs in the API lesson rather than as an afterthought: the choice is made in the interface. An endpoint that says "add one" has made deduplication the caller's problem forever; one that says "record that this user liked this" has not.
Interview signal by level
| Level | What a strong answer sounds like |
|---|---|
| L4 | "Create a counter, increment it, and read the total." |
| L5 | Notes where shard count comes from: "the number of shards is set at creation using heuristics like follower count, since that predicts how much engagement to expect." |
| Staff+ | Names the prediction problem and the API boundary: "shard count is decided at post time, when nobody has interacted yet — so we're predicting from proxies for the author, and it fails both ways: an over-sharded boring celebrity post and an under-sharded viral unknown. The service picks the shard, not the caller, because increments are fungible — unlike pub-sub partition keys, where the client knew which messages were related. And decrements let individual shards go negative, which is harmless for the sum but breaks any code assuming unsigned, and it means we can't enforce a floor." |
Next: choosing how many shards.