Price Engine Low-Level Design: Rule Evaluation, Currency Conversion, and Real-Time Price Updates

Price Engine Low-Level Design

A price engine computes the effective selling price for a product at request time by evaluating a prioritized set of rules: base price, customer tier discounts, volume breaks, promotional overrides, and multi-currency conversion. Interviewers focus on rule evaluation order, cache invalidation, and consistency under concurrent rule updates.

Requirements

Functional

  • Compute the effective price for a (product, variant, customer, quantity, currency) tuple.
  • Support rule types: base price, customer tier (bronze/silver/gold), volume break, flash sale override, and cost-plus markup.
  • Apply multi-currency conversion using configurable exchange rates refreshed from an external feed.
  • Allow merchants to schedule rules with start and end timestamps.
  • Provide a price explanation response listing which rules fired and their contributions.

Non-Functional

  • Price computation under 20 ms for a single (variant, customer) pair at the 99th percentile.
  • Rule updates propagate to all nodes within 30 seconds.
  • Consistent pricing: two requests for the same tuple within the same second must return the same price.

Data Model

  • price_rule: id (UUID), rule_type (BASE | TIER | VOLUME | SALE | COST_PLUS), priority (lower number wins), scope_type (PRODUCT | VARIANT | CATEGORY | ALL), scope_id, customer_segment_id (nullable), min_quantity (default 1), amount_type (FIXED | PERCENT), amount, currency, starts_at, ends_at, active.
  • exchange_rate: id, from_currency, to_currency, rate, source, fetched_at — latest rate per pair is the active one.
  • customer_tier: customer_id, tier (BRONZE | SILVER | GOLD), effective_from.
  • price_cache: key (VARCHAR, composite of variant_id + segment_id + currency), price_cents, rule_snapshot_version, computed_at, expires_at.

Core Algorithms and Flows

Priority-Ordered Rule Evaluation

At compute time the engine fetches all active rules matching the request context, ordered by priority ascending. Rules are filtered by: scope (variant ID, product ID, category path, or ALL), customer segment, quantity threshold, and active time window (starts_at and ends_at bracket the current timestamp). The evaluation loop applies each rule in priority order. FIXED rules set the price absolutely and halt further evaluation; PERCENT rules stack multiplicatively. The final result is the price after all applicable rules have been applied, along with a list of rule IDs and their contributions for the explanation response.

Multi-Currency Conversion

Rules are stored in the merchant base currency. After rule evaluation the engine looks up the latest exchange_rate for the requested currency pair and multiplies the computed base-currency price. Rates are refreshed from an external feed (e.g., ECB or Open Exchange Rates) every 15 minutes by a background job and written to the exchange_rate table. A materialized view maintains the latest rate per pair, eliminating a subquery at compute time.

Customer Tier Pricing

The customer_tier table is resolved at request time using the customer_id from the auth token. The tier is translated to a customer_segment_id that maps to TIER-type price rules. If no tier record exists the customer is treated as the default segment. Tier lookups are cached in Redis per customer_id with a 5-minute TTL, reducing database load for high-traffic authenticated sessions.

Cache Invalidation

The price cache key encodes variant_id, customer segment_id, and currency. A rule_snapshot_version counter is incremented on any price_rule write. Cached entries include the version at compute time; on cache hit the engine compares the stored version against the current global version (held in Redis as a single key). A version mismatch triggers a synchronous recompute and cache refresh. This approach avoids per-key invalidation fan-out when a broadly scoped rule (e.g., a sitewide sale) changes.

API Design

  • GET /v1/prices?variant_id={id}&quantity={n}&currency={c} — returns effective price for anonymous buyer.
  • GET /v1/prices?variant_id={id}&customer_id={cid}&quantity={n}&currency={c} — returns effective price for authenticated buyer with tier applied.
  • GET /v1/prices/{variant_id}/explain?customer_id={cid}&currency={c} — returns price breakdown listing all rules evaluated.
  • POST /v1/price-rules — creates a new pricing rule with scope, priority, and schedule.
  • PATCH /v1/price-rules/{id} — updates rule parameters; increments rule_snapshot_version.
  • DELETE /v1/price-rules/{id} — deactivates a rule; increments rule_snapshot_version.
  • POST /v1/prices/bulk — batch price lookup for up to 500 variant/customer pairs; used by cart rendering.

Scalability Considerations

  • The rule set for any given request context is small (typically under 20 rules); evaluation is CPU-bound and completes in microseconds, making the 20 ms budget dominated by network and cache latency rather than computation.
  • Rules are loaded into an in-process LRU cache on each engine node; the global version key in Redis provides a lightweight invalidation signal without requiring a pub/sub fan-out per node.
  • Bulk price endpoints for cart rendering are essential: fetching 500 prices individually at 20 ms each would take 10 seconds; bulk evaluation amortizes rule loading and exchange rate lookup across all variants in a single call.
  • Exchange rate fetches are isolated from the price compute path; stale rates (up to 15 minutes old) are acceptable and the background refresh job retries on failure without blocking price serving.
  • Flash sale rules with narrow time windows are pre-loaded by a scheduler 60 seconds before their starts_at time, preventing a thundering herd of cache misses at sale launch.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does priority-ordered rule evaluation work in a price engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Pricing rules are stored with an explicit priority integer. The engine fetches all rules matching the request context (SKU, customer segment, channel, date range) and sorts them ascending by priority. Rules are evaluated in order; the first matching rule that is marked exclusive stops further evaluation. Non-exclusive rules continue accumulating modifiers. This allows flash-sale rules (high priority, exclusive) to override base rules while still allowing additive surcharges from lower-priority rules when no exclusive match fires.”
}
},
{
“@type”: “Question”,
“name”: “How is multi-currency conversion handled in a price engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The engine maintains a currency rates table populated by a scheduled job that fetches rates from an FX provider (e.g., Open Exchange Rates). Base prices are stored in a canonical currency (e.g., USD). At request time the engine looks up the target currency rate, applies the conversion, and rounds to the currency's minor unit precision. A configurable margin (e.g., 2%) is added to hedge rate fluctuations. Rates are cached in Redis with a TTL matching the refresh interval so the DB is not hit on every price call.”
}
},
{
“@type”: “Question”,
“name”: “How is customer tier pricing implemented in a price engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Customer tiers (e.g., Bronze, Silver, Gold) are mapped to discount schedules stored in a tier-pricing table keyed by tier and SKU or category. When the engine receives a pricing request with a customer ID, it resolves the customer's current tier from the customer service (cached), then looks up the applicable tier discount. The discount is applied as a percentage off the base price or as an absolute deduction depending on configuration. Tier resolution is cached per customer with a short TTL to reflect tier changes promptly.”
}
},
{
“@type”: “Question”,
“name”: “What cache invalidation strategy is used in a price engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Price rules and tier schedules are cached in Redis with TTL-based expiry as the baseline. For immediate invalidation on rule updates, the admin service publishes a PriceRuleUpdated event to a pub/sub channel. Price engine instances subscribe and delete or refresh the affected cache keys on receipt. This write-through invalidation ensures stale prices are served only for the propagation lag (typically sub-second). A short fallback TTL (e.g., 60 seconds) limits blast radius if the invalidation message is lost.”
}
}
]
}

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Shopify Interview Guide

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

Scroll to Top