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

Source: https://www.techinterview.org/post/3233470578/lld-fx-rate-service/
Updated: 2026-04-17 · techinterview.org

## 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.

See also: [Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems](https://www.techinterview.org/post/3233460268/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](https://www.techinterview.org/post/3233460275/scale-ai-interview-guide-2026-data-infrastructure-rlhf-pipelines-and-ml-engineering/)

See also: [Coinbase Interview Guide](https://www.techinterview.org/post/3233459920/coinbase-interview/)
