← All writing
articleAug 31, 202519 min read

Dynamic Pricing for Travel: The Model Is the Easy Part

Revenue management for travel — demand forecasting, price fences, the difference between dynamic and surge, and why published prices must be honoured.

PricingForecastingOptimizationArchitecture
Dynamic Pricing for Travel: The Model Is the Easy Part cover illustration

Dynamic pricing gets the attention, but in a travel business the forecasting and the fare-management mechanics are the engineering, and the legality of changing a price that someone has already seen is a product and legal constraint that outranks the model. The most important property of the system is that a published price is a promise, and everything else is downstream of honouring it.

The scale, and the two problems

  PROBLEM A: revenue management
    a hotel, 200 rooms, 365 nights
    - a booking horizon of up to 365 days
    - prices set days-to-weeks ahead
    - demand is a function of season,
      events, day of week, and how
      full the hotel is expected to be
    - objective: maximise RevPAR
      (revenue per available room)
      or total contribution margin

  PROBLEM B: ride/delivery surge
    a city, 12,000 drivers
    - a price for the NEXT request
    - supply and demand are observable
      in near real time
    - objective: clear the market,
      then recover
    - and it is REGULATED in many
      jurisdictions

  the differences that matter
    - horizon: 365 days vs 30 seconds
    - observability: forecast vs
      measured
    - the customer relationship:
      one booking vs thousands per day
    - regulation: mostly none vs
      substantial

Conflating the two is the most common design error in a travel company’s pricing stack, and it produces a system that is either too slow to clear a surge market or too aggressive for a hotel. They belong in separate services with separate objectives, separate failure tolerances, and separate governance. A hotel’s revenue manager must not be able to quadruple a price on a Tuesday because the forecast changed; a ride-hailing surge price must be able to move in thirty seconds.

Forecasting, the actual hard part

  the booking curve
    for a given date (a "flight", a
    "room night"), bookings accumulate
    over the booking window

    365 days out:  1% booked
    90 days out:    8% booked
    30 days out:   25% booked
    14 days out:   45% booked
    7 days out:    70% booked
    3 days out:    88% booked
    0 days out:   100% booked (or not)

  the model
    forecast_final_occupancy(date)
      = f(
          days_to_departure,
          historical booking curve
            for this date type,
          season, day of week,
          events, holidays, weather,
          current bookings,
          competitor prices,
          macro/region signals,
          remaining capacity )

  and the fare ladder maps
    remaining capacity + forecast ->
    a fare bucket

The structural difficulty is the long horizon. Thirty days out, the forecast is almost entirely prior — historical booking curves for similar dates, adjusted for season and events. The signal-to-noise ratio is terrible, and a model trained to optimise revenue at a seven-day horizon will be badly calibrated at 180 days. The correct architecture reflects that: a hierarchical model with a strong seasonal prior, explicit event and holiday features, and a live correction that grows in weight as the departure date approaches. And the uncertainty must be reported, because a fare ladder built on a 180-day forecast with a narrow confidence band is a false claim.

The other hard part is cannibalisation and substitution effects, which pure per-unit optimising gets wrong. Raising the price of room type A pushes demand to room type B, and if B is also priced optimistically the hotel captures neither. Optimising the set of prices jointly is a constrained optimisation problem per stay date, and the constraint set includes the fare ladder relationships (a suite must always cost more than a deluxe), the fence structure, and competitor position.

Fare ladders, buckets, and price fences

  a fare ladder (a "rate plan set")
    BAR (best available)          $180
    member-only                    $150
    prepaid, non-refundable       $130
    3-night advance purchase       $120
    corporate negotiated           $140

  the rules
    - a fare must be reachable by
      SOMEONE for the inventory to
      be sellable
    - a cheaper fare must not be
      publicly visible to customers
      who are not eligible for it
      -> that is a FENCE

  fences
    - advance purchase (book N days
      before)
    - minimum stay (2+ nights)
    - minimum/maximum nights
    - closed/open (a specific date
      range)
    - member / loyalty tier
    - geographic origin
    - channel (direct vs OTA)
    - corporate rate (requires a
      corporate ID)

  WHY fences are the whole design
    a revenue manager wants to sell
    a discounted inventory to a
    customer who will commit (advance
    purchase) while showing a higher
    price to a customer who will
    not

    without fences, the only way to
    do that is to publish a low
    price to everyone
    -> the "low" price becomes the
       reference price
    -> nobody will book the full
       fare
    -> the revenue manager's
       segmentation is worthless

The fence is the mechanism that makes revenue management possible, and removing fences is why dynamic pricing goes wrong in public. If a platform publishes its lowest available rate to everyone, then “dynamic pricing” collapses to a single lowest price, the ladder disappears, and the segmenting is gone. The engineering requirements for fences are that they are evaluated at price-display time and at purchase time — the price a customer sees must be fenced against that customer’s eligibility, and the same check must run at checkout so a customer cannot book a rate they were not entitled to.

Channel fences are the sharp edge. A direct channel and an OTA channel may have different rates, and showing a lower direct rate to a customer who arrived via an OTA triggers the affiliate’s rate parity concerns and, in some jurisdictions, regulatory attention. This is why fare files, net rates, and parity rules are a real subsystem rather than a configuration detail.

Quotes, and the promise problem

  the requirement
    a price shown to a customer is a
    commitment honoured for a defined
    window

  the machinery
    1. PRICE VERSION
       a fare is a versioned object
       fare_id, version, price, fences,
       valid_from, valid_to
       -> prices are IMMUTABLE once
          quoted
    2. QUOTE
       when a price is shown, create a
       quote:
       - the exact fare version
       - the fence evaluation for THIS
         customer
       - a validity window (e.g. 15 min
         for a basket, longer for a
         single item)
       - an id the checkout references
    3. CHECKOUT
       on purchase, look up the QUOTE,
       not the live price
       - honour it if the window is
         open and the fence still holds
       - the price is whatever the
         quote says, even if the live
         price has moved
    4. EXPIRY
       quote expires -> price may
       change -> tell the user

  the failure this prevents
    price shown $180
    20 seconds later the fare moves
    to $240
    user clicks buy
    checkout shows $240
    -> the platform has broken a
       commitment, and the customer
       knows it

This is the design decision with the highest legal and trust stakes. A live recalculation at checkout is technically simpler and it is what most naive implementations do, and it is a breach of the commitment the interface made. The quote object — immutable, versioned, fence-evaluated for a specific customer, with a validity window — is the mechanism that makes the promise real, and it also makes the whole thing auditable: for any completed sale there is a record of the exact fare version and the price shown.

An inverse-DOM and an integrity check apply here too: a tampered client must not be able to submit a lower price and have the server accept it, because the server’s job on checkout is to resolve the quote, not to trust the request.

Surge pricing specifically

  the mechanics
    equilibrium price
      p* = (expected demand
            x price elasticity) /
           (supply)

    in practice, a marketplace does
    not compute a single p*
    - it has a price ladder / grid
    - it moves a vehicle between
      "incentive" and "surge" bands
    - it changes by steps, not
      continuously (a jump from $12
      to $30 feels punitive; $12 ->
      $14 -> $17 does not)

  the rules that make it survivable
    - the multiplier is visible and
      explained (an ETA shown, a
      reason shown)
    - it is capped (many jurisdictions
      and most company policies cap
      the multiplier)
    - it rises gradually, with a
      ramp, not a cliff
    - it is applied to a quote
      (so the price the user saw is
      the price they pay)
    - and it decays: the surge must
      fall as fast as it rose, or the
      market loses trust in recovery

  the regulation
    - some cities cap the multiplier
      and require disclosure
    - some require an appeal
    - some prohibit surge on
      wheelchair-accessible vehicles
      (which is a specific, tested
      failure)
    - and the acceptance criteria
      should assume a regulator will
      ask for the exact multiplier
      applied to each trip

The “applied to a quote” requirement is the same machinery as the fare ladder’s, and reusing it means surge pricing inherits the price-honouring guarantee. The decay rule is the one that gets neglected: a surge that rises in ten minutes and takes an hour to fall is a system that overcharges, and the visible effect is riders waiting out the peak hoping the price drops. Ramp rates matter more than peak levels in determining whether a pricing system is regarded as fair.

Testing a pricing system

  the problem
    the feedback loop is delayed
    (bookings arrive days later)
    and ENDOGENOUS
    (raising the price changes the
     demand you then observe)

    so A/B testing a pricing change
    is not a clean experiment:
    the two arms see different
    demand conditions, and the
    high-price arm's lower volume
    looks like a demand difference

  what actually works
    1. OFFLINE EVALUATION
       replay historical demand for a
       rate period, and compute what
       the pricing policy would have
       done
       - biased, because demand under
         the old price is not demand
         under the new one
       - but useful for detecting
         catastrophic policy bugs and
         for comparing policies on
         the same demand
    2. SHADOW PRICING
       compute what a new policy
       WOULD have charged, alongside
       the live price, and log the
       counterfactual
       - no revenue risk
       - gives a distribution of
         "revenue we left on the table"
         or "we overcharged by"
       - does not tell you the new
         demand response, which is
         the whole point and remains
         unknown
    3. LIMITED ROLLOUT WITH
       GUARDRAILS
       - a small traffic slice
       - hard guardrails on price
         floors and ceilings
       - a pre-registered decision
         rule and a kill switch
       - and the honest acknowledgement
         that the result is confounded
         by selection and time

  what does NOT work
    - a naive A/B test on a pricing
      change, read as causal
    - optimising for a short-term
      revenue proxy (conversion)
      while ignoring the
      long-horizon response

The honest position, which should be written into the evaluation plan before the experiment rather than discovered afterwards: shadow pricing measures the revenue effect of a new price on existing demand but tells you nothing about how demand would respond, which is the actual question. Shadow volume — what fraction of sessions would have found the new price acceptable — is a much better proxy, and is measurable, and is what should be instrumented. Everything else requires a limited rollout with guardrails and a pre-registered decision rule.

Failure stories worth testing

Show $180, then let the fare move to $240 in 20 seconds, and check out

The system must honour the quote. This is the promise test and it is the one that matters most.

Let a client submit a lower price at checkout

It must be rejected and the quote resolved server-side. Never trust the request price.

Drop the advance-purchase fence and publish the lowest rate to everyone

Watch the fare ladder collapse to one price and the full-fare bookings fall. This is why fences exist.

Show a member-only rate to a non-member via a stale page

The fence must be evaluated at display time AND at purchase. Testing display only is the common gap.

Run a fare ladder with a suite cheaper than a deluxe

Fare ladder integrity must be validated as a whole, not per fare.

Forecast 180 days out with a model calibrated at 7 days

The confidence band is meaningless and the price ladder is noise. Check calibration by horizon.

Let a surge jump from $12 to $30 in one step

Compare to a ramped rise. The perception difference is large and the revenue difference is small.

Cap the surge at 2x and add a ramp

This is what most regulators and most sane companies converge on. Verify the cap is enforced in the quote, not just the display.

Decay a surge over an hour after it peaked in 10 minutes

Riders wait out the peak. This is the trust cost and it is measurable.

Run a naive A/B test on a price change and read the result as causal

It is confounded by time and selection. This test demonstrates why the offline tools exist.

Price two room types independently and ignore substitution

Watch demand move to the unoptimised one. Joint optimisation is the correct scope.

Let a competitor’s price feed into the model with a 6-hour lag

The model reacts to yesterday’s market. Lag is a first-class model parameter.

A production-ready architecture

   demand signals
     - bookings by stay date
     - searches / abandoned sessions
     - competitor prices
     - events, holidays, weather
     - seasonality, macro
        |
        v
  +----------------------------------------------------------+
  |  FORECAST SERVICE                                        |
  |  - per stay-date occupancy forecast                      |
  |  - hierarchical: seasonal prior + event + live weight    |
  |  - explicit uncertainty by horizon                       |
  |  - calibrated separately by horizon bucket               |
  +----------------------------+-----------------------------+
                               |
                               v
  +----------------------------------------------------------+
  |  FARE / RATE SERVICE (hotel, revenue management)         |
  |  - joint price optimisation per stay date                |
  |  - fare ladder with integrity constraints                |
  |  - fences: advance, LOS, member, channel, corporate     |
  |  - immutable FARE VERSIONS                                |
  +----------------------------+-----------------------------+
                               |
                               v
  +----------------------------------------------------------+
  |  QUOTE SERVICE (the promise layer)                       |
  |  - quote = fare version + fence eval + validity window   |
  |  - checkout resolves the QUOTE, never the live price     |
  |  - idempotent, auditable, expiry enforced                |
  +----------------------------+-----------------------------+
                               |
                               v
  +----------------------------------------------------------+
  |  SURGE SERVICE (marketplace, separate)                   |
  |  - equilibrium-ish estimate, grid + banded steps          |
  |  - ramped up AND down, hard caps, disclosure            |
  |  - reuse the quote service so the price is honoured      |
  +----------------------------+-----------------------------+
                               |
                               v
  +----------------------------------------------------------+
  |  EVALUATION                                              |
  |  - offline replay (bug detection, policy comparison)     |
  |  - shadow pricing (revenue + volume counterfactual)      |
  |  - limited rollout with guardrails and kill switch       |
  +----------------------------+-----------------------------+

  watch: quote expiry rate, checkout price variance vs quote,
        fence violations, surge ramp and decay symmetry,
        calibration by horizon, shadow-vs-live volume delta

Delivery checklist:

  1. Separate revenue management from surge pricing into distinct services. Different horizons, different objectives, different regulation, different failure tolerances.
  2. Build the forecast hierarchically with a strong seasonal prior and an explicit live weight that grows as the stay date approaches, and report uncertainty by horizon.
  3. Calibrate separately by horizon bucket. A model tuned at seven days is not a model for 180, and pretending otherwise produces a confident meaningless forecast.
  4. Implement fences as first-class fare attributes, evaluated both at price display and at purchase.
  5. Version fares immutably and route every purchase through a quote object with a validity window. Never recalculate at checkout.
  6. Reject client-submitted prices and resolve the quote server-side.
  7. Ramp surge prices in and out with symmetric decay, cap them, and disclose them.
  8. Reuse the quote service for surge so the price the rider saw is the price charged.
  9. Optimise room-type prices jointly within the fare ladder rather than per unit, and validate ladder integrity as a whole.
  10. Instrument shadow pricing for both revenue and volume, because shadow revenue does not capture the demand response.
  11. Pre-register the decision rule and kill switch for any limited rollout, and treat the result as confounded rather than causal.
  12. Monitor checkout-price variance against the quote as a correctness metric. Any non-zero unexplained variance is a bug.
  13. Version the competitor-price feed lag as a model parameter rather than letting staleness be implicit.
  14. Audit the multiplier applied to every surge trip, because a regulator will ask and the answer has to be reconstructable.

Common mistakes

Mistake What actually happens Better decision
Recalculating price at checkout The interface’s promise is broken Resolve an immutable quote
Trusting a client-submitted price Tampering, and no audit trail Server resolves the quote
Publishing the lowest rate to everyone Fare ladder collapses to one price Fences, enforced at display and purchase
One model for all horizons Confident nonsense at 180 days Hierarchical prior + live weight, calibrated by horizon
Pricing room types independently Substitution defeats the optimisation Joint optimisation, ladder integrity checks
Independent A/B test on price Confounded by time and selection Offline replay, shadow pricing, guarded rollout
Reading shadow revenue as the answer Demand response is the unknown, not revenue Shadow volume as the proxy
Surge as a single step Reads as punitive, caps invite regulators Ramped rise and decay, disclosed
Slow surge decay Riders wait out the peak; trust lost Symmetric ramp rates
Fences evaluated only at display Stale pages leak ineligible rates Evaluate at display and at purchase
Live price recomputed in the client Drift, and a promise nobody kept Quote is server-owned end to end
Ignoring competitor-feed lag Model reacts to yesterday’s market Lag as a first-class parameter
Fare ladder validated per fare Suite cheaper than deluxe ships Validate the ladder as a whole
No audit trail per sale Cannot answer a regulator or a dispute Quote + fare version on every sale

The complete story in one minute

Dynamic pricing is two different problems wearing one name. Revenue management prices against a demand curve over a 365-day booking horizon with mostly-forecast information; surge pricing prices against observable supply and demand in the next thirty seconds and is heavily regulated. Conflating them produces a system either too slow to clear a surge market or too aggressive for a hotel, so they belong in separate services with separate governance — and a hotel’s revenue manager must not be able to quadruple a price on a Tuesday because a forecast moved.

The model is the easy part. The hard part is that a seven-day forecast and a 180-day forecast are different problems, so the architecture is hierarchical: a strong seasonal prior, explicit event and holiday features, a live correction whose weight grows as the stay date approaches, and honest uncertainty reported by horizon. Calibrate per horizon bucket, because a model tuned at seven days is not a model for 180. Then optimise room-type prices jointly within the fare ladder, because independently optimised room types just push demand to the one you forgot.

Fences are what make the whole thing possible. A revenue manager needs to sell discounted inventory to a customer who will commit while showing a higher price to one who will not, and without fences the only way to do that is to publish the low price to everyone — at which point the ladder collapses, nobody books the full fare, and segmentation is worthless. Fences must be evaluated at price display and at purchase, and a fence that is only checked at display leaks ineligible rates through stale pages.

The highest-stakes design decision is the quote. A price shown to a customer is a commitment, honoured for a defined window, so fares are immutable versioned objects and every purchase resolves a quote — the exact fare version, fence-evaluated for that customer, with an expiry — rather than a live recalculation. Never trust a client-submitted price. Reuse the same machinery for surge, ramp the rise and the decay symmetrically, cap it, disclose it, and be able to reconstruct the multiplier applied to every single trip, because a regulator will ask.

And testing is genuinely awkward: the feedback loop is delayed and endogenous, so a naive A/B test on price is confounded. Offline replay finds catastrophic policy bugs, shadow pricing measures the revenue and volume counterfactual — volume matters because the demand response is the actual unknown — and a limited rollout needs guardrails, a kill switch, and a decision rule registered before the experiment rather than after.

revenue management and surge as separate services
hierarchical forecast, calibrated by horizon, uncertainty reported
fences enforced at display and purchase; joint ladder optimisation
immutable fares and server-owned quotes; never recalculate at checkout
ramped symmetric surge, capped, disclosed, auditable per trip
offline replay and shadow volume, not naive A/B on price

The hard part was never the forecast. It was that the moment you show someone a number, you have made a promise about it, and the whole system has to be built so that promise survives contact with a changing market.

What this team still owns

The model may recommend a price, but the offer service owns the customer commitment. Persist the currency, taxes and mandatory fees, eligibility fences, inventory context, model/rule version, expiry, and an opaque quote ID in one immutable offer. Checkout resolves that server-side offer; it never trusts a price returned by the browser. When regulation or policy differs by market, the governing rule set is versioned and effective-dated rather than hidden inside model code.

Technical references

Keep reading
Browse everything