System design sample solution

System design: design a food review system

A step-by-step system design answer you could actually give in 60 minutes.

System design interview · Sample 60-minute answer · Not DoorDash's actual design · Independent, not affiliated with DoorDash

The system design question

Design a review system for DoorDash. Customers can review a specific item on a restaurant's menu, others can upvote or downvote reviews, and reviewers get a reward based on how good their review is.

Agree on the scope (5 minutes)

Say up front what you're covering.

We'll designWe'll skip
Writing a review of one menu itemPhoto and video uploads in detail
Upvoting and downvoting reviewsSearching review text
Showing the best reviews on a menu item fastSpam and abuse models in detail
Rewarding reviewers for good reviews, exactly onceHow rewards get spent

Then agree on what "good" means:

  • Fast to read: reviews show up on the menu page with no noticeable delay, even for the busiest restaurants.
  • One vote per person: a customer can vote once per review, and change their mind, but never count twice.
  • Counts can lag a little: a vote count that's a few seconds behind is fine. A missing vote isn't.
  • Fair rewards: each good review gets its reward once, and people can't game it with fake votes.

Rough numbers (5 minutes)

These are assumptions, but reasonable ones: 20 million orders a day, about 5% of them lead to a review, each review gets about 10 votes, and the menu pages that show reviews get about 300 million views a day. Peaks are about 5 times the average.

~1M
new reviews a day
~600
votes per second at a peak
~20K
review reads per second at a peak
~400 GB
of reviews a year
QuestionRough mathWhat it means
How many reviews?20 million orders × 5%About 1 million a day, roughly 12 a second. Writing reviews is easy
How many votes?1 million reviews × 10 votes ÷ 86,400 seconds × 5About 600 votes a second at a peak overall, but they bunch up on a few popular reviews
How many reads?300 million views ÷ 86,400 × 5About 20K a second at a peak. Reads are where the load is, so they come from a cache
How much storage?1 million × about 1 KB × 365About 400 GB a year of reviews. Votes are smaller but there are 10 times more of them

The overall design (10 minutes)

Customer appreviews and votesReview servicechecks and savesReviews databasereviews and votesVote eventsa queueCounts and rewardsupdated in batches
Reviews and votes are saved straight away. Vote events go on a queue, and workers update the counts, the cached top reviews and, later, the rewards.
  1. After an order's delivered, the customer can review any item from it. The review service checks the order really had that item, then saves the review.
  2. A vote saves one row per voter per review. Saving it also puts a vote event on a queue.
  3. A counting worker reads vote events in batches and updates each review's upvote and downvote counts.
  4. A ranking worker keeps a short list of the best reviews for each menu item, and puts it in a cache. Menu pages read from that cache.
  5. Once a review has been up for a while, a reward job scores it and adds a reward to the reviewer's balance, using a key so it can only happen once.

What the apps need to do

ActionWho does itWhat's sent
Write a reviewCustomer appOrder ID, menu item ID, rating, text
Vote on a reviewCustomer appReview ID, up or down, or clear the vote
Get reviews for an itemMenu pageRestaurant ID, menu item ID, sort order, page cursor
Get my rewardsCustomer appCustomer ID

What we store and where

DataWhere it goesWhy
ReviewsDatabase, split by restaurant IDA menu page needs every review for one restaurant, so keep them together
VotesDatabase, one row per voter and reviewA unique rule on voter and review stops double votes
Vote countsColumns on the review row, updated in batchesReading a count shouldn't mean counting votes
Top reviews per itemIn-memory cacheRead about 20K times a second and changes slowly
Reward ledgerRelational databaseMoney-like data needs transactions and a unique rule per reward

Step 1: Tables and votes (10 minutes)

Start with the tables. A review has an ID, the restaurant, the menu item, the order it came from, the author, a rating, text, a status, and the upvote and downvote counts. Index it by restaurant and menu item, so the menu page finds an item's reviews in one lookup.

A vote row is the review ID, the voter, and up or down. Make the review ID and voter together unique. A second tap just updates the same row, and switching from up to down changes it. That's idempotency without any extra work.

Split the data by restaurant ID. One restaurant's reviews and votes all live together, so the menu page never has to ask several machines. It's a sharding choice, and the size fits a NoSQL or relational store either way. We'd pick relational for the unique rules.

Hot reviews

Say a popular restaurant gets featured and one review gets thousands of votes a minute. If every vote updates the count on the same row, those updates line up behind each other.

So don't update the count on every vote. Save the vote row, put an event on the queue, and let a worker add up a few seconds of events and apply them in one update. A thousand votes become one write. If one review is still too hot, split its count into a few counters and add them up when reading.

Step 2: Showing the best reviews fast (10 minutes)

Sorting by net votes (upvotes minus downvotes) favors old reviews that have had time to collect votes. Sorting by the share of upvotes favors a review with one upvote and nothing else. A common fix is to use a confidence-adjusted score: treat a review with few votes as uncertain, and only rank it high once enough votes agree.

Don't work that out on every page view. When a review's counts change, the ranking worker recomputes its score and updates that item's top 20 or so reviews. That list goes in a cache, keyed by restaurant and menu item.

Most menu pages only show the top few reviews and an average rating, so nearly every read is a cache hit. Scrolling further pages through the database is fine, because few people do it.

Updating the cached list on every vote would churn it. Updating it every few seconds from the batched counts is plenty, since nobody notices if a review moves up a spot a little late.

Step 3: Rewarding reviews exactly once (10 minutes)

Decide when a review is judged. Votes keep coming in for days, so wait a settling window, say 7 days after it's posted. Then a daily job scores each review that just hit 7 days: its votes, whether it's from a real order, and maybe its length or a photo.

The job writes rewards to a ledger. Each reward has a key, like the review ID plus 'week one reward', with a unique rule. If the job crashes halfway and runs again, the rows it already wrote are rejected, so nobody gets paid twice. That's idempotency again, this time for money.

The reviewer's balance is the sum of their ledger rows, cached for reading. Never just add to a balance column without the ledger row behind it, or a retry can double it.

Stopping people gaming it

Rewards make fake votes worth something. Only count votes from customers with a real order history, give less weight to brand-new accounts, and flag reviewers whose votes mostly come from the same small group of accounts. Let the settling window double as a review period, so flagged rewards can be held before they're paid.

Choices you should be able to defend

We choseInstead ofBecauseWe'd reconsider if
Batching vote countsUpdating the count on every voteA hot review becomes one write every few seconds instead of thousandsCustomers needed counts to tick up instantly
Splitting data by restaurantSplitting by review IDA menu page reads from one placeA single restaurant got too big for one machine
A cached top list per itemSorting on every page viewReads are 20K a second and the list barely changesEach customer saw a different, personalized order
Rewards after a 7-day windowRewarding as votes come inScores have settled, and there's time to catch fake votesReviewers complained rewards felt too slow

What could go wrong (5 minutes)

What happensWhat we do about it
The counting worker falls behindCounts lag a bit. The votes are safe in the database, so nothing's lost. Add workers
The cache goes downMenu pages fall back to the database for the top reviews, with a short limit on how many reviews they load
The reward job crashes halfwayRun it again. The unique key skips rewards that were already written
A vote ring targets one reviewerDiscount votes from new or linked accounts, and hold the reward for a human check
A restaurant gets a flood of reviewsBatching keeps writes small, and the rate limit per customer stops one person flooding it

Follow-up questions (5 minutes)

How would you rank reviews differently for each customer?

Keep the cached top list as a starting set, then reorder it on read using a few customer signals, like dietary preferences. Don't build a separate list per customer.

What if a reviewer edits their review?

Keep the votes, but save the edit history. If the edit changes the review a lot, reset the reward clock so people can't post something good, collect a reward, then swap it.

How would you show an item's average rating?

Keep a running sum and count of ratings per menu item, updated by the same batch worker. The average is one division at read time.

How the 60 minutes add up

MinutesWhat you're doing
0–5Agree on what you're building
5–10Rough numbers
10–20Draw the overall design
20–30Tables and votes
30–40Showing the best reviews fast
40–50Rewarding reviews exactly once
50–55What could go wrong
55–60Follow-up questions

Key concepts used here

Further reading

We wrote this sample answer ourselves.

Practice Loop is independent and isn't affiliated with or endorsed by DoorDash.