Skip to content

Designing a Fraud Detection System

Synchronous fraud check in the payment path, random forest scoring, and Spark pipelines for training data.

Builds on Message Queues and Streams and Sharding and Partitioning

Stage 5advanced16 min read

The whiteboard for this problem is in excalidraw/8. Designing a Fraud Detection system.excalidraw. This file is the full write up of that board.

1. Problem Statement

Financial transactions are risky and prone to fraud, so banks deploy a fraud detection system. It has to:

  • Detect fraud in real time.
  • Block the transaction.
  • Inform a customer support executive so they can take the customer's consent.

When a user initiates a transaction, the bank makes an entry, but before proceeding any further it checks for potential fraud.

  • If it is not fraud, allow the transaction.
  • If it looks like fraud:
    1. Hold the transaction.
    2. Involve a support executive.
    3. The executive calls the customer and confirms.
    4. If the customer says yes, allow the transaction. Otherwise abort it.
Loading diagram…

2. Requirements

2.1 Functional Requirements

RequirementDetail
Register a transactionRecord it before doing anything else
Real time scoringClassify every transaction as fraudulent or not
BlockHold a suspicious transaction rather than completing it
Involve supportCreate a case so an executive can call the customer
Resolve a caseExecutive confirms or rejects, and the transaction proceeds or aborts
Notify the customerTell them their transaction was held
Learn from outcomesFeed confirmed frauds back into training data
Rules as well as a modelHard rules such as blocked regions must always apply

2.2 Non Functional Requirements

  1. Low latency. The fraud detection service must respond within 200 ms. It sits inside the transaction path, so every millisecond is on the customer's clock.
  2. High throughput. Peak load, such as a salary day or a sale, must be handled without degrading.
  3. Adaptable. New fraud patterns appear constantly, so new rules and retrained models must ship without touching the transaction service.
  4. Available. The detection service being down must not stop the bank from taking transactions, but it also must not silently let everything through.
  5. Auditable. Every decision must be explainable later, since money and regulators are involved.

2.3 Out of Scope

The core ledger and settlement, KYC onboarding, chargeback processing with card networks.

3. Storage for Transactions

We need the ability to register a transaction.

source_acc | target_acc | status | amount | currency | ip | region | location | target_bank | ...

Status values:

INITIATED | BLOCKED | FRAUD | ALLOWED | DONE | FAILED

The schema is simple and clearly structured, so the choice of database is not complicated. We start relational. The data is also shardable, and we shard on source_acc, because every query is about one account's activity.

We can drop foreign key constraints here. At this write volume, and with sharding in play, enforcing referential integrity across shards costs more than it gives. Correctness moves into the application layer.

Loading diagram…

The bank API is a set of servers behind a load balancer, drawn as one box from here on.

4. The Transaction Handler

For an incoming HTTP request the handler:

  1. Gets the source and target account entries.
  2. Extracts details from the HTTP request, such as IP, location and user agent.
  3. Sends those details synchronously to fraud detection.

Fraud detection is a simple HTTP service that:

  1. Receives the transaction and request details over HTTP.
  2. Returns whether it is fraudulent or not.

The bank API checks fraud detection synchronously and updates the database based on the answer.

Loading diagram…

4.1 Why Synchronous

This is the one place in these designs where a synchronous call is correct. The decision has to be made before money moves. Making it asynchronous would mean either completing the transaction and reversing it later, which is exactly the loss we are trying to prevent, or holding every transaction until a worker gets to it, which makes normal payments feel broken.

The 200 ms budget is what makes synchronous acceptable, and it is why the model must be loaded in memory and never fetched per request.

5. Fraud Detection Algorithm

The service uses many parameters to judge whether the current transaction is fraudulent. We generate a lot of decision trees, which is a random forest, from training data and train the model. Every transaction is classified as either fraudulent or not fraudulent.

Different trees look at different feature combinations:

  • One tree for location plus time.
  • A second tree for location plus bank.
  • A third tree for time plus user plus bank.
  • And so on.

A new transaction is classified by all the trees and the majority answer wins.

Loading diagram…

A forest is a good fit here because each tree is a series of cheap comparisons, so scoring is fast enough for the 200 ms budget, and the path through a tree is readable, which matters when a regulator asks why a transaction was blocked.

5.1 Rules Alongside the Model

Some decisions must not be probabilistic. Transactions from a sanctioned region, or above a hard limit, are blocked outright. So the service evaluates rules first and the model second, and a rule hit short circuits the model.

Rules are configuration, not code, so a new one ships without a deploy. The model handles patterns nobody wrote a rule for.

6. Preparing the Training Data

Who knows which transactions were actually fraud? Customer support and the complaint portal. We need the transactions that had a fraud dispute.

Loading diagram…

We write a job that periodically processes the customer support database, extracts the fraudulent transactions, the communication summary and the details shared on the ticket, and puts them into blob storage such as S3.

The amount of data moved is huge, so we need distributed computing. One machine reading, transforming and writing would be far too slow. We use a big data tool such as Spark.

Loading diagram…

To train the model we also need the transaction details themselves: who sent to whom, from which IP, location, region, currency and so on. All of that is in the transaction database, so the same kind of Spark job moves it to S3.

6.1 Training

Now that all the data is in S3, we use distributed ML libraries to build the model. For example Spark with MLlib trains the decision trees and puts the model back in S3.

The model is just a file holding the serialized version of the decision trees. That single sentence is what makes the serving story simple.

Loading diagram…

7. Serving the Model

The fraud detection service is a simple HTTP service that:

  1. Loads the model from S3.
  2. Creates an in memory structure to classify transactions.
  3. Exposes an API to do this synchronously.
Loading diagram…

The user facing transaction API calls fraud detection to find out whether the transaction is fraudulent. If not, the transaction proceeds. If it is, customer service is involved and a notification is sent to the user.

Loading the model at startup is what keeps the request path fast. No network call, no deserialization per request, just tree traversals over in memory structures.

New model versions are published to S3 with a version marker. Services poll it, load the new model into a separate structure, and swap the reference when it is ready, so no request ever waits on a load. A bad model rolls back by pointing the marker at the previous file.

8. API Design

8.1 Transaction API, Public

MethodPathPurpose
POST/api/transactionsInitiate a transaction
GET/api/transactions/:idStatus of one transaction
GET/api/accounts/:acc/transactionsAccount history, paginated

Initiate:

{
  "sourceAcc": "ACC7781920",
  "targetAcc": "ACC5510338",
  "amount": 4500000,
  "currency": "INR",
  "targetBank": "HDFC",
  "idempotencyKey": "app-req-99120"
}

Response when it is held:

{
  "transactionId": "t_88120",
  "status": "BLOCKED",
  "reason": "pending_verification",
  "message": "We have put this transaction on hold. Our team will call you shortly.",
  "caseId": "c_4410"
}

Response when it goes through:

{ "transactionId": "t_88121", "status": "ALLOWED", "riskScore": 0.07 }

The customer never sees which rule or feature fired. Telling an attacker exactly which signal tripped is a free tuning loop for them.

8.2 Fraud Detection Service, Internal

MethodPathPurpose
POST/internal/fraud/scoreScore one transaction, synchronous
GET/internal/fraud/modelLoaded model version, for health checks
POST/internal/fraud/reloadForce a model reload
GET/internal/fraud/rulesActive rules
POST/internal/fraud/rulesAdd or change a rule

Score request:

{
  "transactionId": "t_88120",
  "sourceAcc": "ACC7781920",
  "targetAcc": "ACC5510338",
  "amount": 4500000,
  "currency": "INR",
  "targetBank": "HDFC",
  "ip": "203.0.113.9",
  "region": "IN-KA",
  "location": { "lat": 12.97, "lng": 77.59 },
  "userAgent": "BankApp/4.2 Android/14",
  "deviceId": "d_5512",
  "requestedAt": "2026-08-19T10:31:00Z"
}

Score response:

{
  "transactionId": "t_88120",
  "isFraud": true,
  "riskScore": 0.91,
  "decision": "BLOCK",
  "triggeredRules": ["AMOUNT_ABOVE_ACCOUNT_P99", "NEW_DEVICE_HIGH_VALUE"],
  "topFeatures": ["amount_vs_account_avg", "device_age_hours", "geo_distance_km"],
  "modelVersion": 42,
  "latencyMs": 38
}

The response carries triggeredRules, topFeatures and modelVersion because the internal caller has to be able to explain the decision six months later.

8.3 Case Management, Internal

MethodPathPurpose
GET/internal/cases?status=OPENQueue for support executives
GET/internal/cases/:idCase with transaction context
POST/internal/cases/:id/notesLog the call
POST/internal/cases/:id/resolveCustomer confirmed or denied

Resolve:

{ "outcome": "CUSTOMER_CONFIRMED", "executiveId": "e_221", "notes": "Verified on call" }

CUSTOMER_CONFIRMED releases the transaction and labels it as a false positive. CUSTOMER_DENIED aborts it and labels it as confirmed fraud. Both outcomes are training labels, which is the whole reason support is in the loop.

9. Data Model

9.1 Transaction Store, Sharded Relational

transactions

ColumnTypeNotes
iduuidPrimary key
source_accvarchar(20)Shard key
target_accvarchar(20)No foreign key, may be at another bank
amountbigintMinor units
currencychar(3)
target_bankvarchar(32)
statusenumINITIATED, BLOCKED, FRAUD, ALLOWED, DONE, FAILED
ipinetFrom the request
regionvarchar(16)
location_lat, location_lngnumeric
user_agenttext
device_idvarchar(64)
risk_scorenumericWhat the model returned
model_versionintegerWhich model decided
created_attimestamptz
updated_attimestamptz

Foreign key constraints are dropped deliberately, as noted on the board. The target_acc may not even exist in this bank, and cross shard constraints are not enforceable anyway.

accounts

ColumnTypeNotes
acc_numbervarchar(20)Primary key, shard key
customer_iduuidForeign key idea to customers.id
typeenumSAVINGS, CURRENT
home_regionvarchar(16)Baseline for geo features
opened_attimestamptz
is_frozenboolean

fraud_cases

ColumnTypeNotes
iduuidPrimary key
transaction_iduuidForeign key to transactions.id, unique
statusenumOPEN, CALLING, RESOLVED_CONFIRMED, RESOLVED_DENIED, TIMED_OUT
assigned_touuidForeign key to executives.id
risk_scorenumericCopy at case creation time
triggered_rulesjsonbWhat fired
opened_attimestamptz
resolved_attimestamptz
outcome_labelenumFRAUD, NOT_FRAUD, UNKNOWN

outcome_label is the most valuable column in the whole system. It is the ground truth the next model trains on.

case_notes

ColumnTypeNotes
iduuidPrimary key
case_iduuidForeign key to fraud_cases.id
executive_iduuidForeign key to executives.id
notetextCommunication summary from the call
created_attimestamptz

fraud_rules

ColumnTypeNotes
iduuidPrimary key
nametextUnique
expressiontextCondition over transaction features
actionenumBLOCK, FLAG, ALLOW_LIST
priorityintegerLower runs first
is_enabledbooleanKill switch
updated_attimestamptz

model_versions

ColumnTypeNotes
versionintegerPrimary key
s3_keytextSerialized forest file
trained_attimestamptz
training_rowsbigint
precision, recallnumericOffline evaluation
is_currentbooleanExactly one true row

9.2 Feature Store, Redis

Not a table. Precomputed per account aggregates that the model needs but cannot compute in 200 ms:

feat:acc:{acc}:avg_amount_30d
feat:acc:{acc}:txn_count_1h
feat:acc:{acc}:distinct_devices_7d
feat:acc:{acc}:last_txn_region
feat:ip:{ip}:txn_count_5m

A streaming job keeps these fresh. The question "how many transactions from this IP in the last five minutes" is exactly the kind of signal that catches an attack in progress, and it is impossible to compute from the transaction table inside the latency budget.

9.3 Relationships

Loading diagram…

In words:

  • One customer owns many accounts, one account belongs to one customer.
  • One account sends many transactions, one transaction has one source account.
  • One transaction may open at most one fraud case, and a case belongs to exactly one transaction.
  • One case has many notes, one note belongs to one case.
  • One executive handles many cases and writes many notes.
  • One model version scored many transactions, and each transaction records the version that scored it.
  • Rules trigger on many transactions, recorded as a list on the case rather than as a join table, because the list is written once and only ever read whole.

10. Key Flows

10.1 A Clean Transaction

Loading diagram…

10.2 A Suspicious Transaction

Loading diagram…

10.3 The Training Loop

Loading diagram…

This loop is what makes the system adaptive. Support outcomes become labels, labels become a model, and the model changes tomorrow's decisions.

11. Deep Dives

11.1 Meeting the 200 ms Budget

CostApproach
Model loadAt startup, never per request
Feature computationPrecomputed in Redis by a streaming job
Rule evaluationIn memory, ordered by priority, short circuit on the first block
Forest scoringTree traversals over in memory nodes, a few microseconds each
NetworkSame region, connection pooling, no chained service calls
LoggingAsynchronous, never in the response path

Rules run before the model on purpose. A blocked region is a single comparison, so there is no reason to score a forest first.

11.2 Precision Versus Recall

This is the real product decision, not a technical one.

  • A false positive blocks a legitimate customer, generates a support call and annoys someone who did nothing wrong.
  • A false negative lets actual fraud through, and the bank absorbs the loss.

The threshold is a business choice, and it is usually not one threshold. A practical setup uses bands: below 0.3 allow, 0.3 to 0.8 allow but flag for review, above 0.8 block and open a case. High value transactions get a lower block threshold than small ones, because the loss from a false negative scales with the amount and the cost of a false positive does not.

11.3 Class Imbalance

Fraud is rare, often well under one percent of transactions. A model that always says "not fraud" would be 99 percent accurate and completely useless. So:

  • Never evaluate on accuracy. Use precision, recall and area under the precision recall curve.
  • Rebalance the training set by undersampling clean transactions or oversampling fraud.
  • Weight the classes so a missed fraud costs the model more than a false alarm.

11.4 Why a Forest and Not a Neural Network

ReasonDetail
LatencyTree traversal is trivially fast and predictable
ExplainabilityThe path through a tree is readable, and regulators require an explanation
Tabular dataForests are strong on structured features, which is exactly what a transaction is
Training costSpark with MLlib trains this on commodity hardware
RobustnessLittle feature scaling needed, and outliers do less damage

11.5 Graph Signals

Individually clean transactions can form an obvious ring when viewed as a graph: many accounts sharing one device id, one phone number or one IP, moving money in a cycle. A graph database over accounts and devices finds these patterns.

Those queries are too slow for the 200 ms path, so they run as a batch job. The output is a precomputed risk flag per account, written into the feature store, where the synchronous path reads it as a single lookup.

12. Failure Handling

FailureEffectHandling
Fraud service downCannot scoreDecide by policy: allow small transactions with a flag for later review, and hold high value ones. Never blanket allow, and never blanket block
Fraud service slowTransaction latency risesHard timeout at 200 ms, and treat a timeout the same as unavailable
Feature store downMissing aggregatesScore with the features we do have, and shift toward a more conservative threshold
Bad model deployedSudden spike in false positivesMonitor block rate continuously, alert on a jump, roll back by pointing the version marker at the previous model
Model file corruptLoad could failVerify a checksum, build into a new structure, only swap on success, keep the old model otherwise
Case service downCases cannot openQueue case creation asynchronously, the transaction still holds
Support backlogCustomers wait on held transactionsAuto expire cases after a defined window and abort with a clear message, rather than leaving money in limbo
Transaction shard downThat account cannot transactReplicas with failover, other shards are unaffected
Spark job failsModel goes staleAlert, keep serving the current model, and retry. A stale model is far better than no model

The fail open versus fail closed question is genuinely harder here than in a rate limiter, because failing open costs money directly. The compromise above, splitting by transaction value, is what real systems do.

13. Tradeoffs

DecisionChosenAlternativeReason
Check timingSynchronous, before money movesAsynchronous with reversalReversing fraud is the loss we are preventing
Latency budget200 ms hard capBest effortThe customer is waiting inside a payment flow
Transaction storeSharded relationalDocument storeSimple fixed schema, and transactional status updates
ConstraintsForeign keys droppedEnforcedCross shard and cross bank references cannot be enforced
Shard keysource_accTransaction idEvery query is per account
ModelRandom forestNeural networkFast, explainable, strong on tabular data
Model deliveryFile in S3, loaded at startupRemote inference serviceNo network hop, no per request deserialization
FeaturesPrecomputed in RedisComputed at request timeWindowed aggregates cannot be computed in 200 ms
RulesBefore the modelModel onlyHard requirements must not be probabilistic, and rules are cheaper
LabelsFrom support outcomesManual labellingSupport already produces ground truth for free
PipelinesSparkSingle machine ETLData volume is far past one machine
Graph analysisBatch, into the feature storeLive in the request pathGraph traversals are too slow to be synchronous

14. Future Work

  • Stream processing with Flink or Spark Streaming for real time windowed features, replacing the periodic feature job.
  • Graph database such as Neo4j for synthetic identity rings, shared devices and shared phone numbers.
  • Shadow mode, where a new model scores live traffic without affecting decisions, so precision and recall can be compared before the switch.
  • Per segment thresholds, since a first time user and a ten year customer should not face the same bar.
  • Device fingerprinting and behavioural biometrics, such as typing rhythm and how the app is held, as extra features.
  • Feedback on false positives, tracking how often a blocked transaction is confirmed legitimate, and treating that rate as a first class metric.

15. Interview Talking Points

  • Say why this check is synchronous, and note that it is the exception. Most other designs in this folder push work off the request path, and here you cannot.
  • Name the 200 ms budget early, then show how every design choice serves it: the model in memory, the features precomputed, the rules short circuiting.
  • The sentence "the model is just a file holding the serialized decision trees" is what makes deployment and rollback obvious.
  • Explain where the labels come from. Customer support is the ground truth source, and most candidates never answer that question.
  • Bring up class imbalance without being asked. Accuracy is a useless metric when fraud is under one percent.
  • Discuss precision versus recall as a business decision with value based thresholds, not as one magic number.
  • Handle the availability question carefully. Neither blanket allow nor blanket block is acceptable, and splitting by transaction value is the answer.

In this topic

Whiteboard diagram

Open full screen
Loading diagram…
Designing a Fraud Detection system

Done with this topic?

How well do you know it?

PreviousDesigning Realtime Abuse Master