Engineering

Faster review pages at scale: How we cut database costs and increased performance for millions of travelers

See how targeted schema changes and smarter data access enabled GetYourGuide to scale reviews at 4x the traffic — driving performance, lowering costs, and creating a better traveler experience.

Primanshu Choudhary

Software Engineer

Key takeaways:

Reviews are one of the biggest trust signals on GetYourGuide. As we expanded how and where our review content appears, our ‘reviews read path’ (the chain of calls and queries required to show reviews on the page) saw traffic increase by 4x. In this post, Software Engineer Primanshu Choudhary walks through how we redesigned it to keep latency steady, simplify the architecture, and optimize costs.

{{divider}}

Overview

When pages load slowly, the travelers browsing them may hesitate or drop off. As we expanded our Activity Details Page (ADP) to feature more user-generated content, namely reviews from other travelers, our “reviews read path” experienced a massive 4x surge in request traffic. This drove our database query volume up from 300 million to over 1.3 billion per day. 

This traffic spike posed a major technical challenge. It exposed critical bottlenecks in our read path, resulting in significant latency volatility and a 5.5x increase in database infrastructure costs.

Instead of endlessly scaling up database hardware or introducing fragile caching layers, we decided to redesign the reviews read path using targeted denormalization. By co-locating highly accessed “hot” fields into a single primary table and eliminating over 23 relational JOINs, we achieved:

  • 4.6x improvement in p99 latency (reduced from 140ms to 30ms).
  • 30x reduction in database infrastructure costs (dropping from ~$6,000/month to ~$200/month, saving ~$70,000 annually).
  • Elimination of cache maintenance overhead and database statement timeout errors.

If you’re in engineering, management, or product, dive in below to see how we achieved these significant improvements and the challenges we overcame along the way.

The challenge: Rapid growth and production bottlenecks

Reviews serve as a cornerstone of marketplace trust at GetYourGuide. Customers rely heavily on community feedback (i.e., reviews) to evaluate activities before booking. However, the general access pattern of review platforms is inherently asymmetric: users consume vast quantities of reviews but write relatively few.

Figure 1: Growth in database queries 300M/day to 1.3B/day

As part of product expansion, engineering integrated review data across multiple new components on the Activity Details Page (ADP). Coupled with organic platform visitor growth, the service faced increased demand:

  1. Number of requests to the service. This grew from ~60M/day to 240M/day
  2. DB query volume surge. Daily database queries escalated from 300 million to 1.3 billion (~4x growth).
  3. Latency volatility. As database CPU utilization spiked, user experience degraded due to severe response latency variance.
  4. Infrastructure cost escalation. To maintain service availability under heavy load, the number of database reader nodes was scaled from 2 to 4. Consequently, monthly database costs increased ~5.5x.
Metric Legacy State Scaled Peak
(Pre-Redesign)
Daily requests for service 60 million 240 million (4x)
Daily DB Queries 300 million 1.3 billion (4x)
Database Reader Nodes 2 readers 4 readers (2x)
Monthly DB Cost $1,070 $5,890 (5.5x)
p99 Latency ~45ms baseline 140ms spike

Dissecting legacy read path

Figure 2: Legacy Read Path 

Bottleneck A: Monolithic Endpoint Responsibilities

The primary reviews endpoint served as a monolithic controller, executing search, sorting, multi-criteria filtering (e.g., star-rating splits, language filters), pagination, fallback routing, and review-statistics computation within a single request context.

Solution

Introduced a separate endpoint to handle review statistics requests, and kept all other existing functions in the primary endpoint.

Bottleneck B: Expensive Pagination Operations

To display pagination information such as page, offset, and remaining review count, queries execute a COUNT(*) operation on each request to compute the total number of eligible reviews for the user’s filter settings.

Solution

We fetch the requested number of reviews, let’s say 10, and one extra to check whether there are more reviews, and simply return a nextPageAvailable flag. This works with systems where users scroll to discover more reviews.

Bottleneck C: Heavy JOINs

The legacy database schema adhered strictly to third normal form (3NF). Every review fetch required executing relational JOIN operations across three distinct tables:

  • review (primary metadata)
  • review_rating (rating scores - overall rating, guide, value for money, etc)
  • review_text (translated and original text comments)

Legacy read query strategy (simplified example to show JOINs)

SQL

SELECT r.id, r.created_at, rr.overall_rating, rt.original_comment,
rt.language_code
FROM review r
JOIN review_rating rr ON r.id = rr.review_id
JOIN review_text rt   ON r.id = rt.review_id
WHERE r.activity_id = :activityId
ORDER BY r.created_at DESC
LIMIT :limit OFFSET :offset;

The insight: Not all fields are equally hot

The redesign started with access-pattern analysis. Most of the read traffic was concentrated on a small subset of fields.

  • About 90% of queries on review_text were specifically for original_comment and language_code.
  • About 70% of queries on review_rating were specifically for overall_rating.

Because customer reviews are immutable (non-editable once submitted), the read-to-write ratio was heavily skewed toward reads. This presented a strong case for selective field denormalization.

When a service repeatedly needs the same small set of attributes, keeping those fields separate across multiple tables forces the database to do extra work on every request by performing expensive JOINs. Even when the right indexes are in place, you can’t skip the JOIN.

Technical solution: Strategic denormalization

Denormalization is an optimization strategy in which redundant data is intentionally co-located within target tables to eliminate complex JOINs during data retrieval and to optimize highly accessed read paths.

Schema redesign

We expanded the primary review table to co-locate the three highly retrieved fields directly:

  1. overall_rating (from review_rating)
  2. original_comment (from review_text)
  3. language_code (from review_text)

Extended Single-Table Schema

SQL

ALTER TABLE review
ADD COLUMN overall_rating SMALLINT,
ADD COLUMN original_comment TEXT,
ADD COLUMN language_code VARCHAR(10);

Redesigned read path

The earlier read path (Figure 3) was simplified, and we could now serve all operations directly via the review table. No more heavy JOINs, and we could now contribute to a simplified read path more easily.

Figure 3: Read path after denormalisation

The new read path made our lives simpler in two ways:

  1. Previously, to serve top reviews quickly, we maintained a separate cached table. That meant every read had to account for multiple sources: first checking the cache, then falling back to the source tables, along with logic to handle cache misses and to spawn an async thread to update the cache later.
  2. With the denormalized fields, query performance on the source-of-truth review tables became fast enough that we no longer needed this caching layer. As a result, we removed the cache table entirely and now serve reviews from a single source (i.e., review table). Eliminating the cache also removed the fallback and reconciliation logic, significantly simplifying the read path.

By co-locating frequently accessed review content and rating fields with the main review record, the system avoided repeated multi-table lookups in the read path.

How did we preserve data integrity and prevent drift?

Introducing the same fields in multiple places, i.e, redundancy, introduces the risk of data divergence, and to prevent it, we applied two rules:

  1. Disabling setters for denormalized fields: Direct updates to denormalized fields were turned off in the application code. We achieved this via disabling public setters for these fields.
  2. Enforce a single write path to prevent divergence: The synchronization is handled at the setter level. When the source value changes (e.g., a rating update), the setter automatically updates the corresponding denormalized field as part of the same write operation. Direct updates to the denormalized field are disabled, so there’s only one path that can modify it.

We traded a small amount of write complexity for a much simpler read path and removed an entire caching layer. Since the reviews service is heavily read-oriented, we optimized for reads.

Zero downtime rollout strategy

The migration was done incrementally rather than as a risky big-bang rewrite. The rollout sequence below is a good example of operationally safe database evolution.

  1. Ingestion enablement: Created columns for denormalized fields and enabled populating them for all new reviews.
  2. Historical data backfill: Executed chunked migration script to backfill denormalized values for 30 million historical review records with the right batch size and update statement timeouts to prevent indefinite locking.
  3. Vacuum and analyze: Ran VACUUM on the review table to clean up dead tuples generated during massive batch updates, followed by ANALYZE to update table statistics so the query planner can use the latest stats.
  4. Introduce indexes: We introduced the correct indexes and ran EXPLAIN ANALYZE to ensure the query planner reliably selected them.
  5. Query rewrite and cutover: We switched read path queries to fetch directly from the denormalized review table and decommissioned the cache-table retrieval path.

Production impact and business wins

Following full production deployment, metrics across performance, cost, and developer experience improved substantially:

Win 1: Latency reduction

  • p99 latency reduced by 4.6x, dropping from 140ms down to 30ms.
  • similarly p95 latency improved from ~80ms → ~30ms
Figure 4: Latency comparison pre- and post-denormalisation showing substantial flattening of p99 latency spikes.

Win 2: 30x infrastructure cost reduction

Eliminating JOIN operations and reducing CPU cycles enabled downscaling database instance sizes and reader node counts. Monthly database costs dropped from ~$5,890 to ~$200, realizing approximately $70,000 in annual recurring savings.

Figure 5: Monthly database infrastructure cost comparison showing a 30x drop.

Win 3: System architecture and operations simplification

  • Cache removal: Eliminated pre-filling cache routines and background maintenance.
  • No more query timeouts: Eliminated database query timeouts for complex multi-filter requests on the Activity Details Page.
  • Deterministic data consistency: Serving reads directly from the relational source of truth eliminated stale cache anomalies for end users.
Metric / Metric area Before optimization After optimization
p99 response latency 140 ms 30 ms (4.6x faster)
p95 response latency 80 ms 30 ms (2.6x faster)
Monthly infrastructure cost $5,890 / month $200 / month (~30x less)
Cache layer overhead High maintenance Decommissioned

When not to denormalize

Denormalization is an optimization technique, not a default design strategy. Engineering teams should avoid denormalization when…

  • Write volume dominates. The system handles high write or update throughput, where maintaining duplicate fields introduces excessive lock contention or write amplification.
  • When data changes frequently. Frequent updates to source data require propagating changes across multiple tables, increasing the risk of data divergence if the right transactions aren’t in place.
  • Reads aren’t a bottleneck. Denormalization is specifically to optimize reads; if they aren’t a bottleneck, we have no reason to denormalize.

Key takeaways and conclusion

  1. Co-locate hot fields for read-heavy workloads. If your service is read-heavy, look for the hot fields and co-locate them so your service and database have to do less work retrieving them.
  2. Design around your data. Identify access patterns in your service to cater to its unique needs.
  3. More traffic doesn’t mean more infrastructure. Increased traffic doesn’t always mean you need to increase server capacity; it could be a temporary solution while you address the inefficiencies.
  4. Make targeted, incremental improvements. Substantial performance gains do not always require a big rewrite; it might be hiding in plain sight.

The end result was a simpler reviews read path with meaningfully lower tail latency and far lower database spend. By focusing on real access patterns and making a deliberate trade-off in favor of read performance, we also made the experience more reliable for customers who just want answers fast when deciding what to book. 

If you enjoy scaling high-traffic systems with practical engineering, clear metrics, and smart data design, we’d love to meet you! Find your best fit on the GetYourGuide tech team here.

‍

Quick Tip

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
01/05

Highlights

01/05