Your phone buzzes: a payment you did not make. Somewhere, a system had about a tenth of a second to say “allow,” “ask for more proof,” or “block,” and it got this one wrong. This article shows how to design that system in an interview and defend each choice. It is written for L5 candidates, with the L4 essentials marked and senior follow-ups at the end.

We use one running example the whole way: Pingpay, a made-up app where people chat and send each other money. Two bad things happen there. Spam is unwanted messages. Fraud is stolen accounts and fake payments. One system can handle both, because both ask the same question: should this event be allowed right now?

How to use this for Google or Microsoft-style loops

Large companies run system design rounds that test how you reason, not which tools you name. I could not verify employer-specific grading rules from official sources, so treat the 45-minute plan below as a practice aid, not a rule. Read each company’s own preparation pages (for example Microsoft’s guide to technical interviews, and the interview pages on Google’s careers site) and confirm the format with your recruiter. Prepare the habits that transfer everywhere: ask clarifying questions, state assumptions, start simple, do the math out loud, and name what would make you change the design.

The path through this article

We walk in the order you would speak in the room, and we never jump ahead. Each step is numbered the same way in the headings below. A suggested split for a 45-minute round is in brackets; it is a practice aid. The deep dives (step 8) and evaluation (step 10) are longer here than you could speak in the room, because the goal is that you understand them well enough to pick the few points that matter.

  1. The problem and the questions to ask (5 min, with step 2)
  2. What “good” means, and the limits
  3. A simple baseline (10 min, with steps 6 and 7)
  4. Workload and capacity (5 min, with step 5)
  5. Data and the learning problem
  6. The high-level design
  7. Model choices
  8. Deep dive on the parts most likely to break (15 min)
  9. Reliability and security (7 min, with step 10)
  10. Evaluation and operations
  11. Trade-offs and follow-ups (3 min, with step 12)
  12. Spoken recap

1. The problem and the questions to ask first

Begin by narrowing the problem. A good opening is: “Before I draw anything, can I ask a few questions?” Then ask, and say your assumed answer if the interviewer shrugs. For Pingpay, these are our assumptions:

QuestionAssumed answerWhy you asked
Which events?Sign-up, message send, and payment.Each event has different risk and different data. Payments move money; messages do not.
What does the system output?One decision per event: allow, challenge (ask for a code or ID check), or block.Three outcomes give you a safe middle option when you are unsure.
How fast?Payments and messages wait for the decision, so it is on the critical path. Target: 100 ms at p99, meaning 99 of every 100 requests are faster.The latency target decides what can run inside the request and what must run in the background.
How fresh?Behaviour from the last few minutes must count. Yesterday’s data is too old for attacks that happen in bursts.This is what forces a streaming design instead of a nightly job.
Who sees the result?The user sees allow, challenge or block. Staff see scores and reasons in a review tool. Attackers must never see reasons.It shapes the API and the privacy rules.
Out of scope?Legal case management, chargeback disputes, and image or video moderation.Saying what you will not build protects your time.

L4 essentials: clarify events, the three outputs, and the latency target. L5 adds: freshness and who sees what. The rest of the article builds on these answers.

2. What “good” means, and the limits

Say early that accuracy is a trap. In Pingpay, about one payment in a thousand is fraud. A system that always says “allow” is 99.9% accurate and useless. So we measure four different things, and keep them apart:

LayerMeasureWhy it matters
ProductFraud loss in dollars; share of good users blockedThis is the business result. It is what leaders care about.
ModelPrecision, recall, and PR-AUCPrecision: of the events we flag, how many are truly bad. Recall: of all bad events, how many we flag. PR-AUC summarises both and works well when bad events are rare.
Systemp99 latency under 100 ms; availability 99.95%A slow or broken decision blocks real payments.
Safety and costReview hours per day; appeal rateHumans are expensive, and wrongly blocked users complain and leave.

The picture below makes precision and recall concrete. Take one day of Pingpay payments: 10 million payments, 10,000 of them fraud. Say the system catches 8,000 fraud payments and wrongly flags 889 good ones (the full math is in step 4). Every box is a count of payments.

Flagged by system
Not flagged
Really fraud
(10,000)
8,000Caught. This is the win.
2,000Missed. Money lost.
Really good
(9,990,000)
889Wrongly flagged. A real person is upset.
9,989,111Correctly left alone.
Figure 1 — One illustrative day. Recall = 8,000 ÷ 10,000 = 80%. Precision = 8,000 ÷ (8,000 + 889) = 90%. Notice that the bottom-right box is huge, which is why plain accuracy hides all the interesting mistakes.

The central tension is simple: every extra bad event we catch risks blocking one more good user. The whole design is a way to choose that trade-off on purpose.

3. Start with a baseline

A strong candidate does not jump to a neural network. Propose the simplest thing that could work, so you can later show what the model adds. For Pingpay, the baseline has no machine learning:

This baseline is fast, easy to explain, and ships in days. Its weakness is also easy to explain: attackers learn the thresholds and stay just under them, and every new attack needs a human to write a new rule. That weakness is the reason to add a model.

The baseline does not go away. We keep the rules after the model arrives, for three jobs: hard policy limits (never pay out to a sanctioned account, whatever the model says), instant response to a brand-new attack while the model catches up, and a safe fallback when the model is unavailable. Two habits keep rules healthy over time. First, every rule has an owner and an expiry date, so old rules do not pile up and quietly block good users. Second, a new rule first runs in shadow mode: it writes down what it would have done, but does not act. After a few days you read the log, count the good users it would have hit, and only then switch it on.

Also say how you will compare later: “I will report the model against this baseline at the same good-user block rate, so we can see what machine learning actually adds.” This one sentence shows interviewers you plan to prove value, not assume it.

4. Workload and capacity

Do the calculations with units, and say what each result means for the design. These numbers are assumptions, not measurements.

Traffic. 50 million daily active users, 10 scored events each: 500 million events per day. Divide by 86,400 seconds: about 5,800 events per second on average. Plan for peaks of about 3 times that: 17,400 per second. Meaning: one machine cannot do this, so the Decision Service must be stateless and scale by adding copies.

Feature reads. Each event looks up history for four things: the user, the device, the network address and the payee. That is about 70,000 key-value reads per second at peak (17,400 × 4). Meaning: the Online Feature Store must be a fast key-value system, not a data warehouse.

Counter memory. To count “payments in the last 10 minutes,” the Stream Processor keeps one count per minute for each device and adds up the last ten. With 100 million active devices, 10 one-minute counts each, and 8 bytes per count, that is 100,000,000 × 10 × 8 bytes = 8 GB. Meaning: this fits in memory across a small cluster, so seconds-fresh counters are affordable.

Log volume. At 2 KB per event (the event, the features used, the decision), 500 million events per day is about 1 TB per day, or about 90 TB for 90 days. Meaning: this goes in cheap bulk storage, not the Online Feature Store. We keep 90 days because labels arrive late (see step 5).

Catch quality. Say 2% of events are payments: 10 million per day. At 0.1% fraud, that is 10,000 fraudulent payments daily. If the model catches 80% of them at 90% precision, it catches 8,000 and flags about 8,889. So about 889 good payments are flagged each day, 0.009% of good payments. That is the number you tell the interviewer you are trading for the 8,000 catches (Figure 1).

The latency budget below shows where the 100 ms goes. The point is not the exact values. The point is that feature reads, not the model, are the biggest cost, so we run them in parallel.

Network + API
10 ms
Feature reads
30 ms
Rules
5 ms
Model scoring
20 ms
Log + reply
5 ms
Slack
30 ms
Figure 2 — An illustrative 100 ms p99 budget (assumed, not measured). Feature reads are the largest step; the grey slack absorbs slow requests.

5. Data and the learning problem

A model learns from labels: “this event was bad” or “this was fine.” Labels are the hardest part of fraud, so spend time here. Four sources are common, and each is flawed in its own way:

Label sourceDelay (assumed)QualityMain flaw
Chargebacks (the bank takes money back)30 to 90 daysHighLate. Also, some customers dispute honest payments.
User reports (“this message is spam”)Minutes to daysMedium to lowNoisy. People also report things they simply dislike.
Human reviewersHoursHighFew, and only for cases we chose to send them.
Rule hits and blocksInstantCircularThe label is just our own earlier decision. Training on it teaches the model to copy the rules.

The delay problem

Last week’s events look “fine” only because their chargebacks have not arrived yet. If you train on them as good, you teach the model that fraud is fine. The timeline below shows what to do: pick a cut-off and train only on events older than it, so their labels have mostly settled. The newest events are used for monitoring, not for training.

labels still arriving: do not train settled: train training cut-off today 30 d 60 d 90 d 120 d 150 d Age of the event, counted back from today. Chargebacks arrive 30 to 90 days after the payment (assumed).
Figure 3 — Label maturation. Events younger than the cut-off still lack labels, so training on them would call hidden fraud “good.” Use them for monitoring only.

Feedback bias and the random holdout

We never learn the truth about events we blocked, because the attacker leaves and no chargeback ever comes. If we train only on what we allowed, the model slowly forgets what blocked attacks look like, and then the old attacks return. The fix is a small random holdout: for a tiny share of events the system would normally allow or challenge (say 0.5%), we record the score but do not use stricter action, so the outcome is observed anyway. Keep the share small, and cap the dollar risk per user, so a stolen account cannot take much. This holdout is also what lets us measure the true miss rate later (step 10).

Leakage, splits, cold start and cadence

6. The high-level design

Draw two paths and name them. The online path answers one event in milliseconds. The update path runs behind it, keeping features fresh and models current. Keep these names for the rest of the article.

Online path — one event, under 100 ms
Client app→ Event APIvalidates, assigns event_id→ Decision Servicerules + model→ allow · challenge · block
Online Feature Storeread by Decision Service Decision Storedecision by event_id Model Registrymodel version, loaded at start Review Queueborderline cases
Update path — runs in the background
Event Logevery event + decision→ Stream Processorcounters, seconds→ Online Feature Store
Event Log→ Offline Storehistory + labels→ Training Pipelineweekly→ Model Registry
Figure 4 — Two paths. The Decision Service reads the Online Feature Store, Decision Store and Model Registry, but never touches the Offline Store. The orange-outlined box is the component we deep-dive.

One request. A user taps “Send $50.” The Event API checks the request and gives it an event_id. The Decision Service checks the Decision Store in case this event was already decided, reads features for the four keys in parallel, runs the rules, then scores the model. The score maps to a decision (step 7). The reply goes back, and the event plus its decision are written to the Event Log.

One update. That same log entry is read by the Stream Processor, which bumps counters like “payments from this device in the last 10 minutes” in the Online Feature Store. Seconds later, the next payment from that device sees the new count. Separately, the Offline Store keeps the entry for the next weekly training run.

Say what each store holds, and why it is a different kind of store:

StoreHoldsAccess patternWhy this kind
Online Feature StoreLatest counters and profile values per user, device, address, payee~70,000 small reads/s at peak; writes from the Stream ProcessorFast key-value store; values are small and looked up by key
Decision StoreDecision per event_id, kept for about 24 hoursOne read and one write per eventShort-lived key-value store with expiry; it makes retries safe
Event LogOrdered stream of every event, the features used, the decisionAppend, then read by several consumersA durable log, so the Stream Processor and the Offline Store each read at their own pace
Offline StoreMonths of events and labels (about 90 TB for 90 days)Big scans for trainingCheap bulk storage; slow is fine
Model RegistryVersioned models, rule sets and feature definitionsRead at deploy and start-upVersion control with a promote and roll-back button

Three things are versioned so you can replay and roll back: the model, the rule set, and the feature definitions. Name that out loud.

7. Model choices

Choose the model after the data and the path are clear. Compare options on the same axes:

OptionStrengthWeaknessUse when
Rules onlyFast, clear, no trainingEasy to dodge; hand-maintainedDay one; hard policy limits
Gradient-boosted trees on tabular featuresStrong on counts and history; scores in single-digit milliseconds; easy to inspectNeeds good feature workOur starting point
Text model for message contentCatches spam wording and new link tricksMore compute; attackers rewrite textMessages, not payments
Graph features (shared devices, shared cards)Finds fraud rings that look normal one account at a timeBuilt offline; slower to refreshWhen rings are the main loss
Large language modelGood at reading messy textToo slow and costly for every eventOffline labeling or review help, not the hot path

Our choice: rules plus a tree model for all events, with a small text model for messages and graph-based features computed offline and stored as ordinary features. This keeps one scoring path and one latency budget.

What the model sees

For a payment, the feature list falls into a few plain groups. Naming the groups in the interview shows you know where the signal is:

For messages, add content features: links and their reputation, a text-model score, and near-duplicate counts. Spam campaigns send thousands of almost identical messages. A simple fingerprint of the text (a hash that stays the same for small edits) lets you count how many senders sent “the same” message in the last hour. A high count is a strong signal even when the words are new.

Fraud rings: why graph features help

Each stolen account may look ordinary. The pattern shows up only when you connect accounts through what they share. Below, five accounts share one device and a card, while a normal account touches neither. A model that sees only one account at a time will miss this; a feature such as “number of accounts on this device” catches it.

A1 A2 A3 A4 A5 D1 device C1 card N1 D2 device Ring: 5 accounts, 1 device, 1 cardNormal user
Figure 5 — Accounts are circles; devices and cards are squares. Lines mean “used by.” Offline, we count accounts per device (here 5) and store that as an ordinary feature the model can read in the hot path.

From score to decision

The model outputs a score between 0 and 1, but a score is not a decision. First make sure the score means what it says: a calibrated score of 0.8 should be fraud about 80% of the time. Calibration is a small extra step after training (fit a simple mapping from raw scores to real rates on recent held-out data), and without it your thresholds will drift every time the model changes.

Then pick two thresholds from the cost of each mistake. Letting fraud through costs the payment amount. Wrongly blocking costs trust and support time. Between the two thresholds, we ask for proof instead of choosing.

allowchallengeblock
0.00.600.851.0
Figure 6 — Score to decision. The 0.60 and 0.85 cut-offs are illustrative; in practice they are chosen on held-out data from the cost of each kind of mistake, and may differ for messages and payments.

Also keep three levels of “correct” separate: a model metric (PR-AUC), a decision metric (recall at a fixed good-user block rate), and a product metric (dollars lost). A model can improve the first and leave the last unchanged.

8. Deep dive: the parts most likely to break

You will not have time for everything. Choose by risk. For this system, two things fail most often in real life: features that disagree between training and serving, and a slow dependency stalling the decision. We take each one slowly, in the order: what goes wrong, a worked example, the fix, and how you would know it is working. A third, shorter dive covers keeping the counters correct under duplicates.

Deep dive A: features that must match

What goes wrong. The model was trained on a feature such as “payments from this device in the last 10 minutes,” computed from stored history. At serving time, the Stream Processor computes the same feature live. If the two calculations differ even slightly, the model sees different numbers in production than it learned from, and scores drift for no visible reason. This is called training-serving skew. It is common and hard to spot, because nothing crashes: the system just gets a bit worse.

A worked example. Device D7 makes payments at these times. The batch job and the stream job both want “count in the last 10 minutes” at 12:10:00.

PaymentHappened atReached the stream atBatch counts it?Stream counts it?
P112:00:3012:00:31YesYes
P212:04:0012:04:01YesYes
P312:08:0012:09:45 (delayed)Yes (uses when it happened)No, not yet arrived
P412:09:0012:09:01, then again at 12:09:03Yes, onceYes, twice (duplicate)

The batch job says 4. The stream job says 4 as well, but for the wrong reasons: it missed P3 and double counted P4. Today the totals happen to match; tomorrow they will not. Three separate bugs are hiding here: a different clock (when it happened versus when it arrived), a late event, and a duplicate. Each needs its own fix.

The fix, in four parts.

  1. One definition, written once. Every feature lives in a short written spec that both the batch backfill and the stream job read. No one re-implements it by hand. A spec has the entries below.
  2. Event time, not arrival time. Count by when the payment happened. The Stream Processor keeps a watermark, a moving line that says “I believe everything before this time has arrived.” It waits a short, fixed time (allowed lateness, for example 2 minutes) before closing a window. A payment later than that is still saved in the Event Log, so the next training run sees it, but the live counter ignores it. You choose the lateness by looking at how late events really arrive (say, 99.9% arrive within 2 minutes).
  3. Duplicate-proof updates. Covered in Deep dive C below.
  4. Point-in-time training data. When building training rows, compute each feature as of the event’s own time, using only events before it. This is called a point-in-time join. The simplest safe method is to rely on the features we logged at decision time (step 6) rather than recomputing them from today’s data.
Spec fieldExample for device_payments_10m
Entity (the key)device_id
AggregationCount of distinct event_id with type = payment
Window10 minutes, built from ten one-minute buckets
Time basisEvent time (when it happened), allowed lateness 2 minutes
Default if missing0, and a flag device_history_missing = 1
ExpiryDrop device state after 30 days without events
OwnerFraud ML team; change needs review
One written definition
Feature specentity, window, time basis, default
Training side (batch)
Event Log→Offline Store→Backfill jobreads spec, event time→Training rows
Serving side (stream)
Event Log→Stream Processorreads spec, event time, dedupes→Online Feature Store→Decision Service
Daily check
Sample of events→Recompute offlinevsValue logged at serving→Mismatch rate per feature
Figure 7 — Both sides read the same spec, and a daily job measures any gap. The gap, not the design, is what proves the two sides agree.

How you know it works. Do not trust the design; measure the gap. Every day, take a sample of events, recompute their features offline from the Event Log, and compare with the values that were logged at serving time. Track the share of mismatches per feature. Alert if one feature mismatches more than a small share (for example 1%). Also compare the feature’s average and spread between training data and live traffic; a sudden gap on a single feature points at a pipeline bug rather than a change in user behaviour.

When the numbers are allowed to differ. Perfect equality is not the goal. A late event that the live counter missed but the backfill saw is a known, small, bounded difference. Write it down, measure it, and make sure it is small enough that the model’s accuracy does not move. If it does move, shorten the lateness window or accept a slightly later feature.

Deep dive B: the decision call (low-level design)

What goes wrong. The decision call depends on several other systems. Any one can become slow. With no care, one slow store makes every request slow, requests pile up, and a small slowdown becomes an outage. The aim of this design is that a slow dependency costs a little accuracy, never the whole request.

The diagram shows one request from start to finish. Names match Figure 4. Time limits are the budget from Figure 2.

Event API Decision Service Decision Store Online Feature Store Event Log ① decide(event_id, deadline 100 ms) ② get(event_id): already decided? miss (limit 5 ms) ③ read 4 keys in parallel features (limit 30 ms; late reads get defaults) ④ rules (5 ms), thenmodel score (20 ms) ⑤ put(event_id, decision) ⑥ allow · challenge · block ⑦ append event, features, decision (async, after reply)
Figure 8 — One decision call. Solid arrows are requests, dashed arrows are replies. Steps ① to ⑥ must fit the 100 ms deadline; step ⑦ happens after the reply, so a slow Event Log never slows a user.

Walk through it.

  1. Deadline. The Event API stamps the request with a deadline, 100 ms from now. Every later step is told how much time is left, and no step is allowed to wait longer than what remains. Without this, a retry deep inside could keep working long after the user has given up.
  2. Dedupe first. The Decision Service looks up the event_id in the Decision Store. If the same event was already decided (the phone retried after a bad network), return the saved decision. Without this, a retried payment could be decided twice and could even get two different answers. Two copies of the same event can also arrive at once; to handle that, the first copy writes a “working on it” marker using a set-if-absent operation, and the second copy waits briefly for the result.
  3. Parallel reads. The four feature reads go out at the same time, so the wait is the slowest one, not the sum of four. Each has a hard limit of 30 ms. If one read is late, we do not wait: we use the default value from the feature spec and set a flag such as features_partial = 1. The model was trained with such flags present (we simulate missing values in training), so it knows to be more careful when they are set.
  4. Rules, then model. Hard-block rules run first. A rule that says “banned device” ends the request immediately, with no model call. Otherwise the model scores. The model is loaded into the Decision Service’s own memory at start-up from the Model Registry, so scoring is a local call with no network hop. The cost is that a new model needs a rolling restart or a careful in-memory swap; we accept that.
  5. Decide and save. Map the score to allow, challenge or block, and save it under the event_id. Return it to the client.
  6. Log afterwards. The write to the Event Log happens after the reply, so a slow log never slows a user. If the log is down, buffer on local disk for a short time and retry.

Retries, hedging and circuit breakers. Three small tools keep a bad dependency from spreading:

A worked failure. Suppose the Online Feature Store’s p99 climbs from 12 ms to 80 ms at 14:00. What happens, step by step?

WhenWhat the system doesWhat users and the team see
14:00:00Reads that pass 30 ms are cut off; defaults and features_partial are used.Latency stays near budget. A few percent of requests carry partial features.
14:00:20The partial-feature rate crosses the alert line; the circuit breaker opens for the worst keys.An alert fires on “partial features above 5%.” Request latency actually improves, since we stop waiting.
14:01 onThe model runs on defaults. Rules and amount limits matter more. Payments above a set amount go to “challenge.”Slightly more challenges; no outage. Fraud catch dips a little.
14:15The store recovers; the breaker lets test requests through, then closes.Partial rate returns to near zero. The team reviews the dip in catch rate.

Deep dive C: counters that survive duplicates

Streams generally promise that each event is delivered at least once, which means that after a failure the same event can come again. For a counter, that means double counting. The fix is to make each update idempotent: applying it twice has the same effect as once.

Note what this does not cover: writing the counter and sending a message to another service are separate actions, and no setting makes the pair “exactly once” by itself. That is why we deduplicate by id instead of relying on a delivery guarantee. Check your platform’s documentation for the exact guarantees you depend on.

9. Reliability and security

Trace real failures, one at a time, and say what the user sees. A helpful habit: ask of each failure, “is it worse to let a bad event through, or to block a good one?” That answer tells you whether to fail open (allow) or closed (challenge or block).

Level 0 · NormalAll features, model, rules.p99 under 100 ms
Level 1 · Partial featuresDefaults for late reads; model told with a flag.trigger: reads past 30 ms
Level 2 · Rules plus default scoreModel skipped; amount limits and blocklists decide.trigger: model errors or timeouts
Level 3 · Risk-based failsafeMessages: allow and scan later. Large payments: challenge.trigger: Decision Service overloaded
Figure 9 — The degradation ladder. Each step gives up some accuracy to keep answering. Moving down is automatic; moving back up waits for health checks.
What failsResponseWhy
Online Feature Store is slow or downUse defaults; for messages, allow and scan later. For large payments, challenge.Failing open is fine when harm is small and reversible. Failing closed fits risky, money-moving actions.
Model fails to load or times outRules plus default score; alert if more than a small share of requests do this.The system degrades instead of stopping.
Stream Processor lagsCounters go stale; alert on lag; add a “data age” feature so the model knows.Stale counters help attackers burst quickly.
Event Log unavailableDecisions continue; buffer locally for a short time; raise an alert.Logging must never block a payment, but lost logs mean lost training data, so watch it.
Traffic spike or retry stormShed load by dropping low-risk event types first; retries use backoff with a small cap.Unlimited retries turn a small outage into a big one.
Bad model or rule releasePin the previous version in the Model Registry; takes effect without a deploy.Rollback must be faster than the damage.
Whole region downFail over to a second region with its own feature store copy; accept slightly staler counters.Decisions in a second region are better than no decisions.

Attackers are part of the design

Privacy and access

Store identifiers hashed where you can. Keep personal data in the Offline Store only as long as you need it, and set a retention limit. Restrict who can open the Review Queue, log every access, and keep reviewer tools separate from the production scoring path so a compromise of one does not give the other. If users can ask for deletion, make sure the Offline Store and training sets can honour it.

10. Evaluation and operations

This is where good candidates separate from average ones. Cover three stages: before release (offline), during release (online), and after release (monitoring), and tie them together with one idea: a metric is only useful if you know which decision it will change.

Before release: offline evaluation

Set up the test properly. Use the time-based split from step 5, and only events whose labels have settled. Keep a final “test” period that you look at once; use a separate “validation” period for tuning. If you tune on the test period, your reported score is too good.

Look at the whole curve, not one number. A model has many possible operating points. The precision-recall curve shows all of them, and the thresholds you pick are points on it. The chart below uses made-up numbers to show the shape: as you catch more fraud (move right), precision falls, slowly at first and then quickly.

0.4 0.6 0.8 1.0 0.4 0.6 0.8 1.0 A B C recall (share of fraud caught) precision
Figure 10 — An illustrative precision-recall curve (made-up points, not a trained model). Moving right catches more fraud but flags more good users. Points A, B and C are the operating points in the cost table.

Pick the operating point from cost. Suppose, as an assumption, a missed fraud payment costs $120 on average, and a wrongly flagged good payment costs $60 (support time, lost trust, some customers leaving). The daily cost for each point is (missed fraud × $120) + (wrongly flagged × $60):

PointRecallPrecisionMissed fraudWrongly flaggedDaily cost
A80%90%2,000889$293,333
B88%80%1,2002,200$276,000
C92%60%8006,133$464,000

B is cheapest in this example, even though A looks “cleaner.” The lesson is the method, not the numbers: moving from A to B saves more in missed fraud than it costs in extra wrongly flagged users, but moving on to C costs more than it saves. If the cost of a wrong flag were higher, A would win. Say this out loud: the right threshold is where one more catch costs more in wrong flags than it saves in fraud.

Check slices. A model that is good on average can be bad for one group, and that is usually where complaints begin. Report the same metrics for each slice, and compare to the overall number:

SliceWhy check itWhat bad looks like
New accounts (under 7 days)Little history, and where most fraud startsRecall much lower than average
Each country or regionDifferent habits, payment methods, languagesFalse flags much higher in one region
Device type and app versionOld apps may send fewer signalsMissing features cause strange scores
Payment size bandsCost of mistakes differs by amountMany small frauds slip through
Heavy business usersNormal behaviour looks like fraud (many payments)Good users flagged for being busy

Beware of testing on only what you allowed. Your logged data holds labels mostly for events the old system allowed or reviewed. If you test a new model on only those, it looks better than it is, because the events the old system blocked are missing. Two protections: score the random holdout from step 5 (a fair sample), and, as a more advanced fix, weight each reviewed event by one divided by the chance it had of being reviewed (called inverse propensity weighting). You do not need the formula in the room; knowing the problem exists is the senior signal.

Use backtests. Replay last month’s events through the new model and the old one, with features computed as of each event. Compare the two on the same events. Include a stress test: replay a known past attack and check that the model would have caught it.

During release: online evaluation

Offline tests cannot tell you how real traffic, new attackers and real users will react. Roll out in stages, each with a clear exit rule:

1 · ShadowNew model scores every event; its answer is ignored.exit: no crashes, latency in budget, scores match expectations
2 · Canary (1%)Real decisions for 1% of accounts.exit: guardrails hold for a full day
3 · A/B (50%)Half the traffic on each model, sample size set in advance.exit: lower loss, guardrails hold, enough fraud cases seen
4 · Full + holdoutEveryone on the new model; a tiny slice stays on the baseline.keep: rollback by pinning the old version
Figure 11 — Four stages, each with a written exit rule. Any guardrail breach at any stage rolls back to the pinned previous version.

The label delay makes online testing slower than usual. Chargebacks take weeks. While you wait, track early signals: user reports, reviewer-confirmed fraud, challenge failure rate, and account-takeover reports. Treat these as hints, and confirm with chargebacks when they arrive.

Guardrails. A guardrail is a number that must not get worse. For Pingpay (illustrative limits):

GuardrailLimit (assumed)If crossed
p99 decision latencyUnder 100 msStop the rollout and roll back
Good-user block rateNo more than 0.02% of good paymentsStop and investigate
Appeals per 1,000 decisionsNo rise of more than 20% over baselinePause and review the appealed cases
Challenge pass rate for good usersNo sharp dropCheck for a broken challenge flow
Partial-feature rateUnder 1%Fix the feature pipeline first

Humans in the loop

Reviewers handle the challenge band, and, importantly, a random sample of both the allowed and the blocked. Only the random sample tells you what you are missing, because reviewing only the borderline cases measures only the borderline. Take a few steps to keep the labels trustworthy: write clear review guidelines, have two people label a small share of cases to measure how often they agree, and send disagreements to a senior reviewer. Add a quality dashboard for reviewers, and rotate them so one person’s habits do not become “the truth.” If you use a language model to summarise cases for reviewers, compare its summaries with human judgement on a sample before you trust it, and never let it set the final label alone.

Plan staffing from the numbers: if the challenge band sends 2,000 cases per day to review and a person can finish 150 per day, you need about 13 reviewers, plus sampling work. If you cannot staff that, the thresholds must change, not the staffing assumption.

After release: monitoring

Models do not break loudly; they slowly get worse. Watch these signals, each with a reason and a first action:

SignalHow to watch itWhat a change often meansFirst action
Feature distributionsCompare today with training data using a drift score such as the Population Stability Index (PSI), which sums how much each value bucket’s share has movedA broken pipeline, or a change in user behaviourFind the one feature that moved most
Score distributionPlot the share of scores in each band every hourAnything upstream changed, or an attack beganCompare with feature drift to locate it
Decision mixShare of allow, challenge, block per hour and per sliceA bad release, or a new campaignCheck recent deploys first
Feature freshnessAge of the newest counter updateThe Stream Processor is laggingPage the data team
Realized fraud and good-user blocksMatured labels, by weekly cohortTrue model quality, but weeks lateCompare with the plan; adjust thresholds or retrain
Review backlog and appealsQueue length and ageToo many challenges, or too few reviewersRaise a threshold or add capacity

When to retrain. On a schedule (weekly), and early when feature or score drift is large, when a new attack is confirmed, or when the realized loss rises above plan. Each retrain goes through the same offline checks and rollout stages as any release. A retrain is a release, not a cron job that pushes straight to production.

Alert wisely. Page a person for things that hurt users now: latency, error rate, a sudden jump in block rate. Open a ticket for slow things: gentle drift, label shifts. Too many pages train people to ignore them.

An incident plan. When block rate jumps: (1) pin the last good model version, which takes effect without a deploy; (2) compare feature drift to find the cause; (3) look at the decision mix by slice to see who was hit; (4) review a sample of wrongly blocked events and restore access to the affected users; (5) write up what broke and add a test or alert so it cannot go unnoticed again.

11. Trade-offs and follow-ups

DecisionChosenRejectedCost of choiceReconsider when
Main modelBoosted trees + rulesDeep sequence modelNeeds hand-built featuresFeature work stops helping and data is large
FreshnessSeconds-fresh countersNightly batch onlyStreaming is harder to runAttacks stay slow and daily batches are enough
Middle outcomeChallenge bandOnly allow or blockExtra friction for some usersChallenge pass rate shows it adds no signal
Model locationIn-process in the Decision ServiceSeparate scoring serviceRolling restarts for new modelsModels become too large for each copy to hold
Failure stanceOpen for messages, closed for large paymentsOne rule for all eventsSome fraud slips through during outagesLoss during outages becomes material

Follow-up 1: Attackers adapt to our model. What now?

Assume it will happen. Keep the random holdout so new patterns show up in labels. Alert on the share of “near-threshold” scores rising, which often means probing. Add fast rules for a new pattern while the model catches up in the next retrain. Do not reveal scores, and limit how many decisions one account can trigger.

Follow-up 2: Good-user blocks jump an hour after a release. What do you do?

First roll back by pinning the previous version, then investigate. Compare feature distributions before and after: a pipeline change (for example a counter that stopped updating) is more common than a model bug. Look at the decision mix by slice to find who is affected, then check logged feature values for those events. Restore access to the affected users, and add an alert or test so the same failure is caught next time.

Follow-up 3: A brand-new fraud type appears. How do you respond in a day?

Use reviewer labels and the Event Log to find examples, write a targeted rule with a short expiry, and send it through shadow mode. Add a feature that captures the pattern, then include it in the next retrain. The rule buys time; the model makes it durable.

Follow-up 4: Labels arrive in 60 days. How do you ship a model improvement sooner?

Use early signals (reviewer-confirmed fraud, reports, challenge failures) as a proxy to decide on canary and early A/B results, and confirm with real chargebacks later. Backtest on older matured data first. Keep the first rollout small and reversible, so a wrong early read costs little.

12. Spoken recap

You can say this aloud in about a minute:

“Pingpay needs an allow, challenge or block decision on every message and payment, within 100 ms. Because fraud is rare, I measure recall and precision and dollar loss, not accuracy. I start with rules and velocity limits, then add a boosted-tree model on fresh features. Online, the Decision Service checks for a saved decision, reads features in parallel with hard time limits, applies rules, scores the model, and maps the score to a decision. In the background, a Stream Processor keeps counters fresh and a weekly Training Pipeline retrains from time-split data, using a small random holdout so labels are not biased. If a dependency is slow, we fall back to defaults and rules, and risky actions fail closed. I choose thresholds from the cost of each mistake, roll out in shadow mode, then canary, with guardrails and a one-step rollback, and watch drift and matured labels afterwards.”

Split your depth by level:

What is assumed and what is verified

Every number in this article (traffic, fraud rate, latency budget, costs, thresholds, label delay, guardrail limits, curve shapes) is an illustrative assumption chosen to make the math easy to follow. None comes from a real deployment, and nothing here was executed or benchmarked. The precision-recall chart is drawn from made-up points, not from a trained model. The grading rules and loop formats of specific employers were not verified from official sources. Before relying on a product detail, such as stream delivery guarantees or feature store latency, read your platform’s current documentation; for example, Google Cloud describes online feature serving for low-latency reads, though it does not promise the numbers used above.