Design read receipts and unread counts for a messaging product. Why is this harder than it looks?
Show the full answer Hide the answer
What the interviewer is testing
Whether you recognise that per-user-per-channel state at high write volume is a harder problem than message storage itself.
Why it is hard
The write volume exceeds message volume. One message to a channel of 5,000 members generates up to 5,000 read-state updates as people view it. Read state changes far more often than messages are created.
It is per-user-per-channel, so the state space is users times channels — much larger than the message set.
It is read constantly. Every client renders unread counts for every channel on every view, so this is one of the highest-volume read paths in the product.
The design
Store a read position, not per-message flags. A single monotonic position per user per channel — "read up to message N". Unread count is then derived as the count of messages after that position, which reduces the state from one row per message per user to one row per channel per user.
That single modelling choice removes most of the problem.
Derive counts rather than storing them, from the channel's current position minus the user's — with the caveat that an exact count requires knowing how many messages fall in that range, so a cached count with periodic correction is the usual compromise.
Batch and coalesce updates. Read position advances continuously as someone scrolls; sending every change is wasteful. Debounce client-side and send periodically.
Accept eventual consistency. An unread badge being briefly stale is imperceptible. This removes any coordination requirement.
Cap the count. Displaying "99+" rather than an exact figure removes the need to compute large counts precisely, which is a product decision that eliminates real engineering cost.
What a strong answer adds
Read receipts visible to others are a different and harder feature than unread counts. They are fan-out — everyone in a conversation sees who has read — which reintroduces the quadratic problem that per-user state avoided. Many products limit them to small conversations for exactly this reason, which is a product constraint driven by an architectural one.
And the storage choice: this is a high-write, small-value, key-addressed workload, which suits a key-value store far better than the relational store holding messages.
Common weak answers
A per-message read flag per user. Storing exact unread counts and updating them synchronously.