Designing a Real-Time Currency Conversion Fee Transparency System

Designing a Real-Time Currency Conversion Fee Transparency System

Designing a Real-Time Currency Conversion Fee Transparency System

How payment platforms compute, cache, and display the exact markup applied over the market mid-rate on every cross-currency transaction — at a scale of millions of quotes per minute, with sub-second latency and audit-grade accuracy. A deep dive into rate ingestion, aggregation, markup pricing engines, rate-lock semantics, and the compliance-grade audit trail that turns a two-number UI into a defensible pricing system.

01

Introduction & History

Every time a customer swipes a card abroad, sends money to a relative in another country, or buys something priced in a foreign currency, two numbers exist for that transaction: the price the market would give a large institutional trader (the mid-market rate), and the price the customer actually gets. The gap between the two is the FX markup — and for decades, that gap was invisible. Banks and card networks folded it into a single “exchange rate” line, and customers had no easy way to know whether they had paid a 0.5% spread or a 6% spread.

That opacity was profitable, but it created a trust problem. Around the mid-2010s, a new generation of payment companies — largely born out of the remittance and neobank space — began advertising the mid-market rate itself as a marketing weapon: “we show you the real rate, then charge a transparent fee on top.” This forced an industry-wide shift. Regulators followed. The European Union’s Cross-Border Payments Regulation and PSD2 disclosure rules, India’s RBI guidelines on card markup disclosure, and various U.S. state-level remittance disclosure laws (modeled on the Dodd-Frank Act’s Remittance Transfer Rule) began to require that payment providers disclose the exchange rate used and the total cost of a transfer before the customer confirms it.

This regulatory and competitive pressure turned “show me the markup” from a nice-to-have UX feature into a hard, real-time, high-availability system requirement. The system has to answer a very specific question, correctly, in under a few hundred milliseconds, for every currency pair a platform supports, at global scale: “If I convert X units of currency A to currency B right now, what is the mid-market rate, what is the rate I’m actually getting, and what does that cost me in absolute and percentage terms?”

Everyday analogy

Think of the mid-market rate like the price you’d see on a stock ticker for a share of a company — the price at which large, informed buyers and sellers are trading with each other right this second. When you personally want to buy that stock through a small retail broker, you don’t get the ticker price exactly; the broker adds a small spread to cover their risk and make a profit. A fee transparency system for currency conversion is simply a very fast, very reliable “ticker + your actual price + the difference, explained” display, updated continuously and shown to every customer before they commit to a trade.

1.1 What “fee transparency” actually means as a system requirement

It is tempting to think of this as “just show two numbers and subtract them.” In practice, the requirement decomposes into several distinct engineering problems:

  • Sourcing a defensible mid-rate — pulling live rates from one or more market data providers and defining, unambiguously, what “the mid rate at this instant” means.
  • Applying a markup policy — the platform’s own pricing logic, which may vary by currency pair, customer tier, transaction size, payment corridor, or promotional campaign.
  • Presenting it before commitment — the customer must see the breakdown pre-transaction, not buried in a statement afterward, and the rate they see must be honored (often called rate lock or quote guarantee) for a short window.
  • Recording it for audit — regulators and internal compliance teams need an immutable record of exactly what rate and fee were shown and charged, for every transaction, for years.
  • Doing all of the above under extreme load and extreme rate volatility — during market-moving events (central bank announcements, geopolitical shocks), rates can move multiple times per second, and the system must not show stale or contradictory numbers.
💬
What an interviewer may ask

“Why can’t you just store the exchange rate in a database and update it every minute?” — This tests whether you understand that FX markets are continuous and that a stale rate directly translates into either the customer being shown a wrong price (compliance risk) or the platform absorbing market risk on the spread (financial risk). A good answer discusses rate staleness tolerance, rate-lock windows, and how staleness requirements differ from a typical read-heavy caching problem.

02

Why Fee Transparency Is Hard

On the surface this looks like a simple arithmetic and display problem. It is not, for four structural reasons.

2.1 The mid-rate itself is not a single number

“The market rate” is a fiction of convenience. In reality, for any currency pair, dozens of liquidity providers and exchanges are quoting slightly different bid and ask prices at any given moment, and those quotes are themselves moving continuously. A defensible “mid rate” is actually a derived, aggregated value — typically the midpoint between the best bid and best ask across a curated panel of providers, sometimes volume-weighted, sometimes simply averaged, refreshed on a fixed cadence (commonly every 1–10 seconds for major pairs, less frequently for exotic pairs). The system design challenge is building a pipeline that ingests multiple noisy, sometimes conflicting, sometimes momentarily unavailable feeds and produces one trustworthy number the whole platform agrees on.

2.2 Markup policy is a pricing engine, not a constant

A naive implementation assumes “we always add 1% markup.” Real platforms vary the markup by dozens of dimensions: the currency pair (exotic pairs often carry wider spreads because they’re less liquid and riskier to hedge), the transaction size (large transfers sometimes get tighter spreads because they’re more profitable in absolute terms, or wider spreads because they carry more hedging risk), the customer’s tier or loyalty status, the payment corridor and rail (a card transaction settling through a card network has different economics than a bank-to-bank wire), promotional campaigns, and even the time of day relative to market liquidity. This means the “fee” isn’t a constant lookup — it’s the output of a rules or pricing engine that must be evaluated per-quote, consistently, and explainably.

2.3 Regulatory disclosure has hard legal definitions

Different jurisdictions define what must be disclosed differently. Some require disclosure of the “total cost” as a single combined percentage. Others require the mid-market rate to be shown explicitly and separately from any flat fee. Some remittance regulations require the disclosure to remain valid and honored for a fixed period (commonly referred to as a rate-lock or quote-validity window) even if the market moves against the platform in that window. Getting this wrong is not just a UX bug — it is a compliance violation with real regulatory penalties.

💬
What an interviewer may ask

“How would you design the system so that a compliance rule change in one country doesn’t require a code deployment?” — They’re testing whether you’ll hardcode disclosure logic per-market or build a configuration-driven rules engine (a policy-as-data approach) that product and compliance teams can update without engineering involvement.

2.4 Scale and volatility compound each other

A payments platform processing millions of requests per minute across hundreds of currency pairs cannot afford to compute a fresh markup from scratch, synchronously, against a live upstream FX provider on every single customer request — the upstream providers themselves rate-limit and charge per API call, and the added network hop would blow latency budgets. The system must therefore separate the expensive, infrequent work (ingesting and aggregating raw market rates) from the cheap, frequent work (serving a quote to a customer), while still guaranteeing the served quote reflects a rate that is fresh enough to be legally and financially defensible.

Everyday analogy

Picture a busy currency exchange counter at an airport. The staff don’t call their head office to ask “what’s the rate?” for every single customer — that would create a queue stretching out the terminal. Instead, someone updates a big board behind the counter every few seconds with the latest rate, and every staff member reads off that board instantly when serving a customer, applying the counter’s fixed commission on top. The board is the cache; the periodic update is the ingestion pipeline; the commission logic is the pricing engine. Your system design job is building the equivalent of that board — but for hundreds of currency pairs, refreshed globally, and provably accurate enough to survive a regulator’s audit.

03

Architecture & Components

The system splits cleanly into two planes: a rate ingestion plane that continuously builds a trustworthy, cached view of mid-market rates, and a quote-serving plane that combines that cached rate with the platform’s pricing policy to answer a customer’s request in real time. A third, cross-cutting concern — audit and ledger recording — ties every served quote to an immutable record.

flowchart TB subgraph Client[“Client Layer”] A1[Mobile App] A2[Web App] A3[Partner API Consumer] end subgraph Edge[“Edge and API Layer”] B1[API Gateway] B2[Auth and Rate Limiter] end subgraph QuoteServing[“Quote Serving Plane”] C1[FX Quote Service] C2[Markup Pricing Engine] C3[Rate Cache Redis] C4[Quote Lock Store] end subgraph Ingestion[“Rate Ingestion Plane”] D1[Rate Aggregator Service] D2[Provider Adapter Layer] D3[Rate Normalizer and Validator] D4[Stale Rate Detector] end subgraph Providers[“External Market Data”] E1[Provider A Feed] E2[Provider B Feed] E3[Provider C Feed] end subgraph Persistence[“Persistence and Audit”] F1[Quote Audit Store Postgres] F2[Rate History Time Series DB] F3[Kafka Event Bus] end subgraph Ops[“Config and Observability”] G1[Markup Config Service] G2[Metrics and Alerting] end A1 –> B1 A2 –> B1 A3 –> B1 B1 –> B2 B2 –> C1 C1 –> C2 C2 –> C3 C2 –> G1 C1 –> C4 C1 –> F3 F3 –> F1 D2 –> E1 D2 –> E2 D2 –> E3 D2 –> D3 D3 –> D4 D4 –> D1 D1 –> C3 D1 –> F2 D1 –> G2 C1 –> G2
Figure 3.1 — High-level architecture: ingestion plane feeds a shared rate cache; the quote-serving plane reads it and applies markup policy per request.
Component

Provider Adapter Layer

Each external market data vendor exposes a different API shape, authentication scheme, and update cadence. The adapter layer normalizes all of them into one internal schema (currency pair, bid, ask, timestamp, provider identity, sequence number). This isolation means adding or swapping a market data vendor never touches downstream logic — a classic adapter pattern application.

Component

Rate Normalizer and Validator

Raw feeds are noisy. A single bad tick from one provider (a decimal-point error, a stale cached value on their end, a network glitch producing a wildly wrong number) must never propagate into a customer-facing quote. The validator applies sanity bounds (for example, rejecting any tick that deviates more than a configured percentage from the last accepted value or from the median of other providers in the same window) before a tick is allowed to influence the published mid-rate.

Component

Rate Aggregator Service

Combines validated ticks from multiple providers into one published mid-rate per currency pair, using a defined aggregation method — commonly a volume-weighted or simple average of provider mid-points, or a “best of panel” median. It publishes the result to the shared rate cache and appends it to a time-series history store for audit and analytics.

Component

Rate Cache

An in-memory, replicated cache (typically Redis Cluster or an equivalent) holding the latest published mid-rate for every supported currency pair, along with the timestamp of that rate. This is the “big board” from the airport-counter analogy — every quote request reads from here rather than touching an upstream provider directly.

Component

Markup Pricing Engine

A rules-driven engine that takes the mid-rate, the currency pair, and request context (customer tier, transaction amount, corridor, channel) and returns the customer-facing rate and the fee breakdown. Pricing rules are stored as configuration data, not code, so they can be changed by pricing and compliance teams without a deployment.

Component

FX Quote Service

The orchestrating service the client actually talks to. It reads the cached mid-rate, calls the pricing engine, optionally reserves a short-lived rate lock, and returns a structured response containing the mid-rate, the customer rate, the absolute markup, the percentage markup, and the quote’s expiry time.

Component

Quote Lock Store

When a platform guarantees a quoted rate for a window (say, 30–60 seconds) so the number the customer saw is the number they actually get, that reservation needs a short-TTL store — typically the same Redis cluster with a dedicated keyspace, or a lightweight dedicated store — keyed by quote ID.

Component

Quote Audit Store & Event Bus

Every quote served, and every transaction executed against a quote, is published as an event onto a durable event bus (Kafka or equivalent) and persisted into an append-only audit store. This is the system of record for compliance, dispute resolution, and internal analytics.

📌
Production example — Wise (formerly TransferWise)

Wise built its entire brand identity around showing the mid-market rate and a transparent, itemized fee on every transfer, before the customer confirms. Internally this requires exactly the split described above: a continuously updated mid-rate feed decoupled from a per-transaction pricing calculation, because Wise’s fee structure varies by corridor, amount band, and payment method, and needs to be explainable to both the customer and regulators on demand.

04

Internal Working

4.1 How the mid-rate is computed

For a currency pair like USD/INR, the aggregator maintains a small in-memory window of the most recent valid ticks from each subscribed provider. On each refresh cycle it:

  1. Discards any tick older than the configured freshness threshold (for example, 5 seconds for major pairs).
  2. Computes each provider’s implied mid-point as the average of their bid and ask.
  3. Combines the surviving provider mid-points using the platform’s chosen aggregation function (simple mean, weighted mean by provider reliability score, or median to reduce outlier influence).
  4. Runs a delta check against the previously published rate — if the new rate has moved more than a configured circuit-breaker threshold in one cycle, it is flagged for review rather than published blindly, since a huge single-cycle jump is more likely a bad feed than a real market move.
  5. Publishes the accepted rate to the cache with a monotonically increasing sequence number and a timestamp.

4.2 How a customer quote is computed

When a customer opens a currency conversion screen or a transaction is being priced at checkout, the FX Quote Service performs, in order:

  1. Cache read — fetch the latest published mid-rate for the requested pair from the rate cache. This is an in-memory lookup, typically completing in under a millisecond.
  2. Freshness check — verify the cached rate’s timestamp is within an acceptable staleness bound. If the rate is too old (meaning the ingestion pipeline has fallen behind or a provider outage has starved the cache), the service either falls back to a secondary rate source or degrades gracefully by informing the client the rate is temporarily unavailable rather than serving a stale, misleading number.
  3. Context resolution — determine the request’s pricing context: customer tier, transaction amount, corridor, and channel.
  4. Markup calculation — the pricing engine evaluates the applicable rule and returns a markup expressed as a percentage (or sometimes a combination of a percentage spread plus a flat fee).
  5. Customer rate derivation — the customer-facing rate is computed as the mid-rate adjusted by the markup in the direction that favors the platform (for example, mid-rate minus markup-percent for a sell-side conversion).
  6. Quote assembly and lock — a structured quote object is built containing the mid-rate, the customer rate, the markup in both percentage and absolute currency terms, and — if the platform offers rate-lock — a quote ID and expiry stored in the lock store.
  7. Audit emission — the full quote, including which mid-rate sequence number and which pricing rule version were used, is emitted as an event for the audit trail, before the response is returned to the client.
Everyday analogy

This is very similar to how an e-commerce checkout computes a final price from a base price, applicable discounts, and tax rules — except here, the “base price” itself (the mid-rate) is also changing every few seconds, so the system must snapshot which version of the base price it used, the same way an accountant would write down not just the final invoice total but which price list and which exchange rate date it was calculated from.

4.3 The fee transparency calculation, explicitly

The customer-facing markup disclosure typically needs three numbers displayed together:

FieldDefinitionExample
Mid-market rateThe aggregated, provider-sourced rate with no markup applied1 USD = 83.10 INR
Your rateThe rate actually applied to the customer’s conversion1 USD = 82.60 INR
Markup / feeThe difference, shown as both a percentage and an absolute amount for the transaction size0.60% markup, ₹50 on a $100 transfer

The percentage markup is typically computed as $text{markup%} = (r_{mid} – r_{cust}) / r_{mid}$ for a sell-side conversion, and the absolute cost is $text{cost} = text{markup%} times text{amount} times r_{mid}$, expressed in whichever currency is most meaningful for the customer to understand (usually the currency they’re paying in).

QuoteService.java — assembling the fee-transparency payload
public Quote buildQuote(QuoteRequest req) {
    RateSnapshot mid = rateCache.get(req.pair())
        .orElseThrow(() -> new RateUnavailable(req.pair()));
    ensureFresh(mid, freshnessBudget(req.pair()));

    PricingContext ctx = context.resolve(req);
    Markup markup = pricingEngine.evaluate(req.pair(), ctx);

    BigDecimal customerRate = mid.value()
        .multiply(BigDecimal.ONE.subtract(markup.percent()));
    BigDecimal absCost = markup.percent()
        .multiply(req.amount())
        .multiply(mid.value());

    String quoteId = quoteLock.reserve(req, mid, customerRate, LOCK_TTL);
    auditBus.emit(new QuoteIssued(quoteId, mid.sequence(), markup.ruleVersion(),
                                  customerRate, absCost, Instant.now()));

    return new Quote(quoteId, mid.value(), customerRate, markup.percent(),
                     absCost, LOCK_TTL);
}
💬
What an interviewer may ask

“If the customer’s transaction actually settles a few seconds after the quote was shown, and the market has moved, which rate do you charge?” — This probes understanding of rate-lock design: either the platform commits to honoring the originally quoted rate for a bounded window (absorbing small market risk itself, common in remittance and card FX), or it re-quotes at execution time and must handle the case where the previously displayed number no longer matches, which is a worse experience and sometimes a compliance problem if the disclosure rules require honoring the quote.

05

Data Flow & Lifecycle

sequenceDiagram participant U as Customer App participant GW as API Gateway participant QS as FX Quote Service participant PE as Pricing Engine participant RC as Rate Cache participant LK as Quote Lock Store participant EB as Event Bus U->>GW: Request quote USD to INR amount 100 GW->>QS: Forward authenticated request QS->>RC: Get latest mid rate for USD INR RC–>>QS: Mid rate 83.10 timestamp t0 sequence 48210 QS->>QS: Check freshness within threshold QS->>PE: Evaluate markup for context PE–>>QS: Markup 0.60 percent rule version v12 QS->>QS: Compute customer rate and fee breakdown QS->>LK: Store quote lock with 45 second expiry LK–>>QS: Lock confirmed quote id Q-9931 QS->>EB: Publish quote issued event QS–>>GW: Return quote with mid rate customer rate fee GW–>>U: Display rate breakdown to customer U->>GW: Confirm transaction using quote id Q-9931 GW->>QS: Execute transaction against locked quote QS->>LK: Validate quote id still valid LK–>>QS: Valid not expired QS->>EB: Publish transaction executed event QS–>>GW: Confirmation with final rate and fee GW–>>U: Show completed transaction receipt
Figure 5.1 — Sequence of a quote request through to a locked-in transaction execution.

5.1 Lifecycle of a rate, end to end

It helps to trace one rate value through its entire life to understand why the architecture is split the way it is:

  1. Origination — a market data provider publishes a bid/ask tick for a currency pair, delivered over a streaming connection (WebSocket or a persistent gRPC stream) or a fast-polling REST endpoint.
  2. Normalization — the provider adapter converts the vendor-specific payload into the platform’s internal tick schema.
  3. Validation — the tick is checked against sanity bounds and deduplicated against the provider’s previous tick.
  4. Aggregation — the aggregator combines this tick with the latest valid ticks from other providers into one published mid-rate.
  5. Publication — the new mid-rate is written to the shared cache, replacing the previous value, tagged with a sequence number and timestamp, and simultaneously appended to a time-series history store.
  6. Consumption — one or many quote requests, arriving concurrently across the globe, read this same cached value.
  7. Markup application — the pricing engine transforms the mid-rate into a customer rate per request context.
  8. Disclosure — the quote, containing both numbers and the delta, is returned to the client for display.
  9. Optional lock — if the platform guarantees the quote, it is reserved with a short TTL.
  10. Execution or expiry — the customer either confirms within the window (the locked rate is honored) or lets it expire (a fresh quote must be requested).
  11. Audit persistence — every step from aggregation through execution is durably logged for compliance and reconciliation.
stateDiagram-v2 [*] –> Requested Requested –> RateResolved: mid rate read from cache RateResolved –> MarkupApplied: pricing engine evaluates rule MarkupApplied –> QuoteIssued: response returned to client QuoteIssued –> Locked: rate lock reserved QuoteIssued –> Unlocked: platform offers no lock Locked –> Executed: customer confirms within TTL Locked –> Expired: TTL elapses with no confirmation Unlocked –> ReQuotedAtExecution: customer confirms later Executed –> Audited: event persisted to audit store Expired –> [*] ReQuotedAtExecution –> Audited Audited –> [*]
Figure 5.2 — Lifecycle states of a single quote, from request through lock, expiry or execution, to audit persistence.

5.2 Handling concurrent requests against the same rate

Because the rate cache is read-only from the perspective of the quote-serving plane, thousands of concurrent quote requests for the same currency pair simply perform independent, non-blocking reads of the same cached value — there is no write contention on the hot path. The only place writes and reads interact is the rate cache’s own replacement of the value on each ingestion cycle, which is designed as an atomic set operation so no reader ever observes a partially updated rate.

💬
What an interviewer may ask

“How do you guarantee two customers converting the same currency pair within the same second see a consistent rate, even across different data center regions?” — Good answers discuss either a single authoritative rate-publishing region with fast cross-region cache replication, or a rate versioning scheme (sequence numbers) so regions momentarily behind can detect they’re serving an older-but-still-valid rate rather than a corrupted one, plus bounding acceptable cross-region staleness explicitly.

06

Advantages, Disadvantages & Trade-offs

6.1 Advantages of this architecture

  • Decoupled cost profile — expensive upstream market data calls happen on a fixed schedule regardless of customer traffic, so a spike in customer requests never increases upstream API cost or rate-limit pressure.
  • Predictable low latency — because quote serving only touches an in-memory cache and a rules engine, response times stay in the tens of milliseconds even under heavy load.
  • Independent scalability — the ingestion plane and the serving plane can be scaled on entirely different dimensions: ingestion scales with the number of currency pairs and providers, serving scales with customer traffic.
  • Auditable and explainable — every quote carries a reference to the exact mid-rate sequence number and pricing rule version used, making regulatory audits and customer dispute resolution tractable.
  • Configuration-driven pricing — markup policy changes do not require a deployment, which shortens the cycle for pricing and compliance teams.

6.2 Disadvantages and costs

  • Freshness versus cost tension — pulling tighter, more frequent updates from market data providers costs more and adds ingestion load; looser refresh intervals save cost but widen the window during which a served quote might diverge from the true live market.
  • Rate-lock financial risk — honoring a quoted rate for a fixed window means the platform, not the customer, absorbs adverse market movement during that window; this must be modeled and hedged, or the lock windows kept intentionally short.
  • Complexity of a multi-provider aggregation pipeline — building robust outlier detection and fallback logic across several vendors is materially harder than a single-source integration, but a single source is a single point of failure and manipulation risk.
  • Regulatory divergence — different markets require different disclosure formats and different rate-lock durations, pushing complexity into the pricing and disclosure configuration layer.

6.3 Key trade-off table

DecisionOption AOption BTypical Choice
Rate refresh cadenceSub-second streamingEvery few seconds, polledStreaming for majors, polling for exotics
Aggregation methodSimple average across providersWeighted or median-basedWeighted/median to resist outliers
Rate lockNo lock, re-quote at executionShort TTL lock, honor original quoteShort TTL lock for customer trust
Pricing rule storageHardcoded in service logicExternalized configuration/rules engineExternalized, versioned config
Cache topologySingle global cacheRegional caches with replicationRegional caches, bounded staleness
07

Performance & Scalability

At the scale described in the brief — millions of requests per minute — the design goal is that quote serving never performs synchronous work proportional to the cost of sourcing a fresh market rate. Every quote request should resolve through cache reads and in-process rule evaluation only.

7.1 Read path optimization

  • In-memory cache colocated with compute — the FX Quote Service instances keep a local, short-TTL in-process copy of the hottest currency pairs, backed by the shared Redis cluster, cutting network round trips for the most frequently requested pairs (major pairs like USD/EUR, USD/INR, GBP/USD dominate volume disproportionately — a classic long-tail distribution).
  • Read replicas for the shared cache, distributed across availability zones, so no single cache node becomes a bottleneck under fan-out read load.
  • Pre-computed rule evaluation — for the most common pricing contexts (default tier, standard corridor), the pricing engine can pre-resolve and cache the effective markup percentage, only falling back to full rule evaluation for less common contexts.

7.2 Write path optimization

  • Batched, sharded ingestion — currency pairs are sharded across aggregator instances so no single instance needs to process every tick from every provider for every pair.
  • Backpressure-aware provider adapters — if a provider’s feed spikes in volume during high volatility, adapters apply local buffering and coalescing (only the latest tick per pair matters for mid-rate purposes) rather than forwarding every intermediate tick downstream.

7.3 Horizontal scaling model

The FX Quote Service is stateless with respect to customer requests (all state lives in the cache and lock store), which means it scales horizontally behind a load balancer with no sticky-session requirement. The rate cache scales via standard partitioning (sharding currency pairs across cache nodes) combined with read replicas per shard.

📌
Production example — card networks

Visa and Mastercard’s card-network FX conversion happens at a scale of tens of millions of cross-border transactions daily. Both networks publish daily or near-real-time reference rates that issuing banks apply markup on top of — architecturally, this mirrors the ingestion/serving split described here, with the network acting as the shared “rate cache” that thousands of issuing banks read from rather than each bank sourcing its own market data independently.

7.4 Latency budget example

StepTypical Budget
API Gateway auth and routing5–10 ms
Cache read for mid-rate< 2 ms
Pricing rule evaluation2–5 ms
Quote lock write3–6 ms
Audit event publish (async, non-blocking)0 ms added to response
Total end-to-end targetUnder 50 ms p99
< 50 msp99 quote-serving latency target
1–10 sMid-rate refresh cadence for majors
30–60 sTypical rate-lock TTL
≥ 2Providers per major pair for redundancy
💬
What an interviewer may ask

“During a high-volatility event, tick volume from providers can 10x. How does your ingestion pipeline avoid falling behind?” — Strong answers mention coalescing (dropping intermediate ticks and only processing the latest per pair per cycle), horizontal sharding of currency pairs across aggregator workers, and decoupling ingestion throughput entirely from serving-side read latency, since readers never wait on ingestion.

08

High Availability & Reliability

8.1 Provider redundancy

No single market data vendor should be a single point of failure. The provider adapter layer subscribes to at least two, typically three, independent providers per major currency pair, and the aggregator is designed to keep publishing valid rates even if one provider’s feed drops entirely — it simply aggregates over the remaining healthy providers and flags reduced confidence internally.

8.2 Stale rate fallback strategy

If every provider for a pair becomes unavailable, the system must decide between three fallback behaviors, and this decision should be explicit, configurable, and tested:

  • Serve the last known good rate with a visible staleness indicator, up to a maximum allowed staleness window.
  • Widen the markup temporarily to compensate for increased market risk during the outage, rather than freezing the price entirely.
  • Block new quotes for the affected pair entirely once staleness exceeds a hard threshold, rather than risk showing a materially wrong price — this is the safest default for compliance-sensitive markets.

8.3 Multi-region deployment

flowchart LR subgraph RegionA[“Region US-East Active”] A1[FX Quote Service Cluster] A2[Rate Cache Primary] A3[Aggregator Workers] end subgraph RegionB[“Region EU-West Active”] B1[FX Quote Service Cluster] B2[Rate Cache Replica] B3[Aggregator Workers] end subgraph RegionC[“Region AP-South Active”] C1[FX Quote Service Cluster] C2[Rate Cache Replica] C3[Aggregator Workers] end P1[Market Data Provider Pool] P1 –> A3 P1 –> B3 P1 –> C3 A2 <--> B2 B2 <--> C2 A2 <--> C2 GLB[Global Load Balancer / GeoDNS] GLB –> A1 GLB –> B1 GLB –> C1
Figure 8.1 — Active-active multi-region deployment; each region ingests independently and replicates published rates, so a region-level failure does not stop quote serving elsewhere.

Each region runs its own full ingestion pipeline independently (not just a read replica of one master region), because relying on a single region to compute the authoritative rate for the entire globe creates a cross-region latency and single-point-of-failure problem. Instead, each region aggregates from the same provider pool and should converge on nearly identical rates given the same inputs and aggregation logic; small, expected divergence between regions is tolerated within a defined bound, and monitored.

8.4 Graceful degradation under partial outage

If the pricing engine’s rules service becomes unavailable, the FX Quote Service should fall back to the last successfully cached pricing rule set for that context rather than failing the entire quote request — a stale-but-valid markup rule is far less harmful than an outright quote failure at checkout.

📌
Production example — volatile corridors

Remittance platforms operating in high-volatility corridors (for example, currencies subject to central bank intervention or capital controls) commonly widen their markup automatically during detected volatility spikes rather than pausing service — this keeps the platform available while pricing in the additional short-term hedging risk, and is disclosed to the customer as part of the same transparency mechanism used in normal conditions.

💬
What an interviewer may ask

“What happens if your rate cache and your rate-lock store become inconsistent — a quote is locked but the underlying rate has since been invalidated?” — This tests whether you treat the lock store as the source of truth for an already-issued quote (it should be — once locked, the quote is honored regardless of subsequent rate cache changes, up to its expiry) versus re-deriving from the live cache, which would break the transparency guarantee shown to the customer.

09

Security

9.1 Protecting the rate pipeline from manipulation

A currency rate feed is a high-value target: an attacker who can inject a manipulated tick, or exploit a race condition in the aggregation logic, could cause the platform to systematically mis-price conversions in their favor. Defenses include cryptographically signed feeds from market data providers where supported, strict schema and bounds validation on every incoming tick, outlier rejection against a multi-provider consensus, and full immutable logging of every tick that influenced a published rate so any anomaly is traceable after the fact.

9.2 Securing the quote and pricing APIs

  • Authentication and authorization on every quote request, tied to the requesting customer or partner identity, since pricing context (tier, promotions) directly affects the markup applied.
  • Rate limiting per client to prevent quote-scraping abuse, where a bad actor repeatedly requests quotes to reverse-engineer the platform’s exact pricing rules or to arbitrage momentary rate discrepancies across regions.
  • Quote lock tokens must be single-use, tied to the requesting customer session, and validated server-side at execution time to prevent replay or transfer of a locked favorable rate to another transaction.
  • Encryption in transit for all API traffic (TLS), and encryption at rest for the audit store, since transaction-level financial data is highly sensitive.

9.3 Preventing internal pricing rule abuse

Because markup rules are externalized as configuration for agility, that configuration surface itself becomes a security-sensitive asset — an unauthorized or accidental change to a pricing rule (for example, zeroing out markup on a corridor) directly costs the business money. Changes should go through the same rigor as code deployment: peer review, staged rollout, and full audit logging of who changed what rule and when.

9.4 Compliance-driven security requirements

Because this system handles financial transaction pricing, it typically falls under PCI-DSS scope (if card data touches any part of the flow) and regional financial data protection regulations. The audit store, in particular, must satisfy immutability and retention requirements — commonly implemented with write-once storage semantics or append-only ledger patterns, so historical quotes cannot be altered after the fact even by administrators.

💬
What an interviewer may ask

“How would you detect if someone was scraping your quote API to reverse-engineer your markup schedule?” — Good answers cover anomaly detection on request patterns (unusually systematic sweeps across amount bands or currency pairs from a single identity), per-client rate limiting, and treating the exact markup formula as sensitive even though the resulting numbers shown to genuine customers are, by design, fully transparent.

10

Monitoring, Logging & Metrics

10.1 What must be monitored

CategoryKey MetricsWhy It Matters
Rate freshnessAge of published rate per currency pair, provider feed lagDirectly determines quote accuracy and compliance risk
Provider healthFeed uptime, tick rejection rate, per-provider divergence from consensusDetects a failing or manipulated feed before it affects customers
Quote serving latencyp50/p95/p99 response time per regionCustomer experience and SLA compliance
Markup distributionAverage and distribution of applied markup per corridor/tierBusiness and pricing-fairness oversight
Lock honor ratePercentage of locked quotes executed successfully within TTLIndicates UX friction or expiry window mis-tuning
Audit pipeline lagDelay between quote issuance and durable audit persistenceCompliance risk if audit trail falls behind

10.2 Alerting strategy

  • Hard alerts (page immediately) — rate staleness beyond the legal disclosure threshold; all providers for a major pair simultaneously unavailable; audit event publish failures.
  • Soft alerts (dashboard/ticket) — single provider feed degraded but others healthy; markup distribution drifting outside expected historical bounds, which may indicate a misconfigured pricing rule.

10.3 Logging and traceability

Every served quote is logged with a full trace: which mid-rate sequence number was read, which providers contributed to that rate, which pricing rule version was applied, and the resulting customer rate and fee. Distributed tracing (for example, OpenTelemetry spans propagated from the API gateway through the quote service, pricing engine, and cache) makes it possible to reconstruct exactly why a given customer saw a given price, which is essential both for engineering debugging and for answering a regulator’s or customer’s dispute.

Everyday analogy

This is similar to how an airline can tell you exactly why your ticket cost what it did months later — which fare class, which date the price was locked, which promotional code applied. A fee transparency system needs the same forensic reconstructability for every single currency conversion, not just for troubleshooting but as a first-class product feature customers can ask for.

💬
What an interviewer may ask

“A customer disputes the rate they were charged three weeks ago. Walk me through how your system proves what happened.” — This tests whether the audit trail is genuinely self-sufficient: it should be possible to pull up the exact quote event, the mid-rate and its provider inputs, the pricing rule version, and the lock/execution timestamps, without needing to guess or reconstruct from incomplete logs.

11

Deployment & Cloud Considerations

11.1 Deployment topology

The ingestion plane and serving plane are deployed as independently scalable microservice clusters, typically on Kubernetes, with separate horizontal pod autoscaling policies — the serving plane scales on request rate and CPU, while the ingestion plane scales primarily on the number of actively tracked currency pairs and provider connection count.

11.2 Provider connectivity

Persistent streaming connections to market data providers (WebSocket or gRPC streams) benefit from being deployed close to the provider’s point of presence to minimize feed latency, sometimes justifying a dedicated ingestion deployment in a specific cloud region even if customer traffic is served globally.

11.3 Blue-green and canary rollouts for pricing logic

Because pricing rule changes directly affect revenue and compliance, deployments of the pricing engine (and especially rule configuration changes) go through canary rollout — a small percentage of traffic evaluated against the new rule set, with automated comparison of the resulting markup distribution against the previous baseline, before full rollout.

11.4 Infrastructure as code and config management

Given how central externalized pricing configuration is to this system, that configuration is itself version-controlled (often in a dedicated config repository with its own review and approval workflow, separate from application code) and deployed through a controlled pipeline rather than edited directly in a database by hand.

📌
Production example — regional ingestion

Large global payment processors typically run FX rate ingestion in a small number of strategically placed regions close to major financial hubs (for example, near London and New York, where much of global FX liquidity concentrates), while quote-serving infrastructure is deployed far more broadly, close to end customers, with the published rate replicated outward — reflecting the same separation of ingestion and serving concerns described throughout this article.

12

Databases, Caching & Load Balancing

12.1 Storage choices

StoreTechnology PatternPurpose
Rate cacheIn-memory key-value store, clustered and replicatedServe the latest mid-rate with sub-millisecond reads
Rate historyTime-series databaseHistorical rate analytics, backtesting pricing rules, audit lookups
Quote lock storeKey-value store with native TTL supportShort-lived rate-lock reservations
Audit and transaction ledgerRelational database with append-only/immutable semantics, or event-sourced storeDurable, queryable, legally defensible record of every quote and execution
Pricing configurationVersioned document store or config serviceExternalized, auditable markup rules

12.2 Why not just one database

A single relational database could theoretically hold all of this, but it would force the same storage engine to simultaneously serve sub-millisecond hot reads for currency rates and durable, strongly consistent writes for financial audit records — two very different access patterns with different consistency and latency requirements. Splitting them lets each store be tuned and scaled for its actual workload: the cache favors availability and speed with brief staleness tolerance, while the audit store favors durability and correctness over raw speed.

12.3 Caching strategy in depth

  • Write-through on ingestion — the aggregator writes the newly computed rate directly into the cache as the authoritative update, rather than the cache lazily pulling from a database on a miss.
  • No cache invalidation race — because rates are always replaced wholesale with a new value plus sequence number rather than partially mutated, there’s no classic cache invalidation problem; the latest write simply wins, and readers can detect if they’re looking at an old sequence number.
  • Local process cache as a second tier — an in-process, very short TTL cache inside each Quote Service instance for the hottest pairs reduces network calls to the shared cache under extreme load, at the cost of very slightly increased staleness (bounded to a few hundred milliseconds).

12.4 Load balancing

The FX Quote Service sits behind a standard Layer 7 load balancer using round-robin or least-connections distribution, since instances are stateless. Global traffic is additionally routed via GeoDNS or an anycast-based global load balancer to the nearest healthy region, both to minimize latency and to provide automatic regional failover if an entire region’s serving cluster becomes unhealthy.

💬
What an interviewer may ask

“Why use a time-series database for rate history instead of just querying the audit relational store?” — Tests understanding that rate history is a high-write-volume, append-mostly, time-indexed workload (every tick for every pair, continuously) fundamentally different from transaction audit records, and that time-series databases are purpose-built for efficient range queries and downsampling over that shape of data, whereas cramming it into the transactional audit store would degrade both workloads.

13

APIs & Microservices

13.1 Core API surface

Endpoint PurposeDescription
Get quoteGiven a currency pair, amount, and context, returns mid-rate, customer rate, markup, and a lockable quote ID with expiry
Execute against quoteGiven a valid quote ID, executes the transaction at the locked rate, or rejects if expired
Get historical rateReturns the published mid-rate for a pair at a given historical timestamp, used for statements and disputes
Get supported pairsReturns the list of currency pairs the platform currently supports, with current freshness status
POST /v1/fx/quotes — request a quote with full fee breakdown
POST /v1/fx/quotes
{
  "sourceCurrency": "USD",
  "targetCurrency": "INR",
  "amount":         100.00,
  "corridor":       "US_TO_IN",
  "channel":        "app"
}

Response 201:
{
  "quoteId":       "Q-9931",
  "midRate":       83.10,
  "midSequence":   48210,
  "customerRate":  82.60,
  "markupPercent": 0.6019,
  "markupAbsolute": { "amount": 50.00, "currency": "INR" },
  "expiresAt":     "2026-08-11T09:00:45Z",
  "ruleVersion":   "v12"
}
POST /v1/fx/executions — commit a locked quote
POST /v1/fx/executions
{ "quoteId": "Q-9931" }

Response 200:
{
  "executionId":  "X-77a1",
  "quoteId":      "Q-9931",
  "finalRate":    82.60,       // honored from the locked quote
  "settledAt":    "2026-08-11T09:00:12Z"
}

13.2 Microservice boundaries

Each of the following is deployed as an independently owned, independently deployable service, communicating over well-defined APIs and asynchronous events rather than shared databases: Provider Adapter services (one logical service per vendor integration, or a shared service with per-vendor plugins), the Rate Aggregator, the Rate Cache (infrastructure, not a custom service), the Markup Pricing Engine, the FX Quote Service (the customer-facing orchestrator), the Quote Lock service, and the Audit/Ledger service. This separation lets each team iterate and scale their piece independently — a new market data provider integration should never require touching the pricing engine’s code.

13.3 Synchronous versus asynchronous communication

The customer-facing quote request path is synchronous end to end because the customer is waiting for a response in real time. Everything downstream of “quote has been decided” — audit logging, analytics, fraud signal enrichment, notification triggers — is asynchronous, published as events onto the event bus, so none of it adds latency to the customer-facing response and none of it can cause a quote request to fail due to an unrelated downstream hiccup.

Everyday analogy

It’s the difference between a cashier telling you your total immediately (synchronous — you’re standing there) versus the store’s inventory and sales-reporting systems updating in the background afterward (asynchronous — you’ve already left with your receipt, and nothing about that background processing should have made you wait longer at the register).

13.4 API versioning and backward compatibility

Because partner integrators and mobile app versions in the wild can lag behind the latest API contract by months, the quote API is versioned explicitly, and changes to the disclosed fee breakdown format (a compliance-sensitive payload) go through a deprecation window rather than a breaking change, since older app versions must continue to display accurate, correctly labeled fee information.

💬
What an interviewer may ask

“Would you make the audit logging call synchronous, to guarantee it’s never lost?” — This is a good discussion point: making it synchronous guarantees no quote is ever served without a corresponding audit record, but adds latency and a new failure mode to the customer path. A common resolution is a synchronous, fast write to a durable, low-latency log (like appending to the event bus itself, which is durable) rather than a synchronous write to the full audit database, decoupling “durably recorded” from “fully processed into the queryable audit store.”

14

Design Patterns & Anti-Patterns

14.1 Patterns applied

Pattern

Adapter

Normalizing heterogeneous market data provider APIs into one internal schema, isolating vendor churn from the aggregator and every downstream component.

Pattern

Strategy

The pricing engine selects among different markup calculation strategies based on context (tier, corridor, campaign) without the calling code needing to know which strategy applies.

Pattern

CQRS-like Separation

The ingestion plane (writes/updates the rate) is architecturally separate from the serving plane (reads and derives a customer quote), even though this isn’t a textbook CQRS event-sourced system throughout.

Pattern

Circuit Breaker

Around each market data provider connection, so a failing or slow provider doesn’t degrade the whole aggregation cycle.

Pattern

Event Sourcing (partial)

The audit trail is effectively an append-only event log of every quote and execution, enabling full reconstruction of pricing history.

Pattern

Write-Through (not Cache-Aside)

Cache-aside is deliberately avoided in favor of write-through, since the ingestion pipeline is the sole authoritative writer of rate data and customer requests should never trigger a rate fetch themselves.

14.2 Anti-patterns to avoid

Anti-patternWhy it’s dangerous
Fetching a fresh rate from an upstream provider on every customer quote requestDirectly couples customer-facing latency and cost to a third-party dependency and will not scale to millions of requests per minute
Hardcoding markup percentages in application codeEvery pricing change becomes a deployment, slowing the business and increasing the risk of a rushed, undertested change reaching production
Treating the mid-rate as a single-provider value with no validationA single bad tick from one vendor can directly translate into a wrong customer-facing price
Computing the audit record after returning the response to the customer, with no durability guaranteeA crash between response and audit write creates a compliance gap — a transaction with no traceable pricing justification
Silently re-quoting at execution time after advertising a “locked” rateBreaks the transparency promise itself and can be a direct regulatory violation in jurisdictions requiring honored quotes
One giant monolithic service for ingestion, pricing, and servingCouples unrelated scaling and deployment concerns and makes independent iteration by different teams painful
💬
What an interviewer may ask

“Why not use classic cache-aside here, where a cache miss triggers a fresh fetch?” — The expected insight is that cache-aside assumes the origin (an upstream FX provider) can absorb unpredictable, traffic-proportional load on a miss, which is exactly what this design must avoid; a write-through model driven entirely by a scheduled ingestion pipeline, decoupled from request volume, is the correct fit here.

15

Best Practices & Common Mistakes

15.1 Best practices

  • Always show both numbers, never just the delta. Displaying “you paid a 0.6% fee” without showing the actual mid-rate and customer rate side by side is a weaker transparency signal and easier to distrust; showing all three builds credibility.
  • Version every pricing rule and every published rate. This is what makes the system auditable and disputes resolvable months later.
  • Define staleness thresholds explicitly, per currency pair, and enforce them in code, not just policy documents. Exotic, thinly traded pairs may reasonably tolerate looser thresholds than major pairs.
  • Treat pricing configuration changes with the same rigor as code deployments — review, staged rollout, rollback plan.
  • Design the disclosure format to be jurisdiction-configurable from day one rather than retrofitting it later, since global payment platforms almost always end up needing per-market disclosure variants.
  • Make the rate-lock window a tunable parameter, not a constant, so it can be adjusted per corridor based on observed volatility and hedging cost.

15.2 Common mistakes

  • Confusing “mid-rate” definitions across teams. If the aggregation methodology isn’t precisely documented and shared, different parts of the platform (marketing pages, statements, in-app quotes) can end up displaying subtly different “mid rates,” undermining trust even though nothing is technically wrong.
  • Ignoring rounding and currency-decimal-precision edge cases. Currencies have different standard decimal precisions (Japanese Yen typically has zero decimal places, most others have two, some have three), and inconsistent rounding between the mid-rate display and the actual charged amount produces visible, confusing discrepancies of a fraction of a currency unit.
  • Under-provisioning the audit pipeline relative to the serving plane. If quote serving scales to handle a traffic spike but the audit event pipeline doesn’t, a backlog builds silently and compliance data lags behind, sometimes for hours.
  • Testing the pricing engine only with “happy path” contexts. Edge cases — a customer whose tier changes mid-session, a currency pair added mid-day, a promotional rule with an ambiguous precedence versus a standard rule — are where markup calculation bugs actually surface in production.
Everyday analogy

Rounding and precision mistakes are a bit like a grocery receipt where the line-item prices don’t quite add up to the printed total because of how tax was rounded per item versus on the total — individually tiny, but the kind of inconsistency that makes a careful customer lose trust in the whole system, even when no one was actually overcharged.

16

Real-World / Industry Examples

Cross-Border Remittance

Real-Rate Marketing as Product

Remittance-focused platforms built their differentiation on this exact capability: showing the mid-market rate pulled from aggregated FX data providers, alongside a clearly itemized transfer fee, before the customer sends money. Their systems must handle dozens of send/receive currency corridors, each with different liquidity, provider coverage, and regulatory disclosure requirements, which is precisely the kind of context-driven pricing engine described in this article.

Card Networks

Two-Layer Markup

When a customer makes a purchase abroad on a card, the card network converts the merchant’s local currency charge into the cardholder’s billing currency using a network-published reference rate, and the issuing bank may add its own markup on top, shown as a separate line on the statement in many regulated markets. This two-layer markup (network rate plus issuer markup) is a real-world example of the pricing engine needing to compose multiple markup sources into one final disclosed number.

Neobanks

Multi-Currency Wallets

Digital-first banks offering multi-currency wallets need real-time conversion pricing not just at the moment of a transfer but continuously, as customers view balances converted into their home currency inside the app — meaning the rate cache and quote API described here often serve read-heavy “informational” conversion displays at even higher volume than actual money-movement transactions.

16.1 Common threads across these systems

  • They all separate a continuously updated market data layer from a per-request pricing decision.
  • They all treat the disclosed fee breakdown as a compliance-grade artifact, not just a UI nicety.
  • They all handle multiple, sometimes conflicting, market data sources rather than trusting a single vendor blindly.
  • They all need the pricing logic to be adjustable by non-engineering teams without slowing down to a full software release cycle.
17

FAQ, Summary & Key Takeaways

Q1

Why can’t the mid-rate just come from one reliable provider like a central bank feed?

Central bank reference rates are typically published once daily and are meant for accounting or statistical purposes, not live trading — they lag far behind actual intraday market movement. A real-time transparency feature needs a rate that reflects the market at the moment of the customer’s transaction, which requires live, continuously updated commercial market data, usually from more than one provider for resilience and accuracy.

Q2

How fresh does the mid-rate need to be to count as “real-time”?

This is a policy decision, not a fixed technical constant, and it varies by currency pair liquidity and by jurisdiction’s disclosure rules. Major, highly liquid pairs are often refreshed on the order of single-digit seconds; less liquid, exotic pairs may reasonably use a looser threshold. What matters architecturally is that the threshold is explicit, enforced, and visible to the serving layer so it can refuse to serve an overly stale quote.

Q3

Should the rate-lock window be the same for every transaction type?

No. A small retail conversion and a large institutional-scale transfer carry very different hedging risk for the platform during the lock window. Many platforms tune the lock duration, or even whether a lock is offered at all, by transaction size and currency pair volatility.

Q4

What’s the single most important design decision in this whole system?

Separating the expensive, infrequent work of sourcing and validating market rates from the cheap, frequent work of serving a priced quote to a customer. Nearly every other good property of the system — low latency, cost control, independent scalability, resilience to a single provider outage — flows from getting that split right.

17.1 Key takeaways

  • Fee transparency is fundamentally a two-plane system: a rate ingestion pipeline that builds a trustworthy cached mid-rate, and a quote-serving layer that applies markup policy per request.
  • The mid-rate itself must be treated as an aggregated, validated, versioned value from multiple providers — never trusted from a single unvalidated source.
  • Markup policy belongs in externalized, versioned configuration evaluated by a rules engine, not hardcoded in application logic.
  • Rate-lock guarantees are a financial risk decision as much as a UX one, and should be explicit, bounded, and tunable per context.
  • Every served quote needs full audit traceability — which rate, which rule version, which provider inputs — to survive regulatory scrutiny and customer disputes.
  • Freshness, staleness fallback, and multi-region consistency all need explicit, tested policies rather than implicit assumptions, especially during high-volatility market events.
📌
The one idea to remember

Fee transparency isn’t a display feature bolted onto a payments system — it’s a real-time pricing engine with regulatory-grade audit requirements, wearing a simple two-number UI. Every architectural choice on the ingestion, aggregation, and audit paths is really a decision about how much of that responsibility the system carries versus how much it quietly punts onto the customer through opacity.