What Is a Ratings Aggregation Service?
A ratings aggregation service computes summary statistics over individual star ratings for products, sellers, or any entity. It must handle high write throughput, produce accurate averages even for items with few ratings, and expose a star distribution histogram for the frontend.
Requirements
Functional Requirements
- Accept individual rating events (1-5 stars) from the reviews service.
- Compute and expose weighted average rating per entity.
- Apply Bayesian smoothing so new items with one or two ratings are not surfaced with misleading extremes.
- Provide a star distribution histogram (count per star value).
- Support rating recalculation when a review is removed or its rating is amended.
Non-Functional Requirements
- Aggregated rating reads must have p99 latency under 10 ms (served from cache or in-memory store).
- Rating updates should propagate to the displayed average within 30 seconds.
- System must handle millions of products, each potentially receiving hundreds of simultaneous ratings during flash sales.
Data Model
- rating_summary: entity_id, entity_type (PRODUCT, SELLER), total_count, sum_of_ratings, star_1_count, star_2_count, star_3_count, star_4_count, star_5_count, bayesian_avg, updated_at.
- global_prior: entity_type, prior_mean, prior_weight (C, the confidence parameter). Updated nightly from the full catalog average.
- rating_event_log: event_id, entity_id, entity_type, review_id, old_rating (nullable), new_rating (nullable), event_type (ADD, UPDATE, REMOVE), created_at. Used for audit and reprocessing.
Core Algorithms
Incremental Counter Updates
Rather than recomputing the average over all raw ratings on every event, the service maintains running counters: total_count and sum_of_ratings. On ADD: increment total_count by 1 and sum_of_ratings by the rating value, and increment the corresponding star_N_count. On REMOVE: decrement both. On UPDATE: adjust sum_of_ratings by the delta and swap star bucket counts. The simple average is sum_of_ratings / total_count. These increments are applied atomically using database row-level locks or Redis HINCRBY commands.
Bayesian Smoothing
A raw average on an item with two five-star reviews looks identical to an item with 2,000 five-star reviews, which misleads buyers and search ranking. Bayesian smoothing pulls the estimate toward the global prior when evidence is sparse. The formula is:
bayesian_avg = (C * prior_mean + sum_of_ratings) / (C + total_count)
Where C is the prior weight (typically set to the average number of ratings per product, e.g. 25) and prior_mean is the global average rating for the entity type. As total_count grows, the Bayesian average converges to the true mean. C and prior_mean are read from the global_prior table at update time.
Star Distribution Histogram
The star_1_count through star_5_count columns are updated alongside the running sum. The API returns these as an array with percentage shares. This avoids a GROUP BY query over millions of rows on each product page load. The histogram is computed from the counters in O(1).
API Design
GET /ratings/{entity_type}/{entity_id}— returns bayesian_avg, total_count, star distribution array.GET /ratings/bulk— body: [{entity_type, entity_id}, …] up to 100 entities. Returns map of id -> summary. Used by product listing pages.POST /ratings/events— internal endpoint; accepts ADD, UPDATE, REMOVE events from the reviews service. Idempotent by review_id + event_type.POST /ratings/recompute/{entity_id}— admin endpoint to trigger a full recount from the rating_event_log for correction after data issues.
Scalability and Reliability
Redis as Primary Store for Hot Items
For products with high read traffic the rating_summary is mirrored into Redis hashes. Reads go to Redis first. Counter updates use HINCRBY on the Redis hash in the same operation as the database write. If Redis is unavailable writes fall through to the database only; the cache is rebuilt on the next read miss. This two-layer approach handles tens of thousands of reads per second per popular product.
Event-Driven Aggregation
The reviews service publishes RatingAdded, RatingUpdated, and RatingRemoved events to Kafka. The aggregation service consumes these events and applies incremental updates. Consumer lag is monitored and alerted if it exceeds 30 seconds. For burst traffic (a product going viral) the consumer group can be scaled horizontally; each partition is handled by one consumer, so concurrency within a product (same partition key = entity_id) remains safe.
Nightly Prior Recalculation
The global prior mean and weight are recomputed nightly from all published ratings in the catalog. This ensures Bayesian smoothing reflects the current catalog distribution. The job updates the global_prior table and publishes an event that triggers a batch update of all product bayesian_avg values. High-traffic products get their cache invalidated immediately; long-tail products are lazily recomputed on the next read.
Trade-offs and Interview Discussion Points
- Counter-based versus event-sourced aggregation: counters give O(1) reads but can drift if events are replayed or lost. Full event replay from the log always produces correct results but is slow at scale. A periodic reconciliation job bridges the gap.
- Bayesian prior weight C: a higher C makes the smoothing more conservative (new items look more like the catalog average for longer) which reduces gaming via early review stuffing but delays accurate representation of genuinely exceptional products.
- Separate service versus embedded logic: a dedicated aggregation service allows independent scaling and reuse across products, sellers, and drivers. Embedded logic in the reviews service is simpler but creates coupling and duplication if ratings exist for multiple entity types.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does an incremental counter model work for ratings aggregation?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Maintain a denormalized row per product with columns: total_ratings, sum_of_scores, and counts per star level (count_1 through count_5). On each new or updated rating event, issue a single atomic UPDATE incrementing the relevant counters. Average rating is derived as sum_of_scores / total_ratings at read time. This avoids re-aggregating raw rows and keeps writes O(1).”
}
},
{
“@type”: “Question”,
“name”: “How does Bayesian smoothing prevent misleading ratings for items with few reviews?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Bayesian smoothing blends an item's observed average with a global prior: smoothed_score = (C * m + sum_of_scores) / (C + total_ratings), where m is the global average rating and C is a confidence constant (typically the median review count). Items with few ratings are pulled toward the global mean, preventing a single 5-star review from ranking a product above well-reviewed competitors.”
}
},
{
“@type”: “Question”,
“name”: “How do you compute the star distribution histogram for a product's ratings?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Read the five counter columns (count_1 through count_5) from the aggregation row. Divide each by total_ratings to get percentages. Because these are precomputed counters, computing the histogram is a single indexed row read with no GROUP BY scan required, making it suitable for high-traffic product pages.”
}
},
{
“@type”: “Question”,
“name”: “What is the right cache invalidation strategy for ratings aggregates?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Write-through caching with a short TTL (30–60 seconds) is the pragmatic baseline. On a ratings write, update the database counters and immediately delete or update the cached aggregate for that product ID. For very high traffic products, use a write-coalescing approach: buffer writes in Redis with INCR, flush to the database on a short interval, and keep the cache always warm. This bounds stale data while absorbing write spikes.”
}
}
]
}
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering
See also: Shopify Interview Guide