FX Rate Service Low-Level Design: Provider Aggregation, Spread Calculation, and Stale Rate Handling

What Is an FX Rate Service?

An FX rate service aggregates exchange rate quotes from multiple external providers, computes a mid-rate, applies configurable spreads, detects stale data, and exposes a low-latency read API to downstream services. It is a foundational component in payments, trading, and e-commerce platforms where currency conversion accuracy and freshness directly affect revenue and compliance.

Requirements

Functional Requirements

  • Ingest rates from N external providers (REST pull or WebSocket push).
  • Compute a mid-rate from aggregated quotes using a configurable strategy (median, VWAP, trimmed mean).
  • Apply bid/ask spread as basis points or percentage per currency pair and client tier.
  • Detect and handle stale rates: expose last-known rate with a staleness flag or return an error after a hard TTL.
  • Support currency pair subscriptions so callers receive push updates on rate change.
  • Provide a historical rate lookup endpoint for audit and reconciliation.

Non-Functional Requirements

  • P99 read latency under 5 ms for cached rates.
  • Rate ingestion pipeline handles 10,000 quote updates per second.
  • 99.99% availability; provider outages must not cause full service degradation.
  • Rate data retained for 7 years to satisfy financial compliance requirements.

Data Model

RateQuote (raw provider tick)

  • quote_id UUID — primary key.
  • provider_id — references a Provider record.
  • base_currency, quote_currency — ISO 4217 codes.
  • bid, ask — DECIMAL(18,8) to preserve pip precision.
  • provider_ts — timestamp from provider payload.
  • ingested_at — wall-clock time of ingestion.

MidRate (computed canonical rate)

  • pair — e.g., EURUSD.
  • mid DECIMAL(18,8).
  • computed_at timestamp.
  • stale boolean — set true when age exceeds soft TTL threshold.
  • source_count — number of provider quotes used in computation.

SpreadConfig

  • pair, client_tier — composite key.
  • spread_bps INTEGER — spread in basis points.
  • effective_from, effective_to — validity window for scheduled spread changes.

Core Algorithms

Mid-Rate Computation

For each currency pair, collect the most recent quote from every active provider within the freshness window (default 30 seconds). Sort mid-points (bid+ask)/2, remove the top and bottom outlier if N >= 4, then compute the arithmetic mean of remaining values. This trimmed-mean strategy guards against a rogue provider injecting extreme rates. The result is written atomically to an in-memory hash map keyed by pair and simultaneously appended to the time-series store.

Spread Application

Given a mid-rate M and spread S basis points, the client-facing bid is M * (1 - S/10000) and ask is M * (1 + S/10000). Spread lookup hits a local cache with a 60-second TTL backed by the SpreadConfig table. Spread changes scheduled in advance are pre-loaded at startup and applied on schedule via a background timer.

Staleness Detection

A background monitor checks each pair every 5 seconds. Soft TTL (default 10 s) sets stale=true on MidRate and emits a RATE_STALE event downstream. Hard TTL (default 60 s) causes the rate endpoint to return HTTP 503 with a RATE_UNAVAILABLE error code. Callers can opt in to receiving stale rates with explicit acknowledgment via a request header.

API Design

REST Endpoints

  • GET /v1/rates/{pair} — returns current bid, ask, mid, stale flag, and computed_at.
  • GET /v1/rates/{pair}/history?from=&to=&granularity= — returns OHLC mid-rate bars for the requested window.
  • POST /v1/rates/quote — body contains amount, from_currency, to_currency; returns converted amount at current spread-adjusted ask.

WebSocket Feed

Clients subscribe with a SUBSCRIBE frame listing pairs. The service streams RATE_UPDATE frames on every mid-rate recomputation and RATE_STALE frames when staleness is detected. Heartbeat frames every 15 seconds keep connections alive through NAT devices.

Scalability and Reliability

Provider Ingestion Layer

Each provider adapter runs as an independent goroutine or thread. Quotes are published to an internal ring buffer per provider. A fan-in aggregator reads all ring buffers and triggers recomputation when any quote arrives. This isolates slow or failing providers: if a provider adapter stalls, its ring buffer simply stops producing new items and its quotes age out of the freshness window.

Read Path

The current MidRate map lives in process memory, making reads lock-free via copy-on-write. For multi-instance deployments, a Redis hash stores the latest rate per pair. Instances subscribe to a Redis pub/sub channel for invalidation and update their local maps on every message, keeping latency well under 5 ms even across nodes.

Historical Storage

Every computed MidRate is written asynchronously to a time-series database (e.g., TimescaleDB or InfluxDB). The write path is fire-and-forget from the main computation loop; a durable Kafka topic acts as a buffer so no ticks are lost if the time-series store is temporarily unavailable.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does multi-provider rate aggregation work in an FX rate service?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The service polls multiple liquidity providers (banks, ECN feeds, data vendors) in parallel, normalizes their quote formats, and aggregates the results. Each provider's quote is tagged with a timestamp and confidence weight. The aggregation layer applies outlier rejection before computing the composite rate, ensuring no single bad feed distorts the output.”
}
},
{
“@type”: “Question”,
“name”: “How is a mid-rate calculated from bid/ask quotes?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The mid-rate is the arithmetic mean of the best bid and best ask across all valid providers: mid = (best_bid + best_ask) / 2. Some implementations use a volume-weighted mid if provider liquidity data is available, giving more weight to providers with tighter spreads and higher fill rates.”
}
},
{
“@type”: “Question”,
“name”: “How is spread applied on top of the mid-rate?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Spread is configured per currency pair and customer tier. The service adds half the spread to the mid-rate to produce the offer price and subtracts half to produce the bid price. Dynamic spread widening can be triggered by volatility signals or low liquidity conditions to protect the platform from adverse selection.”
}
},
{
“@type”: “Question”,
“name”: “What is the stale rate fallback strategy when providers are unavailable?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “If a provider feed exceeds its staleness threshold (e.g., 30 seconds with no update), its quotes are excluded from aggregation. If all providers for a pair go stale, the service serves the last known composite rate with a stale flag in the response and applies an automatic spread premium. A circuit breaker halts trading for the pair if staleness exceeds a hard limit, preventing client exposure to arbitrarily outdated rates.”
}
}
]
}

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

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

See also: Coinbase Interview Guide

Scroll to Top