Price Recommendation Serving

A pricing decision that a channel manager cannot fetch in time is not a decision — it is a missed booking. Price recommendation serving is the low-latency layer that exposes computed rates to booking engines, channel managers, and internal dashboards, and it is where the correctness work of the rest of the Dynamic Pricing Rule Engines & Optimization pillar either reaches the market or dies in a queue. It consumes the guarded output of Rule-Based Pricing Engines and the optimized point from Yield Optimization Algorithms, and its whole reason for existing is to make those results available fast, fresh, and consistently.

Recommendation serving with cache invalidation Computed rates land in a cache served to channels; a demand or competitor event invalidates affected keys and triggers targeted recomputation. Serving with event-driven invalidation Engine computes rate Cache keyed store Serving API read path Channels fetch rates invalidation event

Taxonomy and schema governance

A served recommendation is a small, strongly typed record: property, room type, rate plan, stay date, the rate in minor units, a currency, a computed-at timestamp, and the engine version that produced it. The timestamp and version are not decoration — they let a consumer decide whether a rate is fresh enough to publish and let an auditor tie a booking back to the exact decision. The cache key is the tuple of state variables that fully determines a rate, so invalidation can be surgical: a demand shock on one room type for one date range invalidates exactly those keys and nothing else. Every record conforms to the rate taxonomy from Rate Plan Structuring & Mapping, which is what lets a channel manager map the served rate onto the correct OTA rate code without translation guesswork.

Ingestion and normalization

The write path takes finalized decisions — post-guardrail, post-promotion — and upserts them into the cache idempotently, keyed on the state tuple, so a recompute overwrites rather than duplicates. Rates are stored in integer minor units and a single canonical currency; per-channel currency conversion happens at read time against a versioned FX table so a stale exchange rate is detectable. Staleness is bounded by a freshness SLA attached to each key class: high-velocity dates carry a short TTL and are eagerly recomputed, while far-horizon dates tolerate a longer one. When an invalidation event arrives from the forecast or competitor streams, the affected keys are marked stale and enqueued for recomputation rather than served blindly.

Core implementation

A minimal serving layer built on FastAPI exposes a read endpoint backed by an in-process cache with TTL and version checks. It returns a 503 rather than a stale price when a key is past its freshness SLA and recomputation has not caught up — failing loud beats publishing a wrong rate.

python
from __future__ import annotations

import logging
import time
from dataclasses import dataclass

from fastapi import FastAPI, HTTPException

logger = logging.getLogger("pricing.serving")
app = FastAPI()


@dataclass(frozen=True)
class Recommendation:
    rate_cents: int
    currency: str
    computed_at: float
    engine_version: str


_CACHE: dict[tuple[str, str, str], Recommendation] = {}
_TTL_SECONDS = 900


def cache_key(property_id: str, room_type: str, stay_date: str) -> tuple[str, str, str]:
    return (property_id, room_type, stay_date)


@app.get("/rate/{property_id}/{room_type}/{stay_date}")
def get_rate(property_id: str, room_type: str, stay_date: str) -> dict:
    key = cache_key(property_id, room_type, stay_date)
    rec = _CACHE.get(key)
    if rec is None:
        logger.warning("cache miss for %s", key)
        raise HTTPException(status_code=404, detail="no recommendation")
    age = time.monotonic() - rec.computed_at
    if age > _TTL_SECONDS:
        logger.error("stale rate for %s (age %.0fs)", key, age)
        raise HTTPException(status_code=503, detail="recommendation stale")
    return {
        "rate_cents": rec.rate_cents,
        "currency": rec.currency,
        "engine_version": rec.engine_version,
        "age_seconds": round(age),
    }

Returning 503 on staleness pushes the freshness decision to an explicit contract instead of silently shipping an old number; the consuming channel manager can then fall back to its last known rate or retry, exactly as it would for any transient upstream failure.

Cross-system dependencies

The serving layer’s writers are the Rule-Based Pricing Engines, Yield Optimization Algorithms, and Promotional Discount Orchestration components. Its readers are the channel managers governed by Channel Manager Integration Patterns. Invalidation events arrive from the forecasting pillar and from competitor feeds delivered through Data Ingestion & OTA API Integration Workflows; when a channel prefers push over pull, the fan-out follows the Webhook vs REST Sync Patterns decision.

Operational governance

The serving layer is the most latency-sensitive stage in the pricing pipeline, so its SLOs are explicit: read latency percentiles, cache hit ratio, and rate freshness against the per-key SLA. Every served response is traceable to an engine version, and a canary compares a sample of served rates against a fresh recomputation to catch cache poisoning. Deployments are version-gated so a new engine version cannot serve until its recomputed rates pass a parity check against the incumbent. Alerting fires on a falling hit ratio (recompute can’t keep up), a rising 503 rate (freshness SLA being missed), and any divergence between served currency conversions and the versioned FX table.

Troubleshooting and failure modes

Symptom Likely cause Remediation
Channels publishing stale prices TTL too long for a high-velocity date Shorten per-key SLA; prioritize recompute queue
Spike in 503 responses Recomputation lagging invalidations Scale recompute workers; widen invalidation granularity
Wrong currency on a channel Stale or mismatched FX table version Pin FX version per response; alert on drift
Served rate differs from computed Cache not invalidated after engine change Gate deploy on parity canary; bump key version

Conclusion

Serving is production-ready when it is fast, honest about freshness, and traceable: surgical cache invalidation, a hard freshness contract that fails loud rather than shipping stale prices, and every served rate tied to the engine version that produced it. That is what lets the careful pricing decisions made upstream actually reach the market intact.