An online travel agency processed 2.3 million flight searches per day. Each search triggered a pricing computation that determined the displayed fare for every matching itinerary. The pricing computation was a batch job that ran overnight, computing fare recommendations for all active itineraries based on current inventory, competitor pricing, demand forecasts, and margin targets. The batch output was stored in a lookup table. When a user searched for flights, the system read the pre-computed fare from the lookup table and displayed it.
The batch approach had a fundamental problem: the fare a user saw was computed on data that was between eight and twenty hours old. During that window, competitor prices changed, inventory levels shifted, and demand patterns evolved. The agency was displaying fares that were optimal for yesterday’s market, not today’s. Revenue analysts estimated that the agency was leaving four to seven percent of potential revenue on the table because their prices could not respond to real-time market conditions.
The business case for real-time pricing was clear: if the pricing engine could compute fares at search time, incorporating the latest competitive and demand signals, the agency could capture the revenue that the batch approach missed. The engineering case was harder: the pricing computation involved evaluating 400 pricing rules against an average of 3,000 itineraries per search, all within the 500ms latency budget that the search experience required.
The batch architecture’s constraints
The overnight batch job processed all active itineraries — approximately 2 million — and evaluated each against the 400 pricing rules. Total computation time was six hours on a cluster of thirty-two compute nodes. Extrapolating to per-search computation: 2 million itineraries times 400 rules divided by 32 nodes divided by 6 hours equals approximately 1,100 rule evaluations per second per node.
A single search, with 3,000 itineraries and 400 rules, required 1.2 million rule evaluations. At the batch processing rate of 1,100 evaluations per second per node, a single search would take over 1,000 seconds on one node, or 32 seconds on the entire batch cluster. Neither was acceptable. The search experience required sub-500ms response times.
The batch approach achieved overnight processing by amortizing the computation across all itineraries simultaneously. The per-search computation was far too expensive for real-time execution. The architecture could not be made faster by adding nodes — the per-search computation was inherently sequential because each rule evaluation depended on the result of prior rules.
The approach: pre-compute the framework, compute the details at search time
We redesigned the pricing engine to split the 400 rules into two tiers: framework rules and dynamic rules. Framework rules determined the pricing structure — base fare classes, advance purchase discounts, length-of-stay requirements, and seasonal adjustments. These rules changed slowly, on a cadence of days or weeks. Dynamic rules determined the real-time adjustment — competitive matching, demand-based surge pricing, inventory-driven scarcity pricing, and promotional discounts. These rules changed by the minute.
This diagram requires JavaScript.
Enable JavaScript in your browser to use this feature.
The overnight batch job continued to run, but its scope was reduced. Instead of computing final fares, it computed framework fares — the base price for each itinerary after applying the stable pricing rules. The framework fare was stored in the lookup table. At search time, the dynamic pricing layer read the framework fare and applied a small number of real-time rules — typically five to ten, not 400 — to compute the final fare.
The dynamic rules operated on a much smaller input space than the full rule set. The framework fare was a single number. The competitive price feed was a small lookup table updated every sixty seconds. The demand forecast was a pre-computed score for the route and date. The inventory level was a single integer. The dynamic pricing layer evaluated these inputs against five to ten rules and produced an adjusted fare. Total computation time was under 30ms.
This approach preserved the batch architecture’s ability to handle complex, sequential rule evaluation for the stable rules, while adding a real-time adjustment layer that was fast enough to meet the latency budget.
The competitive price feed
The most critical dynamic input was competitor pricing. The agency subscribed to a fare monitoring service that provided competitor prices for the same routes and dates. The feed was updated every sixty seconds and cached in a low-latency store.
The dynamic pricing rules used the competitor feed to implement three strategies: match, undercut, and premium. Match applied when the agency’s framework fare was within two percent of the competitor’s fare — no adjustment. Undercut applied when the competitor’s fare was lower — the agency reduced its fare to match minus a configurable margin. Premium applied when the agency’s fare was already lower — the agency held its price or increased it slightly to capture additional margin.
These three strategies covered approximately eighty percent of competitive pricing situations. The remaining twenty percent involved complex multi-competitor scenarios, route-specific promotions, or bundle pricing that required evaluation beyond the dynamic rule set. For these cases, the dynamic pricing layer fell back to the framework fare, accepting that real-time adjustment was not always possible within the latency budget.
What we gave up
The two-tier architecture could not apply all 400 rules at search time. Rules that required historical data spanning months or years — customer lifetime value adjustments, long-term demand trend analysis, loyalty program tier calculations — remained in the batch layer. A customer’s loyalty tier affected their fare, but the tier was computed overnight, not at search time. If a customer crossed a loyalty threshold during the day, their fare would not reflect the new tier until the next batch run.
The second trade-off was pricing consistency. With batch pricing, every user who searched for the same route at the same time saw the same fare. With real-time pricing, fares could change between searches as competitor prices and inventory levels updated. Users who searched, left, and returned minutes later might see a different fare. The agency addressed this with a fare lock feature — users could hold a fare for fifteen minutes — but the inconsistency was a user experience concern that the batch approach never had.
The third trade-off was operational complexity. The dynamic pricing layer added a new service that required monitoring, capacity management, and failure handling. If the competitor price feed went down, the dynamic pricing layer had to decide whether to fall back to framework fares or use stale competitor data. The team implemented a staleness threshold of five minutes — if the feed was older than five minutes, the system fell back to framework fares and logged the fallback for revenue analysis.
Results
Revenue per search increased by fourteen percent in the first quarter after deployment. The increase came from three sources: competitive undercutting captured price-sensitive customers who would have booked with competitors, scarcity pricing increased fares on high-demand routes as inventory decreased, and promotional pricing allowed same-day flash sales that were impossible with overnight batch computation.
The fourteen percent exceeded the four to seven percent that analysts had estimated, because the real-time engine enabled pricing strategies that the batch approach could not implement at all — not just strategies that the batch approach implemented poorly. Same-day flash sales and inventory-driven scarcity pricing were new capabilities, not improvements to existing capabilities.
Search latency increased by an average of 35ms — from 420ms to 455ms at p50. The dynamic pricing layer’s 30ms computation time was within the search service’s latency budget. No degradation in user experience was detected.
The decision heuristic
If your pricing computation involves rules that change at different cadences, do not compute everything at the same frequency. Separate the rules by their rate of change. Pre-compute the slow rules in batch. Compute the fast rules at request time. The batch layer handles complexity. The real-time layer handles freshness. The boundary between them is defined by the latency budget: any rule that can be evaluated within the budget should be evaluated at request time. Any rule that cannot should be pre-computed. The goal is not to make everything real-time. The goal is to make the things that need to be real-time fast enough to matter.