CYART // SCHEMA · REQUEST-PATH7 NODES · 6 LINKSLOOKUP1HTTPS234READ / WRITE5HIT FIRSTACTOR · User — browser / appACTOR01Userbrowser / appEDGE · DNS — resolves nameEDGE02DNSresolves nameEDGE · CDN / WAF — TLS ends hereEDGE03CDN / WAFTLS ends hereEDGE · Load balancer — L7, health checksEDGE04Load balancerL7, health checksSERVICE · API service — authn + authzSERVICE05API serviceauthn + authzDATA · Database — primary + replicasDATA06Databaseprimary + replicasDATA · Cache — short TTLDATA07Cacheshort TTL

Design a payment fraud scoring service

Design the service that scores every card authorisation in 50 ms p99: a real-time feature store with velocity features, rules and a model working as an ensemble, shadow mode and holdouts for safe change, and the chargeback feedback loop that arrives months later.

● HardFREEDesign problem·19 min read·Intermediate → Staff
payment fraudfraud scoringfeature storevelocity featuresshadow modechargebacks

01Understand the problem and size it

✓What you will learn
  • Set a latency budget (50 ms p99) and spend it stage by stage
  • Build velocity features (count and sum per card, device, merchant over sliding windows) with a real-time feature store
  • Combine rules and ML so each does what it is good at
  • Change models safely with shadow mode, champion/challenger and holdouts, and learn from chargebacks that arrive weeks later
◎Interview radar

What they probe: how you hit 50 ms with a feature store in the path, how velocity is computed without counting the current transaction twice or missing it, what happens if scoring times out, how training avoids leakage, why declined transactions have no labels. Where candidates slip: calling a batch warehouse in the hot path, no fallback, training on features computed after the fact, ignoring label delay.

A merchant or payment processor sends an authorisation request (card, amount, merchant, device, IP) and must answer approve, decline or challenge (for example 3-D Secure, the card scheme's step-up for online payments) within the card network's time limit. Fraud scoring sits inside that window. A chargeback is the cardholder disputing a transaction through their bank; fraud chargebacks are the main ground truth and can arrive up to around 120 days later under common scheme rules.

Functional requirements

  1. Score every authorisation and return approve, decline, review or step-up (3-D Secure) with reason codes.
  2. Support rules authored by fraud analysts without a deploy, and ML models trained by data scientists.
  3. Compute velocity features over windows from 1 minute to 30 days.
  4. Run new models and rules in shadow mode and learn from chargebacks and issuer fraud reports.

Non-functional requirements

  1. 10k transactions/s peak, 2k average.
  2. 50 ms p99 end to end inside our service.
  3. 99.99 % availability, with a defined fallback when scoring is slow or down.
  4. Training data is point-in-time correct (no feature from the future).

Back of the envelope

Sizingtxt
average 2,000 tps x 86,400 s     = 172.8 M transactions / day
peak    10,000 tps
entities per txn                 card, account, device, IP, email, merchant = 6 keys
online feature reads             one batched multi-get per txn -> 10,000 calls / s, 60,000 keys / s
online store size                200 M entities x 1 kB = 200 GB, x3 replicas = 600 GB in memory
decision log                     5 kB per txn (features + scores + decision)
  -> 172.8 M x 5 kB              = 864 GB / day, about 315 TB / year before compression
chargeback rate                  ~0.1 % of transactions -> ~173k fraud labels / day at most
▭On the whiteboard · Scope3 points
01
FR
score every auth | analyst rules + ML | velocity windows | shadow + feedback
02
NFR
10k tps peak | 50 ms p99 | 99.99 % with fallback | point-in-time training
03
out
the card network, 3-D Secure server, case management UI internals

02High-level design and the latency budget

FigScoring in the authorisation path
CYART // SCHEMA · REQUEST-PATH7 NODES · 6 LINKSLOOKUP1HTTPS234READ / WRITE5HIT FIRSTACTOR · User — browser / appACTOR01Userbrowser / appEDGE · DNS — resolves nameEDGE02DNSresolves nameEDGE · CDN / WAF — TLS ends hereEDGE03CDN / WAFTLS ends hereEDGE · Load balancer — L7, health checksEDGE04Load balancerL7, health checksSERVICE · API service — authn + authzSERVICE05API serviceauthn + authzDATA · Database — primary + replicasDATA06Databaseprimary + replicasDATA · Cache — short TTLDATA07Cacheshort TTL
  • ACTOR
  • EDGE
  • SERVICE
  • DATA
  • Live flow
  • Lookup / async
//The payment gateway calls the scoring service synchronously. The service fetches online features in one batched read, computes request-time features, runs rules and the model, applies the decision policy and returns. The transaction and decision are written to a stream that updates velocity aggregates, the offline store and the case queue.

Spending 50 ms

StageBudget (p99)How
Ingress, auth, parse3 msPersistent connections from the gateway, protobuf
Online feature fetch10 msOne batched multi-get to an in-memory store in the same zone
Request-time features3 msAmount vs card's average, BIN country vs IP country, local time
Rules2 msCompiled rule set in process
Model inference8 msGradient-boosted trees in process (often about 1 ms); a neural model via a sidecar with a hard timeout
Decision policy, response2 msScore bands by merchant and amount
Headroom22 msGC pauses, retries inside the budget, tail latency of the store

Per-stage p99s do not add exactly (tails rarely coincide), but budgeting as if they do gives a safe plan. Everything slow, such as writing the decision log, updating aggregates, opening cases, happens after the response, asynchronously.

Velocity features

Velocity is how much activity an entity has had recently: transactions per card in the last 10 minutes, total amount per card in 24 hours, distinct cards per device in 1 hour, distinct merchants per card in 24 hours. Fraud is bursty (a stolen card is drained fast), so these are among the strongest features.

  • Streaming aggregates (Flink or similar) consume the transaction stream and maintain bucketed counters per entity per window, written to the online store. Lag is typically a second or two.
  • The current transaction is not in the stream yet, and neither are ones from the last second. So at score time, read the stored buckets and add the current transaction in-request. For card testing (dozens of small authorisations a second), also keep a tiny synchronous counter per card with an atomic increment.
  • Distinct counts (cards per device) use HyperLogLog sketches; exact sets are too big.
Velocity at score time: stored buckets plus the current transactionts
// Sliding-window velocity from per-minute buckets kept in the online store.
// buckets: map of epoch-minute -> { count, amountMinor } for one entity, pruned beyond 24 h.
interface Bucket { count: number; amountMinor: number }

const MAX_WINDOW_MIN = 24 * 60;

export function velocity(
  buckets: ReadonlyMap<number, Bucket>,
  nowMs: number,
  windowMin: number,
  current: { amountMinor: number },
): { count: number; amountMinor: number } {
  if (!Number.isInteger(windowMin) || windowMin < 1 || windowMin > MAX_WINDOW_MIN) {
    throw new RangeError("windowMin must be an integer in 1..1440");
  }
  if (!Number.isSafeInteger(current.amountMinor) || current.amountMinor < 0) {
    throw new RangeError("amountMinor must be a non-negative integer");
  }
  const nowMin = Math.floor(nowMs / 60_000);
  let count = 1;                                   // include the transaction being scored
  let amountMinor = current.amountMinor;
  for (let m = nowMin - windowMin + 1; m <= nowMin; m++) {   // at most 1,440 iterations
    const b = buckets.get(m);
    if (b) { count += b.count; amountMinor += b.amountMinor; }
  }
  return { count, amountMinor };
}
▭On the whiteboard · Core3 points
01
online
in-memory feature store, one batched read, bucketed velocity + HLL distinct counts
02
score
rules (compiled) + GBT model in process, policy by score band
03
async
stream → aggregates, offline store, case queue, decision log

03Deep dives: ensemble, safe change, feedback

Rules and ML as an ensemble

LayerGood atExamples
Hard rules (pre-model)Compliance, known-bad, instant reaction to a new attackSanctioned country, card on block list, more than 5 declines in 10 min on one card
ModelSubtle combinations of hundreds of featuresGradient-boosted trees scoring 0 to 1
Policy (post-model)Business trade-offs per merchant and amountScore > 0.9 decline; 0.6 to 0.9 step-up with 3-D Secure; amount > $2,000 and score > 0.4 review
Override rulesAnalyst response during an active attackTemporarily decline gift card merchants from a new BIN range

Analysts get a rule language with backtesting (run the rule over last month's labelled data before enabling) and every rule carries an owner and an expiry date. Rules that never expire are how a fraud system ends up declining good customers for an attack that ended two years ago.

Training without leakage

A model trained on features computed today for a transaction from last month sees information the scorer did not have, such as later transactions on the same card. That is label leakage, and it makes offline metrics look far better than production. The fix is to train on the decision log: the exact features served at decision time. For new features, the offline store must support point-in-time joins (feature value as of the transaction timestamp).

Shadow mode, champion/challenger, holdouts

  • Shadow mode: the challenger model scores every live transaction alongside the champion; only the champion's score is used. Compare score distributions, decline rates and, as labels mature, catch rates. Shadow costs inference capacity, not risk.
  • Champion/challenger: route a small hashed share of traffic (for example 5 % by card) to the challenger's decisions once shadow results look good.
  • Holdout: you never learn whether a declined transaction was fraud. Approve a tiny random share of transactions the model would decline (under a strict amount cap) to get unbiased labels. Without it, each model is trained only on what the previous model let through.
!Selection bias compounds

Declined transactions get no chargeback, so they look like neither fraud nor good. Retraining on approved transactions only teaches the new model about the fraud the old model missed, and it slowly forgets the fraud the old model caught. A holdout, weighted by the inverse of its sampling rate, is the standard correction.

Chargeback feedback

  • Issuer fraud reports (for example Visa TC40 data) arrive in days and flag transactions the cardholder reported, even without a dispute.
  • Chargebacks arrive over weeks to months. Labels are only "mature" for transactions older than about 90 to 120 days, so evaluate models on matured windows and use early signals for monitoring.
  • Friendly fraud (a real customer disputing a purchase they made) pollutes labels. Keep reason codes and treat them separately.
  • Join labels back to the decision log by transaction ID and write them to the offline store for the next training run.

04Failure modes and trade-offs

Failure modes

FailureBehaviourWhy
Scoring exceeds 50 msGateway uses fallback: rules-only result computed first, or approve under a low amount limit and decline aboveDecided per merchant; never block all payments
Feature store partially unavailableScore with missing features flagged; the model is trained with missingness as a featureMissing data should not crash or silently zero
Stream lag (velocity stale)Synchronous per-card counter still catches card testing; alert on lag > 30 sBursts are the attack
Bad model pushDecline rate monitor per merchant; auto-rollback if it moves > 30 % in 10 minA spike in declines is an outage
Feature driftPopulation stability index per feature dailyUpstream schema or behaviour changes break models quietly

Trade-offs to state

ChoiceGainCost
In-process GBT vs remote deep modelPredictable 1 to 2 msLess accuracy on sequence patterns; mitigate with precomputed embeddings
Fail open under a limit vs fail closedRevenue and customer trust during outagesFraud exposure capped by the limit
Streaming aggregates vs in-request computationScales to 30-day windowsA second or two of lag; needs the in-request top-up
Holdout approvalsUnbiased labels, honest metricsReal fraud losses on the holdout, so cap amounts and share
Step-up (3-D Secure) vs declineSaves good customers in the grey zoneCheckout friction and abandonment
iMetrics that matter

Fraud basis points (fraud losses per $10,000 of volume), false decline rate measured on the holdout, approval rate per merchant, and the share of step-ups that pass. A model with better AUC that raises false declines can lose more money than it saves.

05Final design, follow-ups and self-test

▭On the whiteboard · Final design3 points
01
path
50 ms budget: batched feature read 10 ms, rules 2 ms, GBT 8 ms, 22 ms headroom; fallback per merchant
02
features
streaming bucketed velocity + in-request top-up + sync counter for card testing
03
learn
decision log as training set, shadow → champion/challenger, holdout, chargebacks + TC40 labels
“How to say it in the room

"I'd spend the 50 milliseconds deliberately: one batched read from an in-memory feature store, rules and a gradient-boosted model in process, and everything else async after the response. Velocity comes from streaming per-minute buckets, plus the current transaction added in-request, because the stream lags by a second and card testing doesn't wait. Rules handle compliance and live attacks, the model handles subtle patterns, and a policy table turns scores into approve, 3-D Secure or decline per merchant. New models go through shadow mode then a small challenger slice. I train on the decision log so features are point-in-time correct, and I keep a small capped holdout of would-be declines because otherwise I never learn what my declines cost."

Q1How would you detect card testing, many tiny authorisations on stolen cards?+

Per-merchant and per-device velocity of small amounts, a high share of declines with codes like invalid CVV, many distinct cards per device or IP. A synchronous counter catches it within seconds; respond by stepping up or declining low-value auths from that source.

Q2A merchant wants their own thresholds. How?+

Policy is data keyed by merchant and segment, versioned, with backtesting against that merchant's history before activation.

Q3Why not train on all transactions with the latest features from the warehouse?+

Leakage: later activity on the same card is visible, so offline metrics lie. Use the decision log or point-in-time joins.

Q4How do you scale to 100k tps?+

Partition the feature store by entity key, keep scoring stateless and horizontally scaled, and move rare heavy features to precomputed values. The budget stays the same.

Test yourself

Velocity features come from a streaming job that lags by about 2 seconds. A fraudster fires 30 authorisations on one card within 3 seconds. What design keeps the velocity feature accurate?

Finished this one?
Tick it and it shows as done on the roadmap and the course.