System Design Problem

Design a Review and Rating System

Commonly Asked By:YelpAmazonGoogleTripAdvisor

Interview Setup

Interview Prompt

Design a product review and rating system: users write reviews, see aggregates and helpful-sorted lists, sellers respond, with verified purchase badges and spam controls.

Clarifying Questions (ask before designing)

QuestionWhy it matters
Read vs write ratio?500K aggregate reads/sec vs 200K new reviews/day, requiring aggressive caching of aggregates.
Exact aggregate or eventual?A 5-minute TTL cache lag is acceptable on product page stars.
Full-text search in reviews?Elasticsearch powers search within product reviews.
One review per user per product?UNIQUE constraint ensures single review with update policy rather than duplicate rows.

Scope

In scope

  • Write/read reviews
  • Star aggregates + distribution
  • Helpful votes
  • Verified purchase
  • Seller responses
  • Spam rate limits

Out of scope (state explicitly)

  • Product catalog and inventory service
  • Checkout and payment processing
  • ML fraud pipeline detail

Functional Requirements

Start by confirming whether verified-purchase badges, media attachments, or fake-review detection are in scope. Ask how fresh aggregate ratings must appear on the product page.

  • Write reviews: Text reviews with 1-5 star rating for products/services
  • Photo/video reviews: Attach media to reviews
  • Helpful votes: Other users vote reviews as "helpful" or "not helpful"
  • Verified purchase badge: Mark reviews from actual buyers
  • Aggregate ratings: Average rating, rating distribution histogram, total count per product
  • Sort/filter reviews: By recency, helpfulness, rating, verified purchase
  • Seller responses: Sellers can respond publicly to reviews
  • Edit/delete own reviews: Users manage their own reviews
  • Anti-abuse: Detect and filter fake/spam reviews via ML + rules
  • Review summary: AI-generated summary of common themes across reviews

Non-Functional Requirements

Interviewers focus primarily on read latency on product pages and eventual consistency for star aggregates. Emphasize pre-computed ratings, as performing live COUNT operations across billions of rows per page load is completely infeasible.

  • Low Latency: Aggregate ratings in < 50 ms (shown on every product page)
  • Eventual Consistency: New review reflected in aggregates within 5 minutes
  • Scale: 500M+ products, 5B+ reviews, 200K new reviews/day
  • Availability: 99.99% read; 99.9% write
  • Fraud Resistant: Detect and suppress fake reviews within hours

Capacity Estimations

Run this math before you size Elasticsearch. Total review count and new reviews per day tell you index growth; product-page read QPS drives Redis aggregate cache sizing.

MetricCalculationValue
Total productsGiven500M
Total reviewsGiven5B
New reviews / dayGiven (assumption documented in value)200K
Review reads / secDerived from daily volume ÷ 86400 (+ peak factor)100K
Aggregate rating reads / secDerived from daily volume ÷ 86400 (+ peak factor)500K
Total review storageGiven5 TB text + 100 TB media

Architecture Diagram

In the interview: emphasize that star ratings are pre-computed aggregates rather than live queries, and call out the five-minute eventual consistency window proactively.

The architecture cleanly decouples the write and read paths through an asynchronous event bridge. Review submissions persist durably in PostgreSQL before publishing change events to Kafka, which updates running aggregate counters in Redis and synchronizes Elasticsearch for faceted full-text search. Reads query Redis directly for sub-millisecond star ratings on high-traffic product pages. For order validation and fraud screening integrations, see the Order Management System and the Fraud Detection System.

Loading...

Component Deep Dives

Aggregate Rating: Pre-Computed Architecture

Pre-Computed Aggregates and Event Pipeline

Every product page view depends on sub-50 ms star ratings, which makes live SQL COUNT queries across billions of rows impractical. Review writes persist to PostgreSQL and fan out asynchronously through Kafka, maintaining a five-minute eventual consistency window for public star displays. For caching strategies, review Caching Patterns & Invalidation.

Event Bus Design (Kafka)

Topic: review-created
  Partitions: 64
  Partition key: product_id (preserves per-product aggregate ordering)
  Retention: 7 days
  Replication factor: 3, min.insync.replicas: 2

Producer: Review Service after PostgreSQL INSERT (idempotent producer)
  Event: { review_id, product_id, user_id, stars, body, media_urls[], timestamp }

Consumer groups:
  1. aggregate-worker: INCR product_ratings star_N, recompute avg, and update Redis rating:{product_id} with TTL 300s
  2. search-indexer: full-text index review body in Elasticsearch
  3. fraud-detector: ML and NLP spam and bot scoring, then flag for moderation
  4. notification: alert seller of new reviews
  5. ai-summary: weekly batch LLM summary per product (staff tier)

Topic: review-vote-events: async INCR helpful_count (avoids hot row contention on viral reviews)

Sync path: POST review -> INSERT PostgreSQL -> publish event -> return 201 Created
Async path: aggregate update in under 5s; reads hit Redis cache (99% hit rate)
DLQ: review-created-dlq; nightly batch recomputes aggregates from reviews table

Bayesian Average

Bayesian Average Rating Calculation

A single five-star review should not outrank a product with ten thousand positive reviews averaging 4.5 stars. Bayesian averaging pulls sparse rating distributions toward a global prior mean to prevent sample-size distortion.

Product A: 1 review, 5.0 avg. Product B: 10K reviews, 4.5 avg.
Simple average ranks A higher. That's statistically wrong.

Bayesian Average (what Amazon and IMDb use in production):
  bayesian_avg = (C * m + sum_of_ratings) / (C + total_reviews)
  
  C = 25 (minimum reviews required for confidence)
  m = 3.7 (global platform average rating)
  
  Product A: (25 * 3.7 + 5) / (25 + 1) = 3.75
  Product B: (25 * 3.7 + 45000) / (25 + 10000) = 4.498
  
  Product B correctly ranks higher. As review volume increases,
  the Bayesian average smoothly converges to the true sample average.

Fake Review Detection

Multi-Signal ML Fraud Detection

Abuse mitigation combines behavioral telemetry, order verification, text semantics, and graph network analysis to assign review confidence scores before public indexing.

Signals for ML fraud detection:

Behavioral:
  - Account age < 7 days and review posted: suspicious
  - 50 reviews in 1 day: automated bot behavior
  - All 5-star or all 1-star patterns: biased review history
  - Copy-pasted text across unrelated products

Purchase verification:
  - No purchase record: unverified review receiving lower aggregate weight
  - Purchased and returned same day: suspicious behavior

Text analysis (NLP):
  - Generic text: "Great product! Highly recommended!" without specific product attributes
  - Sentiment mismatch: enthusiastic text paired with a 1-star rating
  - Language similarity across reviews submitted from the same IP address

Network analysis:
  - Multiple reviews from the same IP or device identifier for the same product
  - Review rings: coordinated clusters of accounts mutually reviewing identical products
  
Timing:
  - Product suddenly receives 500 5-star reviews in 1 hour compared to baseline 5 per day

ML Model (XGBoost):
  Features: all behavioral, textual, network, and timing signals
  Output: P(fake) probability score
  
  P(fake) > 0.8: auto-suppress review and enqueue for human moderation
  P(fake) between 0.5 and 0.8: flag review with reduced weighting in aggregates
  P(fake) < 0.5: show normally
  
  Suppressed reviews are excluded from aggregate star distributions.
  Nightly batch reconciliation corrects totals for retroactively suppressed reviews.

API Design

Review and Rating REST APIs

The platform exposes RESTful endpoints for submitting reviews with attached media, paginating filtered reviews with pre-aggregated metadata, and voting on helpfulness.

Write Review

HTTP
POST /api/v1/products/{product_id}/reviews
{
  "rating": 4,
  "title": "Great quality, slow shipping",
  "body": "The product quality is excellent but shipping took 2 weeks...",
  "photos": ["https://cdn.../photo1.jpg"]
}
Response: 201 Created
{ "review_id": "rev-uuid", "status": "published" }

Get Reviews for Product

HTTP
GET /api/v1/products/{product_id}/reviews?sort=helpful&rating=4&verified=true&page=1&limit=10
Response: 200 OK
{
  "aggregate": {
    "avg_rating": 4.3, "total": 50000,
    "distribution": {"5": 30000, "4": 10000, "3": 5000, "2": 3000, "1": 2000}
  },
  "reviews": [
    { "review_id": "rev-uuid", "user": {"name": "John D.", "verified": true},
      "rating": 4, "title": "Great quality", "body": "...",
      "helpful_count": 234, "photos": [...], "created_at": "2026-03-10",
      "seller_response": {"body": "Thank you for your feedback!", "responded_at": "2026-03-11"} }
  ]
}

Vote Helpful

HTTP
POST /api/v1/reviews/{review_id}/helpful
{ "helpful": true }
Response: 200 OK
{ "helpful_count": 235 }

Common Error Responses

400 Bad Request: invalid input, missing required fields, or malformed JSON payload
401 Unauthorized: missing or invalid authentication token or API key
403 Forbidden: authenticated caller lacks required permissions for this resource
404 Not Found: requested resource ID does not exist
409 Conflict: duplicate write or version conflict, retry with a unique idempotency key
422 Unprocessable Entity: syntactically valid request failed semantic business validation
429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header
500 Internal Error: unexpected server failure, retry safely with an idempotency key
503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff

Data Model

PostgreSQL Schema: Reviews and Aggregates

PostgreSQL serves as the transactional system of record, enforcing uniqueness constraints per user and product alongside pre-aggregated rating counters and seller responses.

SQL
CREATE TABLE reviews (
    review_id       UUID PRIMARY KEY,
    product_id      VARCHAR(50) NOT NULL,
    user_id         UUID NOT NULL,
    order_id        UUID,
    rating          SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
    title           VARCHAR(200),
    body            TEXT,
    photos          JSONB,
    verified_purchase BOOLEAN DEFAULT FALSE,
    helpful_count   INT DEFAULT 0,
    unhelpful_count INT DEFAULT 0,
    status          ENUM('published','suppressed','pending_review','deleted') DEFAULT 'published',
    fraud_score     DECIMAL(4,3),
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ,
    UNIQUE (product_id, user_id),  -- one review per user per product
    INDEX idx_product (product_id, status, created_at DESC),
    INDEX idx_product_helpful (product_id, status, helpful_count DESC),
    INDEX idx_user (user_id, created_at DESC)
);

CREATE TABLE product_ratings (
    product_id      VARCHAR(50) PRIMARY KEY,
    avg_rating      DECIMAL(3,2),
    total_reviews   INT DEFAULT 0,
    star_1 INT DEFAULT 0, star_2 INT DEFAULT 0, star_3 INT DEFAULT 0,
    star_4 INT DEFAULT 0, star_5 INT DEFAULT 0,
    updated_at      TIMESTAMPTZ
);

CREATE TABLE review_votes (
    user_id         UUID NOT NULL,
    review_id       UUID NOT NULL,
    helpful         BOOLEAN NOT NULL,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    PRIMARY KEY (user_id, review_id)
);

CREATE TABLE seller_responses (
    response_id     UUID PRIMARY KEY,
    review_id       UUID NOT NULL UNIQUE,
    seller_id       UUID NOT NULL,
    body            TEXT NOT NULL,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

Redis: Caching and Deduplication

Redis caches hot aggregate rating hashes for sub-millisecond page rendering and tracks 24-hour helpful vote deduplication keys.

# Aggregate ratings cache
rating:{product_id}            --> Hash { avg, total, s1, s2, s3, s4, s5 }
TTL: 300

# Helpful vote dedup
voted:{user_id}:{review_id}    --> "1"
TTL: 86400

# Review count per user per day (rate limit)
review_rate:{user_id}:{date}   --> INT
TTL: 86400

Elasticsearch: Full-Text and Faceted Search

Elasticsearch indexes review bodies to power full-text keyword search and filtering by star ratings, verified purchases, and media attachments.

JSON
{
  "review_id": "rev-uuid", "product_id": "prod-123",
  "rating": 4, "title": "Great quality",
  "body": "The product quality is excellent but shipping was slow...",
  "verified": true, "helpful_count": 234, "created_at": "2026-03-10"
}
// Queries: "battery life" within reviews of product X
// Filter by: rating >= 4, verified only, sort by helpful

Fault Tolerance

ConcernSolution
Review written but aggregate not updated
  • Kafka at-least-once delivery to aggregation worker
  • idempotent UPDATE
Duplicate reviewUNIQUE(product_id, user_id) constraint prevents duplicates
Rating cache stale
  • TTL 5 min
  • invalidated on write
  • worst case 5-min lag
Fake review flood
  • Rate limit per user (max 5 reviews/day)
  • async ML detection
Vote spamOne vote per user per review (DB primary key constraint + Redis dedup)
Aggregate drift
  • Nightly batch: recompute all aggregates from reviews table
  • overwrite running values

Additional Considerations

Interview Walkthrough

  • 25-minute cut

    Skip arch50/arch75 depth unless staff.

    • Trust signals: verified-purchase badges and fraud filters (5 min)
    • Write path to PostgreSQL with asynchronous aggregate recomputation (6 min)
    • Product average rating updated on review events (5 min)
    • Helpful-vote ranking using Wilson score interval (5 min)
    • Moderation: ML toxicity scoring to route auto-rejects or review queues (4 min)
  • Start with trust: verified-purchase badges and one-review-per-order constraints prevent fake reviews from polluting rating signals.
  • Explain the write path to PostgreSQL as the transactional source of truth, followed by asynchronous indexing into Elasticsearch for full-text search and faceted filtering.
  • Cover aggregate rating updates: recompute product averages upon new review events and cache aggregates in Redis with write invalidation.
  • Walk through helpful-vote ranking with Wilson score intervals, balancing positive ratios against total sample size so a 10 out of 10 score outranks 100 out of 200.
  • Detail the moderation pipeline: ML toxicity scoring routes clear violations to auto-rejection, sends borderline cases to human moderation, and publishes approved reviews to Elasticsearch.
  • Discuss photo reviews stored in S3 with CDN edge delivery, indexing media metadata alongside text in Elasticsearch for media-only review filters.
  • Highlight the common pitfall of sorting reviews by raw helpful_count, which disproportionately favors older reviews and buries fresh, relevant product feedback.

Engineering Trade-offs

Review Ordering: Wilson Score Interval

Review ranking balances statistical confidence against recency, resolving the trade-off between raw helpful count sorting and verified satisfaction ratios.

Problem: Sorting by helpful_count biases the display toward older reviews that had months to accumulate votes.

Wilson Score balances helpfulness ratio with a statistical confidence interval:
  wilson = (p + z^2/(2n) - z*sqrt((p(1-p) + z^2/(4n))/n)) / (1 + z^2/n)
  
  p = helpful_votes / total_votes
  n = total_votes
  z = 1.96 (corresponding to 95% confidence)
  
  Review A: 10 out of 10 helpful -> wilson = 0.72
  Review B: 100 out of 200 helpful -> wilson = 0.46
  
  Review A correctly ranks higher despite having fewer absolute votes.
  Production platforms like Reddit and Amazon rely on Wilson score ranking for community feedback.

AI Review Summaries

Synthesizing key sentiment themes from tens of thousands of reviews allows shoppers to grasp common feedback immediately without reading long paginated lists.

Instead of reading 50K reviews, the product page displays:
  "Customers frequently mention excellent battery life (45%) and lightweight design (38%).
   Common complaints note slow charging (12%) and screen glare (8%)."

Implementation:
  1. Weekly batch processing per product for SKUs with more than 50 reviews
  2. Extract recurring topics from review text via BERT NER or LDA topic modeling
  3. Identify the top 5 positive and top 3 negative sentiment themes
  4. Generate a concise customer summary using an LLM
  5. Cache results in the product_review_summary table and Redis

Cost considerations:
  Approximately $0.01 per summary across 10M eligible products with over 50 reviews.
  One-time generation costs ~$100K, followed by weekly refreshes for active products at ~$20K per week.

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...