Learn
Should the unread badge be counted when you read it or when mail arrives
The interviewer asks for an inbox with a small number on the icon: how many messages are unread. You draw a counter in a cache, because counters are fast. Then they ask what happens when someone opens the same message on two devices at once, and the board goes quiet.
The requirement
Every user sees an unread count on their inbox badge. It is shown on every page load. Messages arrive at any time, and a message becomes read when the user opens it, from any of their devices.
That is the whole requirement. It is small enough to finish in ten minutes, which is exactly why it is a good test of whether you can name a trade-off instead of reciting components.
Two designs and what each one costs
Option A: count on read. Keep a read flag on each message. When the badge is drawn, count the unread rows for that user, using an index on owner and flag.
- What you get: the number is always right, because there is nothing to keep in sync.
- What you pay: every page load runs a count, and its cost grows with the number of unread messages. A user with a large backlog of unread mail makes every page they open more expensive.
Option B: count on write. Keep a separate counter per user. Add one when a message arrives, subtract one when it is read. The badge reads one number.
- What you get: drawing the badge is a single lookup, whatever the backlog.
- What you pay: two pieces of state that must agree, and every path that changes one must change the other. When they drift, the badge is wrong until something repairs it.
What goes wrong in option B
Here is the input that exposes it. The same unread message is opened on a phone and a laptop within the same second. Both requests see the message as unread, both mark it read, both subtract one. The counter has now dropped by two for one message. Do that a few times and the badge shows a negative number, or zero while unread mail is still waiting.
The fix is to subtract only when this request is the one that actually changed the flag:
def mark_read(db, owner, msg_id):
# Subtract only if this call flipped the flag from unread to read.
flipped = db.execute(
"UPDATE messages SET is_read = 1 "
"WHERE id = ? AND owner = ? AND is_read = 0",
(msg_id, owner),
).rowcount
if flipped == 1:
db.execute(
"UPDATE inbox SET unread = unread - 1 WHERE owner = ?",
(owner,),
)
db.commit()
Run it with both devices. The first request flips the flag and gets a row count of one, so it subtracts. The second finds no unread row, gets zero, and leaves the counter alone. Both statements commit together, so a crash between them does not leave half a change. Deleting an unread message, moving it to another folder, and bulk "mark all as read" each need the same care, and that list is the real price of option B.
There is also a cheap middle path for option A: the badge rarely needs the exact number past a point. Count at most a hundred rows and show "99+" above that. The cost of the count is now bounded, and the answer stays correct for everyone who can actually read the number.
What to say out loud
Before you draw either box, say the deciding question: "How often is the badge read compared with how often the count changes, and how wrong may it be, and for how long?" Then commit: "If badge reads dominate and a few seconds of drift is acceptable, I keep a counter and make every decrement conditional on the flag actually flipping. If it must never be wrong, I count on read and cap the count at a hundred." Two sentences. They show the interviewer you chose a design because of a number, not because of habit.
The exercise
- Pick any feature that shows a number: likes on a post, items in a basket, open tickets in a queue.
- Write option A and option B for it in one line each, and next to each write what it costs in reads, writes and correctness.
- List every event that changes the number. For each one, write the smallest pair of concurrent events that would make a counter drift.
- Say the deciding question aloud, then say which option you would pick for two different answers to it. Time yourself; aim for under a minute.
Where this fits in Gadfly
System design is one of Gadfly's four rooms, next to algorithms, behavioral and code review. The design runs on a shared board, either guided or as a mock. In guided mode the tutor asks the next question, such as what happens when two devices mark the same message, rather than drawing the answer for you. In a mock it behaves like an interviewer, with no coaching during the round and a debrief at the end that quotes what was said, not a grade.
Published 14 October 2026 · System design: one trade-off · all notes · feed
This note was drafted by Gadfly's scout — an automated writing agent run by Dmytro Hryshchenko — and checked by rules against the product's claims registry, not read by a person before it went live. The exercise is original. Spotted a mistake? [email protected].