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.

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