Fraud Detection System Design
Design a scalable enterprise Fraud Detection System — covering real-time scoring, rule engines, ML model serving, device fingerprinting, velocity checks, case management, feedback loops, and the explainability requirements that make fraud decisions defensible.
1-Hour Interview Roadmap
| Time | Topic |
|---|---|
| 0 – 5 min | Requirements clarification |
| 5 – 10 min | Capacity estimation |
| 10 – 18 min | High-level architecture + core services |
| 18 – 28 min | Real-time scoring pipeline — rules + ML model serving |
| 28 – 36 min | Feature engineering — velocity checks, device fingerprinting, graph signals |
| 36 – 43 min | Case management + analyst workflow + feedback loops |
| 43 – 50 min | Database design + API design |
| 50 – 56 min | Model lifecycle — training, deployment, champion/challenger |
| 56 – 60 min | Trade-offs + common interview mistakes |
What Are We Building?
An enterprise Fraud Detection System (FDS) that evaluates every transaction, login, account change, and payment in real time and produces a risk score with an action recommendation — approve, flag for review, or block — within the latency budget of the originating transaction (typically under 150 ms).
Fraud detection is one of the most technically demanding system design problems because it sits at the intersection of three disciplines: high-throughput low-latency systems engineering, machine learning model serving, and operational case management. Getting any one of those wrong costs the business money — either from fraud losses (false negatives) or from declined legitimate transactions (false positives). The cost of a false positive in payments is not zero: a declined card for a good customer generates churn, support calls, and reputational damage.
Real-world scale context:
| Company | Transactions/day | Fraud rate | Latency SLA |
|---|---|---|---|
| Visa / MC | 700 M+ | ~0.05% | < 100 ms |
| PayPal | 60 M+ | ~0.32% | < 150 ms |
| Stripe | 250 M+ | ~0.1% | < 100 ms |
| Retail bank | 5–20 M/day | ~0.2% | < 200 ms |
| Fintech app | 500 K–5 M/day | ~0.5–1% | < 300 ms |
The five challenges that make this hard:
- Sub-100 ms latency at high throughput — a scoring call cannot block the transaction for more than a fraction of the total payment round-trip. Feature computation, model inference, and rule evaluation must all happen in that window.
- Extreme class imbalance — fraud rates of 0.05–1% mean 99%+ of events are legitimate. Naive classifiers that always predict "not fraud" achieve 99%+ accuracy while being completely useless. Precision/recall at the tail matters far more than overall accuracy.
- Adversarial adaptation — fraudsters observe block decisions and adapt. A static rule set or a model that does not retrain will degrade over time as fraud patterns shift. The system must detect model drift and retrain continuously.
- Explainability and regulatory compliance — financial regulators require that adverse decisions (declined transactions, frozen accounts) be explainable to customers. "The model said no" is not a compliant answer. Every decision needs audit trail and human-readable rationale.
- Feedback latency — confirmed fraud labels arrive days or weeks after the transaction (after chargeback or investigation close). The training pipeline must handle delayed labels without poisoning the feature store with stale signals.
Functional Requirements
Core scoring:
- Accept a transaction or event (payment, login, account change) and return a risk score (0–1000) + action (APPROVE / REVIEW / BLOCK) within latency SLA
- Evaluate configurable rule sets before and after ML scoring
- Support multiple event types: card payment, ACH, wire, login, device registration, address change, beneficiary addition
Feature computation:
- Compute real-time velocity features (spend in last 1 min / 5 min / 1 hr / 24 hr / 7 days)
- Evaluate device fingerprint signals (new device, known device, emulator detected)
- Compute graph/network features (shared devices, shared accounts, money mule ring detection)
- Provide feature values to analysts for explainability
Case management:
- Route flagged transactions to analyst queues with risk score, feature breakdown, and decision rationale
- Support analyst decision recording (confirm fraud / clear / escalate)
- Close feedback loop — confirmed labels flow back to model training pipeline
Model lifecycle:
- Support champion/challenger model deployment with traffic splitting
- Track model performance metrics (precision, recall, AUC, false positive rate) continuously
- Trigger retraining when drift detected
Alerts and rules management:
- Allow operations team to create/modify/disable rules without code deployment
- Support A/B testing of rule changes
- Provide rule hit statistics and performance dashboard
Non-Functional Requirements
| Attribute | Target |
|---|---|
| Scoring latency | p50 < 50 ms, p99 < 150 ms |
| Throughput | 50,000 TPS sustained, 200,000 TPS burst |
| Availability | 99.99% (< 1 hr downtime/year) — cannot block all payments |
| False positive rate | < 0.3% for card-present, < 1% for card-not-present |
| Fraud catch rate | > 85% of confirmed fraud flagged before settlement |
| Label feedback | Confirmed labels ingested within 24 hours of resolution |
| Audit retention | 7 years per PCI-DSS / financial regulation |
| Model retraining | Minimum weekly; triggered automatically on drift |
Capacity Estimation
Baseline assumptions (mid-size fintech):
- 5 million transactions/day
- Peak: 5× baseline = 25 M transactions in a 4-hour peak window
- Average transaction payload: 2 KB
- Feature vector size: ~200 features × 4 bytes = 800 bytes
- Fraud rate: 0.3% → 15,000 fraudulent transactions/day
- Analyst team: 50 investigators
Throughput:
5,000,000 / 86,400 = ~58 TPS average
Peak: 5,000,000 * 5 / (4 * 3600) = ~1,736 TPS sustained peak
Design target: 10,000 TPS (3× headroom)
Feature store reads:
- Each scoring call reads ~50 feature keys from Redis
- 10,000 TPS × 50 reads = 500,000 Redis ops/second
- Redis cluster: 6 nodes × ~100K ops/s = 600K ops/s capacity — fits with headroom
Storage:
Event log: 5M events/day × 2 KB = 10 GB/day = 3.65 TB/year
Feature snapshots: 5M × 1 KB = 5 GB/day
Model scores: 5M × 200 bytes = 1 GB/day
Case records: 15,000/day × 5 KB = 75 MB/day
Total: ~15 GB/day hot storage, archive after 90 days
Model inference:
- Gradient boosting (XGBoost): ~1 ms per inference on CPU
- Neural net (MLP): ~3 ms on CPU, < 1 ms on GPU
- 10,000 TPS requires: 10,000 × 1 ms = 10 CPU-seconds/s → 10 CPU cores for inference alone
- Deploy 20 inference pods (2× headroom), each with 4 vCPUs
High-Level Architecture
flowchart TD
subgraph Ingestion
API[Transaction API Gateway]
KAFKA[Kafka: raw-events]
end
subgraph Scoring Pipeline
RULES_PRE[Pre-Score Rule Engine]
FEAT[Feature Computation Service]
FS[(Redis Feature Store)]
MODEL[ML Scoring Service]
RULES_POST[Post-Score Rule Engine]
DECISION[Decision Aggregator]
end
subgraph Persistence
PGWRITE[(PostgreSQL - write)]
S3[(S3 - event archive)]
CH[(ClickHouse - analytics)]
end
subgraph Case Management
QUEUE[Case Queue Service]
ANALYST[Analyst Workbench UI]
LABEL[Label Ingestion Service]
end
subgraph ML Platform
TRAIN[Training Pipeline]
REGISTRY[Model Registry]
DRIFT[Drift Monitor]
end
API --> KAFKA
API -->|sync scoring| RULES_PRE
RULES_PRE -->|not blocked| FEAT
FEAT --> FS
FEAT --> MODEL
MODEL --> RULES_POST
RULES_POST --> DECISION
DECISION -->|response| API
KAFKA --> PGWRITE
KAFKA --> S3
KAFKA --> CH
DECISION -->|REVIEW| QUEUE
QUEUE --> ANALYST
ANALYST --> LABEL
LABEL --> TRAIN
LABEL --> FS
TRAIN --> REGISTRY
REGISTRY --> MODEL
DRIFT --> TRAIN
CH --> DRIFT
Core Service Responsibilities
| Service | Responsibility |
|---|---|
| Transaction API Gateway | Receive events, route to sync scoring pipeline, enforce auth, return decision |
| Pre-Score Rule Engine | Fast deterministic checks (blocklist, velocity hard limits) — block immediately |
| Feature Computation | Pull real-time aggregates from feature store, compute derived signals |
| ML Scoring Service | Load model from registry, run inference, return score + SHAP explanation |
| Post-Score Rule Engine | Apply score-threshold rules, business policy overrides, allow-list checks |
| Decision Aggregator | Combine rule outputs + model score → final action + audit record |
| Redis Feature Store | Sub-millisecond read/write of user/device/merchant velocity counters |
| Case Queue Service | Route REVIEW decisions to analyst queues by risk tier and domain |
| Analyst Workbench | UI for investigators — display features, transaction graph, decision tools |
| Label Ingestion | Accept confirmed fraud/not-fraud labels, publish to Kafka for training |
| Training Pipeline | Retrain models on labeled dataset, validate, register new version |
| Model Registry | Store versioned models + metadata, support champion/challenger traffic splits |
| Drift Monitor | Track feature distribution and model score distribution, trigger retraining |
| ClickHouse | OLAP store for rule performance, model metrics, investigator productivity |
Fraud Detection State Machine
stateDiagram-v2
[*] --> RECEIVED: transaction arrives
RECEIVED --> HARD_BLOCKED: pre-score rule match (blocklist / velocity limit exceeded)
RECEIVED --> SCORING: no hard block
SCORING --> APPROVED: score < low threshold AND no post-rule flag
SCORING --> REVIEW_QUEUED: score >= review threshold OR post-rule flag
SCORING --> AUTO_BLOCKED: score >= block threshold
HARD_BLOCKED --> [*]: event logged, response returned
AUTO_BLOCKED --> [*]: event logged, response returned
APPROVED --> [*]: event logged, response returned
REVIEW_QUEUED --> UNDER_INVESTIGATION: analyst picks up case
UNDER_INVESTIGATION --> CONFIRMED_FRAUD: analyst confirms fraud
UNDER_INVESTIGATION --> CLEARED: analyst clears as legitimate
UNDER_INVESTIGATION --> ESCALATED: analyst escalates to senior / compliance
ESCALATED --> CONFIRMED_FRAUD: escalation resolved as fraud
ESCALATED --> CLEARED: escalation resolved as legitimate
CONFIRMED_FRAUD --> LABEL_INGESTED: label published to training pipeline
CLEARED --> LABEL_INGESTED: label published to training pipeline
LABEL_INGESTED --> [*]
Step 1: Event Ingestion and Routing
Every evaluatable event enters through the Transaction API Gateway. The gateway does two things simultaneously:
Synchronous path (< 150 ms SLA):
- Validates the event payload (required fields, schema check)
- Calls the scoring pipeline inline
- Returns
{ score, action, case_id?, explanation }to the caller before completing the transaction
Asynchronous path (no latency impact):
- Publishes the full raw event to Kafka topic
raw-events - Downstream consumers write to PostgreSQL (durable record), S3 (long-term archive), and ClickHouse (analytics)
The synchronous path must not fail open if the scoring service is unavailable. Define a fallback policy per event type:
- Card payments: fail-open with a configurable "degraded mode" score (treat as medium risk, allow with flag)
- Wire transfers > $10,000: fail-closed (decline until scoring recovers)
- Login events: fail-open (allow login, flag for async review)
This policy is business-defined and stored in configuration, not hardcoded.
Step 2: Pre-Score Rule Engine
Before invoking the ML model (expensive), the Pre-Score Rule Engine performs fast O(1) deterministic checks:
Blocklist checks (Redis set lookups, ~0.1 ms):
- Is the card number in the global blocklist?
- Is the device fingerprint in the device blocklist?
- Is the IP in the known fraud IP range (CIDR match)?
- Is the beneficiary account in the mule account registry?
Hard velocity limits (Redis counter lookups, ~0.5 ms):
- More than 10 transactions from this card in the last 60 seconds?
- More than $50,000 total spend from this account in the last hour?
- More than 5 failed PIN attempts in 10 minutes?
Geographic impossibility check:
- Last transaction was in Singapore 3 minutes ago; this transaction is from Germany — impossible travel time given physics.
- Requires reading the last event timestamp and location from the feature store.
If any pre-score rule matches, the engine short-circuits — it returns HARD_BLOCK immediately without invoking the feature computation or ML model. This protects model serving capacity and keeps latency at < 5 ms for the obvious fraud cases.
Rules are stored in a database and evaluated by the engine in-memory after a cache warm-up on startup. Rule changes propagate within 30 seconds via a config reload mechanism.
Step 3: Feature Computation
For events not caught by pre-score rules, the Feature Computation Service builds the feature vector used by the ML model. Features fall into five categories:
Velocity Features (Redis, computed real-time)
| Feature | Window |
|---|---|
user_txn_count_1m |
1 minute |
user_txn_count_1h |
1 hour |
user_spend_1h |
1 hour |
user_spend_24h |
24 hours |
user_spend_7d |
7 days |
merchant_txn_count_1h |
1 hour |
user_failed_auth_count_10m |
10 minutes |
card_decline_count_24h |
24 hours |
Velocity counters use sliding window approximation in Redis: a sorted set per (entity, feature) where each member is a transaction timestamp. Count members in the window → velocity. Expire old members via a background job.
Device Signals (from device fingerprint service)
is_new_device— never seen this device for this userdevice_age_days— how long since device first registeredis_emulator— emulator/jailbreak detection flag from mobile SDKdevice_txn_count— total transactions ever from this devicedevice_user_count— how many distinct users have used this device (mule detection)
Behavioral Features (user profile store)
avg_transaction_amount_30d— normal spend baselinetransaction_hour_usual— is this transaction at an unusual hour for this user?merchant_category_match— is this merchant category normal for this user?recipient_is_known— has this user sent to this recipient before?
Network / Graph Features (computed async, cached)
recipient_fraud_score— fraud score of the receiving accountshared_device_risk— max fraud score among users sharing this deviceip_cluster_risk— historical fraud rate at this IP subnet
Transaction Context
transaction_amount,merchant_category_code,card_presentflag,country_mismatch,currency_mismatch,channel(mobile/web/POS)
The feature service fetches all these in parallel using Redis pipelining and a batch read from the user profile store. Total feature fetch time: 5–15 ms for warm cache.
Step 4: ML Scoring Service
The ML Scoring Service receives the feature vector and returns a normalized risk score (0–1000) plus a SHAP-based explanation.
Model Architecture
Primary model: Gradient Boosted Trees (XGBoost)
- Reasons: fast inference (~1 ms), handles tabular data well, produces feature importances natively
- Input: ~200 numeric features
- Output: probability of fraud (0–1), converted to score 0–1000
Secondary model: MLP (for behavioral sequence signals)
- Processes user's last N transaction sequences
- Captures temporal patterns XGBoost misses (e.g., account takeover velocity buildup)
Ensemble: weighted average of XGBoost score and MLP score. Weights are tunable per event type.
Champion/Challenger Deployment
The model registry stores multiple model versions. Traffic is split:
- Champion (current production model): 90% of traffic
- Challenger (candidate new model): 10% of traffic
Both models produce scores; only the champion's decision is used for the real-time response. The challenger's scores are logged for offline comparison. When the challenger's AUC on labeled outcomes exceeds the champion's by a statistically significant margin, it is promoted automatically (or with a human approval step for high-stakes event types).
SHAP Explanations
Every model inference produces a SHAP value vector — one value per feature — representing each feature's contribution to the final score. These are:
- Stored in the case record for analyst use
- Used to generate human-readable explanations ("High risk: 3 failed authentication attempts in the last 10 minutes, transaction is 4× user's average spend, new device never seen before")
- Provided to customers in adverse action notices (regulatory requirement)
SHAP computation adds ~3 ms to inference time. For the highest-throughput event types, SHAP is computed asynchronously and attached to the case record within 1 second.
Step 5: Post-Score Rule Engine
After ML scoring, the Post-Score Rule Engine applies policy overlays:
Score threshold rules (configurable):
score >= 800 → BLOCK
score >= 500 → REVIEW
score < 500 → APPROVE
Allow-list overrides:
- Trusted merchant + trusted device + score < 600 → downgrade REVIEW to APPROVE
- Recurring bill payment to known biller → downgrade REVIEW to APPROVE if amount within 10% of typical
Escalation rules:
- Wire transfer > $25,000 → force REVIEW regardless of score
- New beneficiary + international transfer → force REVIEW
- Account age < 30 days + any transaction > $1,000 → REVIEW
Rule performance tracking: Every rule records hit count, false positive rate (when analyst clears), and false negative rate (when confirmed fraud slips through). Rules with FPR > 5% are flagged for review by the ops team.
Step 6: Case Management
REVIEW decisions are routed to the Case Queue Service, which assigns cases to analyst queues based on:
- Risk tier: score 500–650 (low review), 650–799 (high review), manual escalation
- Domain specialization: card fraud queue, account takeover queue, wire fraud queue
- Analyst availability and workload balancing
Case Record Structure
Each case contains:
- The original transaction payload
- Full feature vector with SHAP values
- Timeline: all transactions in last 24h for this user
- Device history: all devices used by this account
- Account network graph: accounts linked via shared device/IP
- Previous cases on this user
- Analyst decision tools: confirm fraud, clear, request more info, escalate
Analyst Decision Workflow
sequenceDiagram
participant FDS as Fraud Detection
participant CQ as Case Queue
participant AW as Analyst Workbench
participant LI as Label Ingestion
participant TP as Training Pipeline
FDS->>CQ: route REVIEW case
CQ->>AW: assign to analyst
AW->>AW: investigate (transaction history, device, graph)
AW->>CQ: record decision (FRAUD / CLEAR / ESCALATE)
CQ->>LI: publish confirmed label event
LI->>TP: label available for training
LI->>FDS: update feature store (confirmed fraud flags)
SLA for Case Resolution
| Queue | Target Resolution Time |
|---|---|
| Low risk | 24 hours |
| High risk | 4 hours |
| Wire fraud | 1 hour |
| Account takeover | 30 minutes |
Cases approaching SLA are auto-escalated to senior analysts.
Step 7: Feedback Loop and Model Training
The feedback loop is the most critical — and most neglected — part of a fraud detection system. Without quality labels flowing back to the training pipeline, the model degrades over time as fraud patterns shift.
Label Sources
| Source | Latency | Quality |
|---|---|---|
| Analyst decision | Hours–days | High |
| Chargeback (card network) | 30–90 days | High |
| Customer dispute | 7–30 days | Medium |
| Automated rule match | Real-time | Low |
Because chargeback labels arrive with significant delay, the training pipeline uses a delayed labeling strategy:
- Training data includes only transactions with labels older than 30 days (to ensure near-complete labeling)
- Near-real-time retraining uses analyst decisions + automated rule matches as proxy labels
- Monthly full retrain uses complete chargeback-confirmed labels
Training Pipeline
sequenceDiagram
participant LI as Label Store
participant FE as Feature Engineering
participant TR as Trainer
participant VAL as Validator
participant REG as Model Registry
participant SCORE as Scoring Service
LI->>FE: pull labeled events (last 90 days)
FE->>FE: reconstruct feature vectors at event time
FE->>TR: training dataset (features + labels)
TR->>TR: train XGBoost + MLP
TR->>VAL: candidate models
VAL->>VAL: compute AUC, precision@K, recall@K
VAL->>VAL: compare challenger vs champion
VAL->>REG: register if challenger wins
REG->>SCORE: deploy as new challenger (10% traffic)
SCORE->>REG: report live metrics after 48h
REG->>SCORE: promote to champion if metrics hold
Point-in-time correctness: Feature vectors for training must be reconstructed as they were at the time of the transaction — not using today's feature values. This prevents label leakage where future information contaminates the training signal. The feature store must store historical snapshots of feature values for every transaction.
Step 8: Device Fingerprinting
Device fingerprinting generates a stable, unique identifier for a client device without requiring user login. It is one of the strongest fraud signals — a confirmed fraud device contaminates all future transactions from it.
Browser Fingerprinting Signals
- User agent, screen resolution, color depth, timezone
- Canvas fingerprint (hardware rendering signature)
- WebGL renderer and vendor string
- Installed fonts list
- AudioContext fingerprint
- JavaScript engine timing signals
Mobile SDK Signals
- Hardware identifiers (IMEI, advertising ID — where permitted)
- Jailbreak / root detection
- Emulator detection (virtualized environment markers)
- App install timestamp
- Sensor fingerprint (accelerometer/gyroscope calibration signatures)
Fingerprint Stability
Browser fingerprints change when users update their browser or clear data. The fingerprinting service uses a fuzzy matching algorithm — if 80%+ of signals match a known fingerprint, it assigns the same device ID and updates the stored profile. This handles the natural drift of legitimate devices while still detecting fresh installations by fraudsters.
Device Graph
The device graph links:
- Device → User (a device used by multiple users is a risk signal)
- Device → IP addresses historically used
- Device → Transaction outcomes
A device used by 10 different users with high fraud rates is a "mule device" — any new user transacting from it inherits a risk penalty.
Database Schema
PostgreSQL: Transaction Events and Cases
-- Core transaction event record
CREATE TABLE fraud_events (
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
external_id VARCHAR(128) NOT NULL UNIQUE, -- caller's transaction ID
event_type VARCHAR(50) NOT NULL, -- PAYMENT, LOGIN, ACH, WIRE
user_id UUID NOT NULL,
account_id UUID NOT NULL,
device_id UUID,
amount_cents BIGINT,
currency CHAR(3),
merchant_id VARCHAR(128),
channel VARCHAR(20),
ip_address INET,
event_payload JSONB NOT NULL,
score SMALLINT, -- 0-1000
action VARCHAR(10) NOT NULL, -- APPROVE/REVIEW/BLOCK
model_version VARCHAR(50),
features_snapshot JSONB, -- feature vector at decision time
shap_values JSONB, -- SHAP per feature
decision_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
decided_at TIMESTAMPTZ
);
CREATE INDEX idx_fraud_events_user_id ON fraud_events (user_id, created_at DESC);
CREATE INDEX idx_fraud_events_account_id ON fraud_events (account_id, created_at DESC);
CREATE INDEX idx_fraud_events_device_id ON fraud_events (device_id, created_at DESC);
CREATE INDEX idx_fraud_events_action ON fraud_events (action, created_at DESC);
CREATE INDEX idx_fraud_events_external_id ON fraud_events (external_id);
-- Fraud cases for analyst workflow
CREATE TABLE fraud_cases (
case_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES fraud_events(event_id),
user_id UUID NOT NULL,
queue_name VARCHAR(50) NOT NULL,
risk_tier VARCHAR(20) NOT NULL, -- LOW, HIGH, ESCALATED
assigned_to UUID, -- analyst user_id
status VARCHAR(20) NOT NULL DEFAULT 'OPEN',
analyst_decision VARCHAR(20), -- FRAUD, CLEAR, ESCALATE
analyst_notes TEXT,
opened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
assigned_at TIMESTAMPTZ,
resolved_at TIMESTAMPTZ,
sla_deadline TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_cases_queue_status ON fraud_cases (queue_name, status, sla_deadline);
CREATE INDEX idx_cases_assigned_to ON fraud_cases (assigned_to, status);
CREATE INDEX idx_cases_user_id ON fraud_cases (user_id, opened_at DESC);
-- Confirmed fraud labels
CREATE TABLE fraud_labels (
label_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES fraud_events(event_id),
user_id UUID NOT NULL,
label VARCHAR(20) NOT NULL, -- FRAUD, LEGITIMATE
label_source VARCHAR(30) NOT NULL, -- ANALYST, CHARGEBACK, DISPUTE, AUTO_RULE
label_confidence NUMERIC(4,3), -- 0.0 – 1.0
labeled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
labeler_id UUID -- analyst user_id or system
);
CREATE INDEX idx_labels_event_id ON fraud_labels (event_id);
CREATE INDEX idx_labels_labeled_at ON fraud_labels (labeled_at DESC);
CREATE INDEX idx_labels_label ON fraud_labels (label, labeled_at DESC);
-- Rule definitions
CREATE TABLE fraud_rules (
rule_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rule_name VARCHAR(100) NOT NULL UNIQUE,
rule_type VARCHAR(20) NOT NULL, -- PRE_SCORE, POST_SCORE
event_types TEXT[] NOT NULL,
expression TEXT NOT NULL, -- DSL expression evaluated by rule engine
action VARCHAR(10) NOT NULL, -- BLOCK, REVIEW, APPROVE
priority INT NOT NULL DEFAULT 100,
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID NOT NULL
);
-- Device registry
CREATE TABLE devices (
device_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
fingerprint_hash VARCHAR(64) NOT NULL UNIQUE,
device_type VARCHAR(20) NOT NULL, -- BROWSER, IOS, ANDROID
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen_at TIMESTAMPTZ,
risk_score SMALLINT NOT NULL DEFAULT 0,
is_blocklisted BOOLEAN NOT NULL DEFAULT FALSE,
signals JSONB NOT NULL
);
CREATE INDEX idx_devices_hash ON devices (fingerprint_hash);
CREATE INDEX idx_devices_blocklisted ON devices (is_blocklisted) WHERE is_blocklisted = TRUE;
API Design
POST /v1/fraud/evaluate
Synchronous scoring endpoint called by the transaction processor.
Request:
{
"external_id": "txn_8f3a9b2c",
"event_type": "PAYMENT",
"user_id": "usr_7d4e8a1f",
"account_id": "acc_3c9b2e5a",
"device_fingerprint": "fp_a1b2c3d4e5f6",
"ip_address": "203.0.113.45",
"amount_cents": 149900,
"currency": "USD",
"merchant_id": "mch_amazon",
"merchant_category_code": "5999",
"channel": "WEB",
"card_present": false,
"metadata": {
"browser_ua": "Mozilla/5.0 ...",
"session_id": "sess_xyz",
"referrer": "direct"
}
}
Response (APPROVE):
{
"event_id": "evt_c2d3e4f5a6b7",
"external_id": "txn_8f3a9b2c",
"score": 210,
"action": "APPROVE",
"model_version": "xgb_v42",
"latency_ms": 38,
"top_risk_factors": [
{ "feature": "is_new_device", "contribution": 45, "value": true },
{ "feature": "user_spend_1h", "contribution": 12, "value": 420.00 }
],
"decided_at": "2026-07-14T09:23:41.210Z"
}
Response (REVIEW):
{
"event_id": "evt_d3e4f5a6b7c8",
"external_id": "txn_9g4b0c3d",
"score": 620,
"action": "REVIEW",
"case_id": "case_e4f5a6b7c8d9",
"model_version": "xgb_v42",
"latency_ms": 52,
"top_risk_factors": [
{ "feature": "user_spend_1h", "contribution": 180, "value": 4500.00 },
{ "feature": "is_new_device", "contribution": 120, "value": true },
{ "feature": "transaction_amount_vs_avg", "contribution": 95, "value": 8.3 }
],
"human_readable_reason": "Transaction is 8× user's average spend; sent from a device not previously seen on this account.",
"decided_at": "2026-07-14T09:23:42.340Z"
}
Response (BLOCK):
{
"event_id": "evt_e5f6a7b8c9d0",
"external_id": "txn_0h5c1d4e",
"score": 920,
"action": "BLOCK",
"block_reason": "RULE_MATCH",
"rule_matched": "device_blocklist",
"model_version": "xgb_v42",
"latency_ms": 4,
"decided_at": "2026-07-14T09:23:43.110Z"
}
POST /v1/fraud/label
Submit a confirmed label for a past event (called by chargeback ingestion, analyst decisions).
{
"event_id": "evt_c2d3e4f5a6b7",
"label": "FRAUD",
"label_source": "CHARGEBACK",
"label_confidence": 1.0,
"labeled_at": "2026-07-14T09:23:41.210Z"
}
GET /v1/fraud/cases?queue=wire_fraud&status=OPEN&limit=20
Returns analyst workbench cases with full context.
PATCH /v1/fraud/cases/{case_id}/decision
{
"decision": "CONFIRMED_FRAUD",
"notes": "User confirmed card stolen 3 days prior. Transaction pattern matches known fraud ring targeting electronics merchants."
}
Kafka Event Architecture
| Topic | Producer | Consumers | Retention |
|---|---|---|---|
fraud.events.raw |
API Gateway | PostgreSQL writer, S3 archiver, CH | 7 days |
fraud.decisions |
Decision Aggregator | Case Queue, Notification Service | 30 days |
fraud.labels |
Label Ingestion | Training Pipeline, Feature Store | 90 days |
fraud.model.deployed |
Model Registry | Scoring Service, Monitoring | 30 days |
fraud.alerts.rules |
Rule Engine | Ops Dashboard, ClickHouse | 30 days |
fraud.device.blocklisted |
Case Management | Pre-Score Rule Engine, Feature Store | 90 days |
Observability
Key Metrics
| Metric | Alert Threshold |
|---|---|
fraud.scoring.latency_p99_ms |
> 150 ms |
fraud.scoring.error_rate |
> 0.1% |
fraud.decision.block_rate |
> 5% (sudden spike) |
fraud.decision.false_positive_rate |
> 1% (rolling 1h) |
fraud.model.auc |
< 0.90 (weekly eval) |
fraud.feature_store.cache_miss_rate |
> 5% |
fraud.cases.sla_breach_rate |
> 2% |
fraud.labels.ingestion_lag_hrs |
> 48 hours |
fraud.model.score_distribution_drift |
PSI > 0.2 |
Distributed Tracing
Every scoring request carries a trace ID through:
API Gateway → Pre-Score Rule Engine → Feature Computation → ML Scoring → Post-Score Rule Engine → Decision Aggregator
The trace records latency at each hop, which features were fetched, which rules were evaluated, and the model score. This is essential for debugging unexpected decisions and for SLA compliance audits.
Design Trade-offs
1. Rules-First vs Model-First
Option A (Rules-first): Run the rule engine before the model. Fast blocks on obvious cases save model compute.
Option B (Model-first): Run the model on everything, then apply rules as a policy layer.
Decision: Rules-first pre-score, model-first for everything else. Hard blocklist and velocity hard limits are O(1) Redis lookups. Running the model on a card that is on the global blocklist wastes compute and adds latency. But all nuanced decisions go through the model — rules-only systems are too brittle for novel fraud patterns.
2. Synchronous vs Asynchronous Scoring
Option A (Sync): Block the transaction response until the score is ready. Full accuracy.
Option B (Async): Return provisional approval immediately, reverse the transaction asynchronously if fraud is detected.
Decision: Sync for all transaction types. Asynchronous scoring has a reversal window problem — the fraudster has already received value (gift card codes, wire funds) before the reversal fires. Sync scoring at < 150 ms is achievable with the right architecture. Async scoring is only acceptable for very low-value, low-risk event types (e.g., login events where the only action is flagging).
3. Single Model vs Ensemble
Option A (Single model): One XGBoost model trained on all event types.
Option B (Ensemble): Separate specialized models per event type (card fraud model, ATO model, wire fraud model) plus a meta-ensemble.
Decision: Specialized models per event type with a common infrastructure. Card fraud patterns (velocity, merchant) are very different from account takeover patterns (login behavior, device changes). Training on mixed data causes one signal to dominate. The infrastructure (feature store, model registry, serving layer) is shared, but each event type has its own trained model.
4. Feature Freshness vs Storage Cost
Option A (Real-time features only): Compute all features from Redis at scoring time. Ultra-fresh, but Redis is expensive for large history windows.
Option B (Pre-computed batch features + real-time deltas): Nightly batch job computes 30-day and 90-day aggregates into a fast key-value store. Real-time features cover the last 24 hours. The model uses both.
Decision: Hybrid — real-time features for windows < 24 hours, pre-computed features for longer windows. Maintaining 90 days of per-user sorted sets in Redis is prohibitively expensive at scale. Pre-compute longer windows with Spark overnight. This introduces up to 24 hours of staleness in long-window features — acceptable because those features are trend signals, not real-time indicators.
Common Interview Mistakes
-
Only designing the happy path. A fraud detection system that doesn't discuss what happens when the scoring service is unavailable is incomplete. Failover policies (fail-open vs fail-closed) are a core design decision.
-
Treating fraud detection as purely a model problem. The model is roughly 40% of the system. Case management, feedback loops, rule engines, and the feature store are equally important and get neglected in interviews.
-
Ignoring label latency. Saying "we retrain the model on fraud labels" without addressing that chargeback labels arrive 30–90 days late is a significant omission. Delayed labeling strategies must be discussed.
-
Designing for precision only. Blocking all transactions above a certain score maximizes precision but destroys recall and creates massive false positive rates. The precision/recall tradeoff and its business implications must be addressed explicitly.
-
Forgetting point-in-time correctness in training. Using today's feature values to train a model on historical transactions leaks future information into the training set. Always reconstruct features as they were at transaction time.
-
Proposing a static rule engine. Rules that cannot be updated without a code deployment are a liability. The operations team needs to add, modify, and disable rules within minutes of discovering a new fraud pattern. A DSL-based rule engine with a management UI is required.
-
Overlooking explainability. "The model said no" is not a compliant response in a regulated financial context. SHAP values and human-readable decision reasons are not a nice-to-have — they are a regulatory requirement for adverse action notices.
-
Underestimating the feature store. Students often propose Postgres for feature storage. At 500K ops/second, you need Redis or a purpose-built feature store. The feature store design (data model, TTL, sliding window approximation) deserves its own detailed discussion.
Summary
flowchart LR
A[Transaction Arrives] --> B{Pre-Score Rules}
B -->|Hard block| Z1[BLOCK — instant]
B -->|Pass| C[Feature Computation]
C --> D[ML Model Inference]
D --> E{Post-Score Rules}
E -->|score < 500| Z2[APPROVE]
E -->|500-799| F[Case Queue]
E -->|800+| Z3[BLOCK — score]
F --> G[Analyst Investigation]
G --> H[Confirmed Label]
H --> I[Training Pipeline]
I --> J[New Model Version]
J --> D
Design Principles:
- Latency is a feature — a scoring system that misses its 150 ms SLA is not production-ready, regardless of its accuracy
- Rules protect the model — fast deterministic rules guard model compute capacity and handle obvious cases cheaply
- Labels are the fuel — a fraud system without a quality feedback loop will drift into irrelevance within months
- Explainability is mandatory — every block decision must produce a human-readable rationale for regulatory compliance
- False positives cost money — declining a good customer has a measurable cost; optimize for the business-defined precision/recall operating point, not raw accuracy
- Adversarial systems degrade — fraudsters adapt; continuous monitoring, drift detection, and retraining are not optional