System design interview
Design a ratings and reviews service
One review per user, star histograms, helpful sorting and spam checks.
Press Play to follow a request through the design, or step through it.
- Client and edge
- Service
- Data store
- Cache and queue
- A shopper submits 4 stars and a short text for an item.
- The API writes the review as pending under the key of item and user. A second post by the same user is an edit, not a new row.
- The write emits a change event on the queue, partitioned by item ID so one item's events stay in order.
- A pipeline worker takes the event.
- It asks order history whether this user bought the item, and runs the spam checks. The answer is yes, so the review gets the verified flag.
- The review passes, and the worker marks it published.
- In the same transaction it adds the 4 stars to the item's histogram and records that this review is counted at 4 stars.
- It then adds the review to the search index, so readers can find it.
- Shopper
- Review API
- Summary cache: 60-second TTL
- Review store: one per user, item
- Item aggregates: star histogram
- Search index: sort, filter, match
- Review events: keyed by item ID
- Review pipeline: check and publish
- Order history: verified purchase
On this page
In 2 minutes
I would store each review in a table keyed by item and user, so one user holds one review per item, and sharded by item ID. Edits append to a versions table. Each item has a small aggregate row with a count for each star value, and the average is worked out on read and served from a cache. To keep that row right, a pipeline worker reads change events from a queue partitioned by item ID.
For each event it re-reads the review, works out whether it should count, and moves it between buckets in one transaction, using a counted-stars field, so retries and reordering are harmless. A nightly job recounts a thirtieth of the items to catch drift. New reviews start as pending. The pipeline runs spam and behaviour checks, asks order history for a verified-purchase flag, and then publishes.
Published reviews go into a search index routed by item ID, which serves filters by stars and keywords. The helpful sort uses the lower bound of the Wilson interval, so 5 of 5 votes scores about 0.57 and 90 of 100 scores about 0.83. Removing a review follows the same path as publishing.
Step 1 of 6: Clarify the scope
Interviewer asks
Design the ratings and reviews on a shop or a listings site. A user gives an item 1 to 5 stars and may add text. Every item page shows the average, a bar for each star count, and a list of reviews that readers can sort and filter.
Questions to ask first
Ask these before you draw anything. Under each one is why its answer changes the design.
- Can a user review an item more than once, edit a review, or delete it?
One review per user and item means a unique key. Edits and deletes mean the stored totals must be able to go down as well as up.
- How many items and reviews are there, and how many page views for each new review?
A site with 100 page views per review is a read problem. The summary must be cheap to read, even if it is a little costly to write.
- How should reviews be ordered: newest, highest rated, or most helpful?
Most helpful needs a vote score that is fair to reviews with few votes. Each order is also an index we must build.
- Must a new review appear at once, and who sees a review that is still being checked?
Holding reviews for a spam check adds a pending state. The author expects to see their own review right away.
- Can anyone review, or only people who bought the item?
A verified-purchase flag needs a link to order history, and it becomes a filter and a trust signal.
Requirements
Functional
- Let a user post one rating, with optional text, per item, and edit or delete it later.
- Show each item's average, review count and star histogram.
- List an item's reviews, sorted by most helpful or newest, and filter by stars or keywords.
- Let readers mark a review helpful, once per review.
- Check new reviews for spam, and flag verified purchases.
Non-functional
- Item summary read: p99 under 50 ms.
- Review list page: p95 under 300 ms.
- The stored histogram always matches the reviews, and a retried or duplicated event never double counts.
- A new review is visible to its author at once and to everyone within 10 seconds of passing checks.
- The summary of a popular item survives 10,000 reads a second without touching the database.
Step 2 of 6: Size it
Reads outnumber writes about 100 to 1, and all the summaries fit in 2 GB, so they cache well. The hard parts are keeping the histogram right under edits and retries, and ranking reviews fairly.
Assumed for the exercise, not facts about a real system:
- 20 million items have reviews, with 100 million reviews in all, so 5 per item on average.
- 2 million new or edited reviews a day. There are 200 million item page views a day, and 20% of them open the review list. Peak is 5 times the average.
- A review takes about 1 KB with its metadata. A search index copy takes about 1.5 times the text.
- An aggregate row (item ID, count and 5 star counters) takes about 100 bytes.
- Review writes
- 2M ÷ 86,400 s ≈ 23/s; × 5 at the peak
- ≈ 120/s at the peak
- Summary reads
- 200M ÷ 86,400 ≈ 2,300/s; × 5 at the peak = 11,600/s; 5% miss the cache
- ≈ 11,600/s at the peak, ≈ 580/s reach the database
- Review list reads
- 200M × 20% = 40M ÷ 86,400 ≈ 460/s; × 5
- ≈ 2,300/s at the peak
- Review storage
- 100M × 1 KB = 100 GB; the index adds 1.5 ×
- ≈ 100 GB of reviews plus ≈ 150 GB of index
- Aggregates
- 20M items × 100 bytes
- ≈ 2 GB, small enough to cache whole
- Nightly recount
- 20M items ÷ 30 days ≈ 667,000 items a night × 5 reviews
- ≈ 3.3M review rows a night
Step 3 of 6: Sketch the path of one request
Step through the diagram at the top of the page to watch a request move through these parts.
A shopper talks to the Review API. A new review is written to the Review store, keyed by item and user, so one user can hold only one review per item. It starts as pending. The write also puts a change event on the Review events queue, partitioned by item ID.
A Review pipeline worker reads the events. It asks Order history, a separate service, whether the user bought the item, runs spam checks, and decides whether to publish. When it publishes, it updates the item's row in the Item aggregates store and adds the review to the Search index.
Reading is cheap. The API serves the summary from a Summary cache, falling back to the one small aggregate row. It serves the review list from the search index, which can sort, filter by stars and match keywords.
Other paths through the system
Open an item page
- A shopper opens an item page. The page asks for the rating summary and the first page of reviews.
- The API looks in the summary cache, which holds the item's histogram for 60 seconds.
- On a miss it reads the item's aggregate row, which is one small row.
- The row returns 1,284 reviews and a histogram, which give an average of 4.3. The API refills the cache.
- For the list it asks the search index for this item's reviews, sorted by helpful score.
- The index returns 10 reviews and a cursor for the next page.
- The page shows the average, the bars and the first 10 reviews.
Remove a spam review
- A moderator removes a review that was flagged as spam.
- The API sets the review's status to removed.
- The change goes on the queue like any other.
- The pipeline worker takes the event for the item.
- It reads the review row now, and sees that the review is removed but still counted at 4 stars.
- It takes one off the 4-star bucket and clears the counted value, in one transaction.
- It deletes the review's document from the search index.
Step 4 of 6: Go deep on the hard part
A histogram that stays correct
Interviewer asks
How do you keep each item's average and star bars right through edits, deletes and retried events?
Computing an average over all of an item's reviews on each page view is far too slow for an item with 100,000 reviews.
Read moreShow less: A histogram that stays correct
Instead, keep one aggregate row per item: a count for each of the 5 star values. The average is the sum of stars times counts, divided by the total, worked out on read. The row is about 100 bytes.
- Reviews table. One row per user and item, with a unique key on the pair. Shard by item ID, so an item's reviews and its aggregate live together, and the unique key is checked inside one shard.
- Versions. Every edit also appends a row to an append-only versions table. Moderators can see what a review said before it was changed.
- Counted stars. Each review row carries a field called counted_stars: the star value currently included in the histogram, or empty if none.
The trap is to update the histogram with plus one and minus one from the event. A retried event adds twice, and events that arrive out of order give wrong totals. Use the review row as the truth instead. On each event, the worker runs one transaction:
- Read the review row. The target is its live stars if it is published, or empty if it is pending, removed or deleted.
- If counted_stars already equals the target, stop. This makes duplicates harmless.
- Otherwise subtract one from the bucket of counted_stars, if any, and add one to the bucket of the target, if any. Then set counted_stars to the target.
The worker ignores the numbers in the event and reads the current row, so any order of events and any number of retries ends in the same state. Partition the queue by item ID so one worker handles an item at a time. A row lock on the aggregate covers the rest.
Repair. Even good code can drift: a bad deploy, a manual fix. Each night, recount a thirtieth of the items from their reviews and compare. In our estimate that is 667,000 items and 3.3 million review rows a night. Fix any difference and count it. A steady stream of drift means a bug, not routine noise.
Read your own write. The author sees their review at once because the API returns it in the response. The histogram follows within seconds.
Sorting and filtering reviews
Interviewer asks
How do you rank reviews by helpfulness fairly, and let readers filter by stars or keywords?
Sorting by the share of helpful votes is unfair.
Read moreShow less: Sorting and filtering reviews
A review with 5 helpful votes out of 5 has a 100% share, and one with 90 out of 100 has 90%. The first has far less evidence. Sorting by raw vote count has the opposite flaw: old reviews pile up votes, and new ones never get seen.
Use the lower bound of the Wilson score interval. It answers: given these votes, what is the lowest share of readers who plausibly find the review helpful, at 95% confidence? With n votes, share p and z = 1.96, the score is (p + z²/2n − z × √((p(1−p) + z²/4n)/n)) ÷ (1 + z²/n).
- 5 of 5 votes: p = 1, and the score works out to about 0.57.
- 90 of 100 votes: p = 0.9, and the score is about 0.83.
- So the review with more evidence ranks first, even though its share is lower. With few votes the bound is low, and it rises as evidence builds.
A review with no votes scores 0. Break ties by date, newest first, and offer a Newest sort so fresh reviews are still read.
Votes are counters on the review row, one vote per user per review, enforced by a unique key, and never on your own review. Recompute scores in batches every few minutes, not on each vote, and write the new score into the search index.
The search index holds one document per published review: item ID, stars, text, date, verified flag and helpful score. Route by item ID, so one shard answers a query. A page of reviews is then a filter on item, an optional filter on stars or verified, an optional text match for a keyword such as battery, and a sort on score or date. Page with a cursor that remembers the last sort value, not an offset, which slows with every page.
The database stays the source of truth. If the index is lost or wrong, rebuild it from the reviews table. The index trails by a few seconds, which readers do not notice.
Trust: spam, verification and removal
Interviewer asks
How do you keep fake and abusive reviews out, and take them out cleanly when they get in?
Reviews move prices and sales, so there is money in faking them.
Read moreShow less: Trust: spam, verification and removal
Assume attackers will try, and build several layers.
- Pending first. A new review is stored as pending and is visible only to its author. The pipeline checks it and publishes it. Most pass within seconds. Some are held for a human.
- Content checks. Look for the same text pasted across many items, links and phone numbers, and a classifier score for abuse. Hash the text so exact copies are found fast.
- Behaviour checks. Many five-star reviews on one item within an hour from accounts less than a week old is a strong signal. So is a user who reviews 30 items in a day.
- Verified purchase. The pipeline asks order history whether the user bought the item, and stores the answer as a flag. Show a badge and offer a verified-only filter. The call is cached, since the answer rarely changes.
- Rate limits. Cap reviews per account per day, lower for new accounts, on top of the unique key that stops a second review of the same item.
Edits. An edit enters the same checks, but the live version stays visible until the new one passes. Otherwise a user's review vanishes while they fix a typo. The live stars are what the histogram counts, so the counted-stars rule still holds.
Removal. Removing a review, by a moderator, by its author, or because an account was closed, sets its status. The pipeline then does the same thing as for any change. It reads the row, sees a target of empty, takes the stars out of the histogram, and deletes the index document. One code path handles publishing, editing and removing, which is why it is easy to trust.
To clean up a spam wave, mark thousands of reviews removed in one batch and enqueue an event for each. Because the update converges, a replay is safe. Keep the removed rows and their versions for audit and for appeals, and never show them.
Step 5 of 6: Name the trade-offs
Rating summary
An aggregate row per item, with a star histogram over Average over all reviews on each read
A read touches one small row, even for an item with 100,000 reviews. The cost is a counter that can drift, so it needs a nightly repair.
Updating the histogram
Re-read the review and set the counted value over Apply plus one and minus one from each event
Retries, duplicates and out-of-order events all converge on the right answer. The cost is an extra read and a transaction for every event.
Helpful sort
Lower bound of the Wilson interval over Raw helpful votes or helpful percentage
5 of 5 votes no longer beats 90 of 100. The cost is a formula to explain, and new reviews start low, so a Newest sort is needed.
New reviews
Hold as pending until automated checks pass over Publish at once and clean up later
Spam never reaches readers or the totals. The cost is a few seconds of delay, and a review queue for the ones held.
Review list
A search index fed from the database over A database query with ORDER BY and filters
Sorting, star filters and keyword search stay fast at 2,300 reads a second. The cost is a copy that trails by seconds and must be rebuildable.
Step 6 of 6: Say what breaks next
Questions an interviewer asks once the design is on the board. Answer each one out loud first.
- How do you stop one person from posting many fake reviews?
The unique key stops repeats on one item, and account limits slow everything else. Burst detection looks for many new accounts giving one item five stars in a short time. Hold those for a human, and show the verified badge so readers can weigh the rest.
- How would you handle product variants, such as colours and sizes?
Key reviews to the parent product, and store the variant as a field on the review. The aggregate then covers the parent. A filter on the variant in the search index lets a reader narrow the list.
- How do you stop people inflating helpful votes?
A unique key allows one vote per user and review, and nobody can vote on their own review. Votes from new accounts, or a sudden burst on one review, are held back like a burst of reviews. Scores are recomputed in batches, so a spike does not reorder the page at once.
- What happens when two product pages are merged?
Move the reviews to the surviving item, then run the aggregate update for both items. If one user reviewed both, keep the newer review and hide the other, so the unique key still holds. Re-index the moved reviews.