DoorDash's Most Frequent System Design Question: Review & Reward System (Vote Aggregation + Rewards Triggered Exactly Once)
Breaking down DoorDash's frequent system design question: a food-delivery review and reward system — caching for read-heavy reviews, vote deduplication with async aggregation (Kafka + materialized views), exactly-once reward jobs (at-least-once delivery + an idempotent state machine), and failure handling.
DoorDash's system design question bank is small. Today let's go through one of the most frequent: design a DoorDash-style Review & Reward System.
1. Requirements
Functional requirements
- Submit a review: after completing a delivery order, a user can review the restaurant with a 1–5 star rating, text, and optional photos.
- Vote: other users can upvote or downvote a review to mark it helpful or not. Each user can vote at most once per review.
- Aggregated stats: maintain the upvote count, downvote count and a helpfulness score, shown alongside reviews when browsing a restaurant.
- Rewards: once a review reaches its evaluation window, reliably trigger the reward calculation, and make sure each eligible review triggers it exactly once.
Out of scope
| Part | Notes |
|---|---|
| User authentication, order management | Already exist; just call them |
| The full ordering and delivery flow | Out of scope |
| Reward Scoring Service | An existing black box that computes the reward amount from review quality and vote stats |
| Reward payout | Handled by a third-party payment provider |
Non-functional requirements
- High availability, low latency, horizontal scalability
- Restaurant review queries < 200 ms (a classic read-heavy, high-QPS path)
- Vote stats can be eventually consistent — no need to reflect the latest count immediately after a vote
- Every write API must tolerate client retries with an idempotency guarantee
2. Overall architecture
Four core modules:
| Module | Responsibility |
|---|---|
| Review Service | Create and query reviews |
| Vote Service | Handle vote writes, one vote per user |
| Vote Aggregation Pipeline | Compute vote stats asynchronously |
| Reward Orchestrator | Trigger reward calculation when due, exactly once |
User --> Review Service --verify--> Order Service
| | write images --> S3
| v
| Review DB <--cache-- Redis <-- read reviews
|
+--> Vote Service --VoteChanged--> Kafka
|
v
Vote Aggregator
|
v
ReviewStats Store
(materialized view)
due reviews --> Reward Orchestrator --> Reward Scoring
(unique job + Service
state machine) (black box)
3. Review Service: writes + the read-heavy path
Creating a review
- Call the Order Service to confirm the order is complete and check whether it already has a review.
- Store review metadata in DynamoDB or Cassandra; put photos in S3 and keep only the object URL in the record.
- Guard against duplicate submissions from client retries: use an idempotency key, or a unique constraint on
(order_id, user_id).
Querying reviews (the main read-heavy path)
- Index by
restaurant_id, sorted bycreated_ator helpfulness score. - Paginate with cursor pagination, which is more stable and faster than offset pagination.
- Cache popular restaurants' review summaries and first pages in Redis, with the Review DB as the source of truth, to reduce database load and keep P99 latency under 200 ms.
4. Vote Service: one vote per user
- Use
(review_id, user_id)as the unique key of the vote table, so the data model itself guarantees at most one active vote per user per review. - If changing a vote is allowed (say, upvote to downvote), update the existing record with a conditional update.
- After a successful write, publish a
VoteChangedevent to Kafka and don't update the review's aggregate fields synchronously — that avoids write contention on popular reviews.
5. Vote aggregation: an asynchronous materialized view
- The Vote Aggregator consumes Kafka events and computes the delta for each vote change (changing a vote is upvote −1, downvote +1), updating the ReviewStats Store.
- Upvote count, downvote count and helpfulness score are all asynchronously maintained materialized views.
- Reads use the pre-aggregated results instead of a live COUNT over the vote table, so read latency stays low and it scales horizontally more easily.
- Eventual consistency is allowed, so brief delays in the stats are fine.
6. Reward workflow: triggering exactly once
Scheduling when reviews are due
- Record
evaluation_atwhen a review is created — e.g. it becomes eligible for evaluation 7 days after posting. - Find due reviews with a delayed queue, a scheduled job, or a time-bucketed task table.
- Due reviews go to the Reward Orchestrator, which calls the Reward Scoring Service.
Exactly-once = at-least-once + idempotent processing
True exactly-once delivery is very hard in distributed systems. In practice you turn it into at-least-once delivery + idempotent processing.
- Create a unique reward job per
(review_id, evaluation_window), so duplicate messages can't create a second job. - When a worker claims a job, a conditional write moves it from
PENDINGtoPROCESSING— only one worker can succeed. - After the Reward Scoring Service call succeeds, mark it
COMPLETED. - If the Reward Scoring Service supports idempotency keys, use
review_id + evaluation_windowas the key so downstream calls can also be retried safely.
PENDING --(only one worker wins)--> PROCESSING
|
(scored)
v
COMPLETED
How the state machine works: only one worker can move a job from
PENDINGtoPROCESSING; it becomesCOMPLETEDafter scoring succeeds. If a worker crashes or times out, the job is claimed again and retried with the same idempotency key.
Here's SQL showing the two key ideas — a unique constraint plus a conditional update (SQLite syntax):
-- 1. Only one reward job per (review_id, evaluation_window)
CREATE TABLE reward_jobs (
review_id TEXT NOT NULL,
evaluation_window TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING',
worker_id TEXT,
PRIMARY KEY (review_id, evaluation_window)
);
-- The scheduler may deliver duplicates: repeated inserts are ignored, no second job is created
INSERT OR IGNORE INTO reward_jobs (review_id, evaluation_window) VALUES ('r1', '7d');
INSERT OR IGNORE INTO reward_jobs (review_id, evaluation_window) VALUES ('r1', '7d');
-- 2. Worker claims the job: succeeds only while status is still PENDING (1 affected row = claimed)
UPDATE reward_jobs
SET status = 'PROCESSING', worker_id = 'worker-A'
WHERE review_id = 'r1' AND evaluation_window = '7d' AND status = 'PENDING';
-- 3. Mark as completed after scoring succeeds
UPDATE reward_jobs
SET status = 'COMPLETED'
WHERE review_id = 'r1' AND evaluation_window = '7d' AND status = 'PROCESSING' AND worker_id = 'worker-A';
7. Failure handling
| Failure | Handling |
|---|---|
| The Vote Aggregator updates the database but the Kafka offset commit fails | The message will be consumed again: deduplicate by event_id, or make the aggregation update idempotent |
| A reward worker crashes after calling the scoring service | Reclaim the job after a timeout and call again with the same idempotency key |
| Redis is unavailable | Fall back to the persistent store (the Review DB) |
Summary
Caching, materialized aggregation and an event-driven architecture decouple the three flows — reading reviews, voting and issuing rewards — while meeting the requirements for high availability, low latency, eventual consistency and horizontal scaling.
Points that earn extra credit in the interview:
- Proactively separate the read-heavy query path from the write path and optimize each.
- Explain why vote stats use asynchronous aggregation instead of a live COUNT.
- Break exactly-once down into "at-least-once delivery + idempotent processing", and explain the unique constraint and state machine concretely.
Found this helpful? Let's talk.
Happy to swap interview notes, do mock interviews, or share referral info.

Scan to add me on WeChat
Related notes
- Microsoft SDE Intern Interview | Two-Round Microsoft SDE Recap | Microsoft InternshipMicrosoft · 2026-10-04›
Amazon VO: Behavioral Deep Dive + Document Processing Under Limited Capacity + Splitting Alexa Conversations into SessionsAmazon · 2026-10-02›
- Apple SWE VO, Four Rounds: Debugging a Swift Class Hierarchy + Focus Mode Schedule API + Telemetry Monitoring DesignApple · 2026-10-02›
- Meta E4 Phone Screen + VO (Passed): Max Parenthesis Depth, Max Leaf-to-Leaf Path Sum, Design DropboxMeta · 2026-10-02›