What Is a Product Reviews Service?
A product reviews service lets buyers leave written feedback and star ratings on items they have purchased. Its core challenges are gating reviews behind verified purchases, detecting spam and incentivized reviews, collecting helpfulness votes, and ranking the most useful reviews to the top of the display list.
Requirements
Functional Requirements
- Allow only buyers with a confirmed order for the item to submit a review (verified purchase check).
- Accept a star rating (1-5) and optional text up to 5,000 characters.
- Run automated spam and quality scoring before publishing; hold borderline reviews for human moderation.
- Let any user vote a review as helpful or unhelpful (one vote per user per review).
- Rank and paginate reviews using a helpfulness-weighted score.
Non-Functional Requirements
- Review submission latency under 500 ms at p99; display latency under 200 ms from cache.
- Support 100,000 reviews submitted per day with burst tolerance during sales events.
- Spam detection must catch at least 95% of bot-generated reviews without false-positiving real buyers.
Data Model
- review: review_id, product_id, user_id, order_id, rating, title, body, status (PENDING, PUBLISHED, HELD, REJECTED), spam_score, quality_score, helpful_votes, unhelpful_votes, created_at, published_at.
- verified_purchase: user_id, product_id, order_id, delivered_at. Denormalized from the order service for fast lookup without a join.
- review_vote: vote_id, review_id, user_id, vote_type (HELPFUL, UNHELPFUL), created_at. Unique constraint on (review_id, user_id).
- review_image: image_id, review_id, s3_url, sort_order.
Core Algorithms
Verified Purchase Check
On submission the service looks up the verified_purchase table for (user_id, product_id). If no row exists the review is rejected with a 403. The verified_purchase table is populated asynchronously when an order moves to delivered status. For multi-item orders one row is written per product. This avoids calling the order service synchronously on every review submission, keeping latency predictable.
Spam Detection Pipeline
After acceptance the review is placed on an async processing queue. A pipeline applies several signals: duplicate text similarity against recent reviews for the same product (MinHash LSH), review velocity per user account (more than three reviews per hour is suspicious), user account age and purchase history richness, and an ML text classifier trained on labeled spam/ham examples. Each signal contributes a weighted sub-score; the combined spam_score gates the final status. Reviews scoring above a high threshold are auto-rejected; those in a middle band go to a human moderation queue; those below the low threshold are auto-published.
Helpfulness Ranking
The display ranking uses a Wilson score lower bound on the helpful/unhelpful vote ratio. This avoids promoting reviews with one helpful vote and no unhelpful votes over reviews with 95 helpful and 5 unhelpful. The formula computes a confidence interval lower bound: score = (p_hat + z^2/(2n) - z*sqrt((p_hat*(1-p_hat)+z^2/(4n))/n)) / (1 + z^2/n) where p_hat is the helpful ratio, n is total votes, and z = 1.96 for 95% confidence. Scores are recomputed on each vote and stored in the review row for fast sort.
API Design
POST /products/{product_id}/reviews— submit review; triggers async spam check; returns review_id and PENDING status.GET /products/{product_id}/reviews?sort=helpful&page=1— paginated list; served from cache with short TTL.POST /reviews/{review_id}/votes— cast helpful/unhelpful vote; idempotent per user.GET /reviews/{review_id}— single review detail including images and vote counts.DELETE /reviews/{review_id}— author or admin soft-deletes a review (status -> DELETED).
Scalability and Reliability
Caching Review Lists
The paginated review list for each product is cached in Redis with a TTL of 60 seconds. The cache key includes product_id, sort order, and page number. On a new review being published or a vote being cast the relevant cache keys are invalidated. For high-traffic products a read-through cache with stale-while-revalidate reduces origin load.
Async Spam Processing
Spam detection is decoupled from the write path. The submission endpoint writes the review with PENDING status and publishes a ReviewSubmitted event to Kafka. The spam pipeline consumes this event, runs all checks, and updates the review status. A reviewer can see their review immediately in a PENDING state; it appears in public listings only once PUBLISHED. This keeps submission latency under 100 ms regardless of spam model inference time.
Vote Deduplication
The review_vote table has a unique constraint on (review_id, user_id). The vote endpoint uses an upsert (INSERT … ON CONFLICT DO UPDATE) to handle the case where a user changes their vote type. The helpful_votes and unhelpful_votes counters on the review row are updated atomically in the same transaction using conditional increments, avoiding a full recount on every vote.
Trade-offs and Interview Discussion Points
- Synchronous versus asynchronous spam gating: async allows fast submission UX but means spam briefly enters the PENDING state. Synchronous gating eliminates this but adds latency and couples the write path to the ML model.
- Storing vote counters on the review row versus computing from review_vote: denormalized counters are faster to read but require careful atomic updates. A pure compute approach is always accurate but does not scale for products with millions of votes.
- Wilson score versus simple helpful ratio for ranking: simple ratio favors low-vote reviews. Wilson score rewards volume and consistency, which better reflects review utility for buyers.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you gate product reviews to verified purchasers?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “At review submission time, query the orders service for a fulfilled order line matching the reviewer's user ID and the product ID. Only proceed if a match exists with a shipped or delivered status. Store a verified_purchase boolean on the review record and surface it in the UI. Token-based review links sent post-delivery can pre-authorize the check to reduce friction.”
}
},
{
“@type”: “Question”,
“name”: “How does MinHash LSH detect duplicate or spam reviews?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Tokenize each review into shingles (e.g., 3-word n-grams) and compute a MinHash signature of fixed length. Divide the signature into bands and hash each band into a bucket. Reviews that land in the same bucket for any band are candidate duplicates. Compute Jaccard similarity on those candidates; reviews above a threshold (e.g., 0.8) are flagged as spam or near-duplicates. This runs in sub-linear time against the full review corpus.”
}
},
{
“@type”: “Question”,
“name”: “How does Wilson score ranking work for review helpfulness?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Wilson score computes the lower bound of the 95% confidence interval for the true helpful-vote proportion given observed up-votes and total votes. It naturally handles low-vote-count reviews conservatively — a review with 1 helpful vote out of 1 ranks lower than one with 90 helpful votes out of 100. This prevents gaming by reviews with a single vote and surfaces genuinely useful content.”
}
},
{
“@type”: “Question”,
“name”: “How do you paginate a high-volume product review list efficiently?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Use keyset (cursor) pagination rather than OFFSET. The client sends the last seen review's sort key (e.g., helpfulness_score + review_id). The query uses a WHERE clause on that composite key with an index that matches the sort order. This keeps page fetch time O(1) regardless of page depth, unlike OFFSET which requires scanning all prior rows.”
}
}
]
}
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering
See also: Shopify Interview Guide