Detecting Account Sharing and Credential Leaks

Designing System for Detecting Account Sharing and Credential Leaks

Detecting account sharing and credential leaks before they turn into a breach

A ground-up, beginner-friendly system design walkthrough of how large platforms build the pipelines, models, graph databases, and streaming infrastructure that spot one account being used from two continents at once — and decide, in milliseconds, what to do about it without punishing every real traveler, VPN user, or family plan on the platform.

01

Introduction & History

Imagine a streaming service with 50 million subscribers. One night, its fraud team notices something strange in the logs: the account belonging to a user in Mumbai just started a video session in São Paulo, four minutes after finishing an episode in Mumbai. No flight exists that makes that possible. Either the user has a very unusual life, or somebody else is watching on their account.

This is the exact problem this guide is about: building a system that can look at a stream of login and usage events and decide, quickly and cheaply, whether an account is being used by more people than it should be — because it was shared with friends, because a password leaked in a data breach, or because a bot farm is testing stolen credentials. We will call this system an Account Anomaly Detection System, or AADS, throughout this walkthrough.

To understand why the system looks the way it does today, it helps to know how the problem itself evolved into the specific shape it now has.

💡
Real-life analogy

Think of a single gym membership card. If the front desk scans the card in the Delhi branch at 6:00 PM and then sees it scanned again in the Chennai branch at 6:10 PM, they don’t need to see a face to know something is wrong — no person can travel that distance in ten minutes. The gym doesn’t need to know who did it to know that the card is being used impossibly. That’s the entire idea behind this system, applied to logins instead of gym cards.

Where this problem came from

In the early days of the web, most services didn’t worry about this at all. A login was a login. If your password worked, you were you. Sharing a password with a family member for a shared email inbox was normal and mostly harmless.

Two things changed that.

  • Subscription businesses. When companies started charging per account instead of per copy — streaming video, streaming music, SaaS seats, cloud storage — every extra person using one account for free became a direct hit to revenue. A password shared between five friends could mean four lost subscriptions, quietly and continuously.
  • Mass credential breaches. Starting in the mid-2010s, billions of username-and-password pairs leaked from unrelated services (forums, retailers, old social networks). Because people reuse passwords, attackers began credential stuffing: taking a breached list and trying those same username-password pairs on banks, e-commerce sites, and streaming platforms automatically, at massive scale. A working pair meant a compromised account, often used quietly for months before the real owner noticed.

Both problems produce the same observable symptom: an account behaving as if it has more than one physical person behind it, often in places that are impossible to reach from each other in the available time. That’s why the two problems — friendly sharing and malicious takeover — are usually solved by the same underlying detection system, even though the response to each is very different.

How detection approaches evolved over time

EraTypical approachLimitation that pushed the next generation
Pre-2010Manual review after user complaints or chargebacksReactive, slow, caught almost nothing at scale
2010–2015Simple hard-coded rules: “block if IP country changes twice in 24 hours”High false positives for real travelers and VPN users; easy for attackers to learn and evade
2015–2019Concurrent session limits + device fingerprintingBetter, but static thresholds still didn’t separate sharing from travel from attack
2019–presentReal-time streaming pipelines + machine-learning risk scoring + graph analysis of device/account relationshipsState of the art, and the subject of the rest of this guide

Today, this kind of system sits quietly behind almost every account you have that costs money or holds something valuable: banking apps, streaming services, cloud email, gaming platforms, and enterprise SaaS tools. It runs continuously, scores every login and every meaningful action, and only occasionally interrupts a real human being. Most of the time it is invisible by design; you notice it only when it stops you and asks you to verify who you are.

The rest of this guide builds that system piece by piece: first understanding precisely what problem we’re solving and why it resists simple solutions, then walking through the full architecture component by component, then going deep on the algorithms, data flow, scaling, reliability, security, and operational practices that turn a clever idea into something that can actually run in production, at scale, for millions of real accounts, without falling over or quietly losing accuracy over time.

💬
What an interviewer may ask

“Why is this problem harder than it sounds — can’t you just block on impossible travel?” A good answer sketches the two sources of the same symptom (consensual sharing vs. non-consensual takeover), notes that the correct response to each is different, and points out that any single strong signal (country change, session count) is exactly what an adaptive attacker will learn to evade first.

02

Problem & Motivation

Before drawing a single box on an architecture diagram, we need to be precise about what we’re actually detecting, because “account sharing” and “credential leaking” are two different problems that look similar from the outside but need very different responses on the inside.

consensual

Account sharing

A legitimate subscriber deliberately gives their password to friends or family who are not on the plan. The original owner knows and consents. The business impact is lost revenue, not user harm.

non-consensual

Credential leak / account takeover

An attacker obtained the password (via a breach, phishing, or malware) without the owner’s knowledge and is using the account without consent. The business impact is fraud, data theft, and real harm to the victim.

The system we design must first catch both using the same low-level signals, because both produce impossible travel and unusual concurrency, and then separate the two cases so the response is proportionate: a gentle nudge (“looks like you’re sharing, want to add a member?”) for sharing, and an urgent lockdown (force password reset, kill all sessions, alert the user through a trusted channel) for takeover. Getting this separation right, rather than treating every anomaly identically, is arguably the single most important design decision covered in this entire guide.

It also helps to be clear about who the stakeholders are for this system, since they don’t all want the same thing. The subscriber wants uninterrupted access and no unfair blame. The business wants revenue protected and fraud losses minimized. The trust and safety team wants explainable, actionable signals rather than noisy alerts. The engineering team wants a system that’s operable, debuggable, and doesn’t page anyone unnecessarily at 3 AM. A good design keeps all four of these audiences in mind rather than optimizing for just one, which is exactly why the graduated-response approach that shows up throughout this guide, rather than a single blunt allow/deny rule, keeps turning out to be the right answer.

Why this is genuinely hard

reality

Real travel looks like an attack

A user who flies from Delhi to Singapore and opens the app at the airport, then again at the hotel, produces a location jump too. The system must not punish ordinary life.

reality

VPNs & mobile networks scramble location

A mobile carrier might route traffic through a gateway in a different city or state than the phone’s actual GPS position. A privacy-conscious user on a VPN might appear to be in Frankfurt while sitting in Bengaluru.

reality

Family plans are supposed to allow multiple locations

A legitimate household plan might have a parent’s phone in one city and a child at university in another, and that is not a violation.

adversarial

Attackers adapt

Once a rule like “block on country change” ships, credential-stuffing tools start routing through residential proxies in the victim’s own country to avoid triggering it.

latency

The decision has to be fast

Nobody will wait five seconds for a login to complete while a batch job scores their risk. The check has to happen inline, in well under a second, without slowing down the vast majority of logins that are completely normal.

asymmetric cost

Being wrong costs different things

A false positive (blocking a real user) causes support tickets, churn, and trust damage. A false negative (missing a real takeover) causes fraud losses and, in regulated industries, legal liability. Tuning the system means constantly balancing these two costs, not eliminating either one.

🎯
Beginner example

Priya logs into her food-delivery account from her phone in Pune at 9:00 AM. At 9:05 AM the same account logs in from a laptop in Warsaw, Poland. No commercial flight covers that distance in five minutes, so this pair of events is physically impossible for one person. That single fact — impossible travel time vs. straight-line distance — is the seed of almost every detection rule in this system.

🌐
Production example

Streaming platforms have discussed password-sharing detection that looks at signals like IP address, device identifiers, and account activity patterns to decide whether a “household” spans more devices and locations than a single home would, prompting the account owner to convert extra users into a paid add-on rather than immediately banning anyone. Banks and payment networks use closely related risk-engine techniques — impossible travel, device fingerprint mismatch, velocity of transactions — to flag account takeover in real time, but with a much stricter, security-first response because the downside of missing an attack is financial loss rather than lost subscription revenue.

What “good” looks like — four goals judged together

A well-designed AADS should be judged against four goals simultaneously, and a large part of the system design here is about the trade-offs between them rather than about hitting any single one perfectly.

GoalWhat it means in practice
AccuracyHigh recall on real takeovers and sharing, low false-positive rate on real users and legitimate travel
LatencyRisk decisions for login-time checks complete in tens of milliseconds; async signals feed a slower background score
ScaleHandles peak traffic (a viral show’s premiere night, a holiday shopping spike) without falling behind
ExplainabilityWhen a user is challenged or locked out, support staff and the user can be told a defensible reason
Common early mistake

Optimizing purely for accuracy metrics on offline test data without measuring what happens to the real user experience of challenged accounts. A model can look excellent on paper while quietly generating a support-cost avalanche if its false positives are concentrated on high-value, well-behaved customers.

💬
What an interviewer may ask

“How would you distinguish a user who genuinely travels a lot from an account takeover?” A strong answer talks about combining multiple weak signals (device fingerprint continuity, behavioral biometrics, historical travel pattern, IP reputation) rather than relying on any single rule like “country changed,” and about using risk scores instead of hard rules so evidence can accumulate over time rather than needing to be decisive in any one moment.

03

Core Concepts & Vocabulary

Before wiring up the architecture, it helps to have a shared vocabulary. Each concept below is one word or short phrase that will keep reappearing through the rest of this guide, so pinning them down once here means the rest of the tour reads faster and lands cleaner.

Impossible travel

A pair of login locations and timestamps that implies a speed of travel no real person could achieve. It is the single simplest, most reliable seed of fraud detection in this space — and the one an attacker will try to defeat first, by choosing proxy locations near the victim rather than obvious foreign ones.

Device fingerprint

A derived identifier built from a device’s browser, OS, hardware, and network characteristics, used to recognize a returning device without needing a stored cookie or explicit login. A good fingerprint combines many hard-to-fake signals server-side rather than trusting a single client-reported ID, so that spoofing it convincingly is genuinely difficult.

Credential stuffing

Automatically trying large lists of previously breached username/password pairs against a different service, hoping for password reuse. It is different from a targeted attack on one specific account; the attacker doesn’t care which account works, only that some account works, which is why fan-out patterns across many accounts often reveal it before any single-account view can.

Account takeover (ATO)

An attacker gaining unauthorized control of a legitimate user’s account. Credential stuffing is one path to ATO; phishing and malware are others. From the detection system’s point of view, the signals overlap even when the initial entry method differs.

Fast path and deep path

The two-tier pattern used repeatedly in this design: a cheap, cache-only, synchronous check that runs inline with login (fast path), and a richer, asynchronous check that follows moments later using ML, graph traversal, and heavier features (deep path). Splitting the work this way is what lets the system be both fast for everyone and thorough where it matters.

Feature store

A specialized storage layer holding precomputed signals (features) ready for fast lookup by both the live system and by model training pipelines. Using the same feature-computation code path for training and serving is what prevents training-serving skew, defined a few entries below.

Device-to-account fan-out

How many distinct accounts a single device or IP has been used to access. When this number is unusually high (a single laptop has tried logging into 40 unrelated accounts in an hour) it is a strong signal of shared attack infrastructure, invisible from any single account’s point of view but obvious from a graph point of view.

Step-up authentication

Asking for extra proof of identity (like an OTP or security question) only when risk is elevated, rather than for every login. Risk-based step-up is what makes multi-factor authentication targeted rather than always-on, concentrating friction where it’s justified and removing it where it isn’t.

Shadow mode

Running a new model or rule in production, scoring real traffic, without letting its output affect any real decision, purely to observe its behavior safely. It is how new detection logic is validated on live traffic patterns before being trusted with real enforcement.

Training-serving skew

A mismatch between how a feature is computed during model training versus during live production serving, which can silently degrade model accuracy in production even though offline evaluation looks fine. The standard mitigation is to share the exact same feature-engineering code path for both.

Risk score

A single numeric summary (typically 0–100) combining many individual signals into one comparable value. Scores are more useful than binary rules because they let evidence accumulate over time and let policy thresholds be tuned without rewriting detection logic.

Graceful degradation

The design principle that when a dependency fails, the system falls back to a simpler, less accurate mode rather than blocking entirely. For an authentication-adjacent system, this is especially critical: a fraud-scoring outage must never become a login outage.

Bulkhead isolation

Isolating resource pools (thread pools, connection pools, service instances) so a slow or failing dependency in one area can’t exhaust resources needed for another. It is the “watertight compartments on a ship” pattern, applied to microservices.

Saga

A pattern for coordinating multi-step operations (kill sessions → force reset → notify user → open case) where each step is tracked so a partial failure can be retried or compensated rather than leaving the account in an inconsistent state.

CQRS

Command Query Responsibility Segregation — separating the write path (raw login/activity events flowing into an event log) from the read path (fast-lookup precomputed state powering the fast-path score). Different shapes for writing versus reading is what lets each path be optimized for its own workload.

💬
What an interviewer may ask

“Which of these concepts would you introduce first, and which last, if you were building this system from scratch?” Look for a pragmatic ordering that starts with impossible travel + device fingerprint (the cheapest, highest-value pair), then adds concurrent-session limits and rate limiting, then invests in a streaming pipeline and feature store, and only then reaches for the graph database and ML model — not the reverse.

04

Architecture & Components

Now let’s design the system itself. Every box in the diagram below is a real, independently deployable component, and every box is labeled with what it actually is (API Gateway, Load Balancer, and so on) so you can see exactly where each piece sits in the request path rather than treating the diagram as an abstract sketch.

CLIENT LAYER Web Browser Client App login, activity, sensitive actions Mobile App Client device fingerprint SDK on-device Smart TV / Set-top / IoT Client stable device ID, rare mobility EDGE LAYER GeoDNS Routing nearest healthy region first (weak) location signal CDN Edge Node TLS termination close to user shields origin, static asset cache WAF / Bot Mitigation signature filtering, IP reputation first cheap fraud line of defense Global Load Balancer health-checked, least-connections L4/L7 traffic distribution API Gateway authN, per-IP rate limit correlation ID, routing CORE IDENTITY SERVICES Auth Service verify credentials + fast score Session Service Redis, active sessions per acct User Profile plan, home region, household size DECISION & RESPONSE LAYER Decision Engine rules + policy tuning Action Service enforce, kill sessions Notification email / SMS / push Case Mgmt T&S dashboard REAL-TIME DETECTION PIPELINE Event Producer Kafka SDK, async emit Event Bus Kafka / Kinesis topic Stream Processor Flink / Kafka Streams job Geo-Velocity Engine stateful operator, haversine Device Fingerprint FP microservice, graph lookup Feature Store Redis hot + offline DB Risk Scoring Service ML inference (GBDT / DL), 0-100 DATA & STORAGE LAYER Account DB Postgres sharded primary + replicas Event Store Cassandra / DynamoDB wide-column, time-range Device Graph Neo4j / Neptune accounts <-> devices <-> IPs Rate-Limit Redis cluster per-IP + per-account Cold Storage S3 archive compliance retention OBSERVABILITY Metrics & Tracing Prometheus OpenTelemetry spans Log Aggregator ELK / Loki structured JSON, correlation IDs
Figure 1 — End-to-end architecture. Every box names both its role and its concrete component type, so you can see exactly where each piece sits in the request path.

Let’s walk through every layer and explain, in plain terms, what each box does and why it exists.

1. Client layer

This is simply whatever the user is holding: a browser, a phone app, or a TV app. Every client sends login and activity events the same way, through the same edge path, so the backend never has to special-case a device type. That uniformity is itself a design win, because it means every new signal added to the pipeline is available for every kind of client automatically.

2. Edge layer

location signal

DNS / GeoDNS

Routes the user to the nearest healthy regional deployment. This is also our first, weakest location signal — it tells us roughly which region a request entered from, before any application code runs.

edge cache

CDN Edge Node

Caches static assets and terminates TLS close to the user, reducing latency and shielding origin servers from raw internet traffic.

first line

WAF / Bot Mitigation

Filters known-bad IPs, malformed requests, and automated bot signatures before they ever reach application servers — a first, cheap line of defense against credential-stuffing scripts.

traffic distribution

Load Balancer

Distributes incoming traffic across many API Gateway instances using health checks, so no single server is overwhelmed and a failed instance is automatically taken out of rotation.

single front door

API Gateway

The single front door for all backend calls. It authenticates the request token, applies per-account rate limits, and routes the call to the correct microservice. It’s also where we attach a coarse-grained IP-based risk check before deeper logic runs.

3. Core identity services

The Auth Service verifies credentials and issues session tokens. The Session Service, backed by a fast Redis cache, tracks every currently active session per account — this is what lets us know, at any instant, how many concurrent sessions an account has and where they claim to be. The User Profile Service holds account metadata like plan type, home region, and household size, which the Decision Engine uses later to interpret concurrent-session numbers in context.

4. Real-time detection pipeline

This is the heart of the system. Every login and every meaningful action (starting a video, opening a document, initiating a payment) is published as an event onto a message queue (Kafka or Kinesis) by an Event Producer. A Stream Processor consumes this event stream continuously and runs two parallel enrichment paths:

  • The Geo-Velocity Engine compares the new event’s location against the account’s last known location and computes whether the implied travel speed is physically possible.
  • The Device Fingerprint Service checks whether this device (browser/OS/hardware signature) has ever been seen on this account before, and cross-references it against a Device Graph Database to see if this same device fingerprint is linked to many other unrelated accounts — a strong signal of credential-stuffing infrastructure.

Both enrichment paths write into a shared Feature Store, which the Risk Scoring Service (a machine-learning inference microservice) reads to compute a single risk score for the event.

5. Decision & response layer

The Decision Engine takes the ML risk score plus business rules (e.g., “never fully lock a banking account without human review”) and decides an action: allow, soft-challenge (send an OTP), hard-challenge (force password reset), or block and alert. The Action Service enforces that decision, the Notification Service tells the user, and anything ambiguous is routed to a Case Management dashboard for a human analyst.

6. Data & storage layer

A relational Account DB holds durable account records. A wide-column Event Store holds the high-volume stream of login/activity events, optimized for time-range queries. A Graph Database models the relationships between accounts, devices, and IP addresses — this is what lets us answer questions like “how many distinct accounts has this device logged into this week?” A Redis-based rate-limit cache enforces per-account and per-IP throttling. Cold, older data ages out to cheap object storage for compliance retention.

7. Observability

Every hop emits metrics and traces so the team can see pipeline lag, model latency, and false-positive rates in real time. Chapter 11 goes deep on this, but note the placement: observability sits alongside every other layer rather than being bolted on at the end, because a detection system that can’t be observed can’t be trusted.

Build versus buy — an honest comparison

Not every team needs to build every one of these components in-house, and a real design discussion should weigh that explicitly rather than assuming everything is custom-built.

ComponentCommon buy optionWhen building in-house makes sense
WAF / Bot MitigationManaged cloud WAF or a dedicated bot-management vendorRarely worth building from scratch; vendors continuously update signatures against evolving bot techniques faster than most internal teams can match
Device fingerprintingSpecialized fraud-prevention vendors offering fingerprinting SDKsBuilding in-house makes sense once fingerprint data needs to feed directly into a proprietary graph/ML pipeline with full data ownership, or when vendor cost at scale exceeds build cost
Event bus (Kafka/Kinesis)Managed streaming service from a cloud providerSelf-hosting only tends to pay off at very large scale with dedicated platform engineering capacity; most teams should start managed
Risk scoring modelThird-party fraud-scoring API as a starting pointIn-house modeling becomes valuable once the business has enough labeled data and the product’s specific fraud patterns diverge meaningfully from what a generic vendor model targets
Decision engine / policy rulesRarely bought as a black box, since policy needs to reflect this specific business’s risk toleranceAlmost always worth owning, since it encodes business-specific judgment calls that a vendor cannot make on your behalf

A pragmatic path many teams follow is to start with vendor solutions for the commoditized edges (WAF, basic fingerprinting) while building the core decisioning logic and data model in-house from day one, since that core is where the business’s actual competitive understanding of its own fraud patterns lives.

Alternative architectural shapes considered

It’s worth briefly naming a couple of alternative designs and why this guide didn’t choose them, since interviewers often want to see that you’ve weighed real alternatives rather than presenting one design as the only possible answer.

Alternative: fully synchronous scoring

Run every check, including graph lookups and ML inference, inline before returning a login response. Rejected because it couples login latency and availability directly to the availability and speed of every fraud-detection dependency, which is an unacceptable risk to core authentication.

Alternative: purely batch, offline scoring

Score all logins in a nightly batch job rather than in real time. Rejected because it gives attackers up to 24 hours of unimpeded access before detection — unacceptable for anything involving financial or highly sensitive data, though it remains a reasonable choice for lower-stakes use cases with tight cost constraints.

The two-tier fast-path/deep-path hybrid used throughout this guide exists specifically because it captures most of the benefit of both alternatives (low latency like the synchronous approach for the checks that matter most, and thorough analysis like the batch approach for the checks that can tolerate delay) while avoiding the worst downside of each.

💬
What an interviewer may ask

“Why put the risk scoring service behind a stream processor instead of calling it directly from the login API?” A good answer: some signals (device-graph lookups, historical behavior aggregation) are too heavy to compute synchronously inside a login request without hurting latency, so cheap synchronous checks happen inline at the API Gateway / Auth Service, while richer asynchronous scoring flows through the stream and feeds back into future decisions, session risk flags, and case queues.

05

Internal Working

Architecture diagrams show what exists. This chapter explains how the pieces actually make a decision, step by step, using the core algorithm at the center of the system: impossible-travel and velocity scoring, then layered with everything the deep path adds on top.

FAST PATH — synchronous, budget under ~25 ms P95 Login request arrives (Auth Service) password verified, features gathered from cache only Implied Travel Speed haversine(prev, now) / dt > 900 km/h = impossible Device Fingerprint Match has this account seen this FP? new device = +15 points Concurrent Session Count Redis lookup, plan limit compare over limit = +20 points IP Reputation (cached) data-center / known-bad / clean flagged range = +10-25 points Combine into 0-100 fast-path score LOW < 35  |  MED 35-69  |  HIGH 70-89  |  CRIT ≥ 90 total budget: single-digit to low double-digit ms Return decision to user — allow / soft challenge / hard challenge this must not block on any DB, ML, or graph call event is queued for the deep path regardless of outcome DEEP PATH — asynchronous, hundreds of ms to a few seconds LoginEvent consumed from Kafka partition Flink / Kafka Streams stateful operator Device Graph Traversal how many accts share this FP? high fan-out = strong stuffing signal Behavioral Biometrics typing cadence, scroll, nav pattern compare against 30-90d history Time-of-day Deviation outside account's active window? learned per account, not global Historical Aggregations 7 / 30 / 90-day windows from offline feature store ML risk model (GBDT / DNN) refines the score richer features + learned interactions between them outputs new score + top contributing features (explainable) Revise decision if warranted — kill sessions, force reset, notify action arrives moments after login, still catches the attacker outcome loops back into training data for next model version event handed off
Figure 3 — The two-tier signal funnel. Cheap, cache-only signals answer the user in milliseconds; richer signals (graph fan-out, behavioral biometrics, learned interactions) refine the decision asynchronously without blocking anyone.

The core idea — velocity between two points

Every account keeps a rolling record of its last known “location fix”: latitude, longitude, and timestamp, derived from IP geolocation (and GPS if the client shares it). When a new login or action event arrives, we compute the great-circle distance between the previous fix and the new one, divide by the time elapsed, and get an implied speed.

pythonspeed_kmh = haversine_distance(prev_location, new_location) / hours_elapsed

if speed_kmh > MAX_PLAUSIBLE_SPEED:      # e.g. 900 km/h, generous for commercial flight
    flag = "IMPOSSIBLE_TRAVEL"
elif speed_kmh > SUSPICIOUS_SPEED:        # e.g. 200 km/h, unusual but not impossible
    flag = "SUSPICIOUS_VELOCITY"
else:
    flag = "NORMAL"
💡
Real-life analogy

This is exactly how a toll-booth system flags a stolen number plate: if the same plate is scanned at two toll booths 500 km apart, 20 minutes apart, no ordinary vehicle could have made that trip, so the system flags it automatically, without a human ever comparing the two photos.

Why raw velocity alone isn’t enough

A pure velocity rule creates too many false positives (VPN switching, mobile carrier IP reassignment, satellite internet) and is trivially evaded by attackers who simply wait an hour between logins. Production systems therefore treat velocity as one feature among many, feeding a machine-learning model rather than a single hard-coded rule. Typical features include:

FeatureWhat it captures
Implied travel speedPhysical plausibility of the location change
Device fingerprint matchIs this a device the account has used before?
Concurrent session countHow many active sessions exist for this account right now?
IP reputationIs the source IP a known VPN, data-center, or previously flagged address?
Device-to-account fan-outHow many distinct accounts has this exact device logged into recently? (from the graph DB)
Behavioral biometricsTyping cadence, scroll pattern, navigation habits compared to this account’s history
Time-of-day deviationIs this wildly outside the account’s normal active hours?
Account value / plan typeUsed to weight response severity, not to change detection sensitivity

Two-tier scoring — fast path and deep path

Because different features are expensive to compute at different speeds, the system runs two tiers.

  1. Fast path (synchronous, inline with login): checks that are cheap and already cached — last known location from Redis, device fingerprint hash lookup, current concurrent session count. This must complete in single-digit to low double-digit milliseconds because it sits directly in the user’s login request.
  2. Deep path (asynchronous, via the stream): heavier checks — graph traversal across the device-account graph, behavioral model inference, aggregation over 30 / 60 / 90-day windows. This can take hundreds of milliseconds to a few seconds and doesn’t block the login; instead it updates the account’s ongoing risk score and can trigger a step-up challenge or session kill moments after the login succeeded.
💻
Software example

A simplified fast-path scoring function that runs inline at login time, combining a few cheap signals into a 0–100 risk score:

javapublic class FastPathRiskScorer {

    public RiskResult score(LoginEvent event, AccountState lastKnown) {
        double speedKmh = GeoUtils.impliedSpeed(
            lastKnown.getLocation(), event.getLocation(),
            lastKnown.getTimestamp(), event.getTimestamp());

        int score = 0;
        List<String> reasons = new ArrayList<>();

        if (speedKmh > 900) {
            score += 60;
            reasons.add("IMPOSSIBLE_TRAVEL_SPEED");
        } else if (speedKmh > 200) {
            score += 25;
            reasons.add("SUSPICIOUS_VELOCITY");
        }

        if (!lastKnown.getKnownDeviceIds().contains(event.getDeviceId())) {
            score += 15;
            reasons.add("NEW_DEVICE");
        }

        if (event.getConcurrentSessionCount() > lastKnown.getPlanSessionLimit()) {
            score += 20;
            reasons.add("SESSION_LIMIT_EXCEEDED");
        }

        RiskLevel level = score >= 70 ? RiskLevel.HIGH
                         : score >= 35 ? RiskLevel.MEDIUM
                         : RiskLevel.LOW;

        return new RiskResult(score, level, reasons);
    }
}

The deep-path model then refines this initial score asynchronously using richer, slower-to-compute signals, and can revise the decision — for example, escalating a session that was initially allowed once the device-graph lookup reveals the same device has logged into forty unrelated accounts in the last hour.

Turning a score into a decision

The Decision Engine maps score ranges to actions using policy that product and risk teams can tune without a code deploy.

Score rangeRisk levelTypical action
0–34LowAllow silently
35–69MediumSoft challenge (send OTP, ask security question)
70–89HighHard challenge (force password reset, kill other sessions)
90–100CriticalImmediate lockout + fraud team alert + notify user via a channel not currently in question

How the machine-learning model is actually trained

It’s worth walking through where the risk model itself comes from, since “an ML model scores the event” glosses over a real engineering process with its own design decisions.

Choosing labels

Supervised learning needs ground truth: was a given login actually fraudulent, actually shared, or actually fine? These labels rarely exist upfront, so teams assemble them from several imperfect sources and combine them carefully:

  • Confirmed fraud reports. Users who report “this wasn’t me” after a notification, or chargebacks explicitly tied to account compromise.
  • Support-resolved cases. Tickets where an analyst manually reviewed a flagged session and confirmed or overturned the automated decision.
  • Self-service confirmations. When a challenged user successfully completes an OTP or security question, that session is a strong (though not certain) “legitimate” label.
  • Known attack infrastructure. Logins originating from IP ranges or device fingerprints independently confirmed as part of a credential-stuffing botnet, often via threat-intelligence feeds shared across the industry.

Because confirmed fraud labels are always rarer than confirmed legitimate ones, this is a classic class imbalance problem, addressed with techniques like class weighting during training, or evaluating the model on precision-recall curves rather than raw accuracy, which would look deceptively good on an imbalanced dataset even for a model that predicts “legitimate” every time.

Choosing the algorithm

Gradient-boosted decision-tree ensembles (such as XGBoost or LightGBM) are a common choice for this kind of tabular, feature-based risk scoring, because they handle a mix of numeric features (implied speed, session counts) and categorical features (device type, plan tier) well, train relatively fast, and, importantly, can expose feature importances that support explainability requirements. Deep neural networks are sometimes layered on top for specific sub-problems, like learning a device-fingerprint embedding from raw device signals or modeling behavioral biometrics sequences (typing rhythm, navigation patterns) where the input is naturally sequential rather than a flat feature vector.

Evaluation before shipping a new model version

A new model candidate is never promoted straight to production. It typically goes through:

  1. Offline evaluation against a held-out historical dataset, comparing precision, recall, and the false-positive rate against the current production model at matched thresholds.
  2. Shadow mode in production, scoring live traffic in parallel with the current model but never acting on its output, purely to compare score distributions and decision overlap on real, current traffic patterns rather than historical data that may already be stale.
  3. Canary rollout to a small percentage of real decisions, as described in Chapter 12, with automatic rollback if key metrics regress.

Feature freshness and training-serving skew

A subtle but important failure mode: if a feature is computed one way during offline model training (say, from a nightly batch job with slightly stale data) and a different way during real-time serving (from the live feature store), the model can behave unpredictably in production despite looking correct offline. This is called training-serving skew, and the standard mitigation is to compute features through the same shared feature-engineering code path for both training and serving, often by having the offline training pipeline read from the same feature store (or its historical snapshots) rather than reimplementing feature logic separately.

💬
What an interviewer may ask

“Why not just always block on any location anomaly?” Because that punishes real travelers and destroys trust; the interviewer is testing whether you understand that this is a probabilistic, graduated-response problem, not a binary allow/deny problem.

06

Data Flow & Lifecycle

Let’s trace one login event from the moment a user taps “Sign in” to the moment a decision is enforced, showing every component it touches. This is what turns the boxes in Chapter 4 from a static picture into an actual working pipeline.

Client App API Gateway Auth Service Session (Redis) Event Bus (Kafka) Stream Processor Risk Scoring ML Decision Eng FAST PATH (synchronous, inline with login) POST /login (credentials, device FP) rate limit OK, WAF pass, forward fetch last known loc + device list last fix, concurrent session count fast-path score = LOW / MED / HIGH token + risk band 200 OK, session token DEEP PATH (asynchronous, milliseconds behind) emit LoginEvent (fire-and-forget) consume from topic partition enrich: geo-velocity + FP graph lookup request deep inference score = MEDIUM, reasons = [NEW_DEVICE, FANOUT] submit scored event to policy apply policy, decide SOFT_CHALLENGE flag session for step-up auth push notification: "Verify it's you"
Figure 2 — A login that passes the fast path can still be escalated moments later by the asynchronous deep path, without blocking the user-facing login response.

Stage by stage

  1. Ingress: Load Balancer picks a healthy API Gateway instance based on least-connections or round-robin health-checked routing.
  2. Edge checks: API Gateway validates the request shape, enforces per-IP rate limits, and blocks anything the WAF has already flagged.
  3. Authentication: Auth Service verifies the password hash and, in the same synchronous call, runs the fast-path risk score using data already cached in the Session Service.
  4. Immediate response: The user gets an answer — allow, challenge, or deny — within the normal login-latency budget. This is non-negotiable from a UX standpoint.
  5. Asynchronous emission: Regardless of the immediate decision, the event is published to the event bus so the deep pipeline can analyze it without adding latency to the user-facing path.
  6. Stream enrichment: The Stream Processor joins the event with historical state (feature store) and calls the ML Risk Scoring Service for a fuller assessment.
  7. Policy evaluation: The Decision Engine combines the ML score with hard business rules (e.g., regulatory constraints, VIP account handling) to pick a final action.
  8. Enforcement: The Action Service updates session state, potentially killing other active sessions, and the Notification Service informs the user through a still-trusted channel like email or SMS.
  9. Feedback loop: The outcome (was this a true or false positive, based on later user behavior or support resolution) is written back into training data for the next model iteration.

Data lifecycle and retention

DataHot storageWarm storageCold storage
Active session stateRedis (milliseconds access)
Recent login/event history (30–90 days)Cassandra / DynamoDB
Historical events (compliance retention)Compressed columnar storeObject storage (S3-compatible), encrypted
Model training snapshotsData warehouseObject storage archive

Retention windows are set by both product need (how far back do we need to compare a user’s “normal” pattern) and legal requirements (many jurisdictions cap how long behavioral and location data can be kept, and require it to be deletable on user request).

Watch out

The feedback loop is the step teams most often skip on the first iteration — the pipeline runs, decisions are made, but nothing labels them as correct or incorrect afterward. Without that loop, the ML model can’t improve, and the false-positive/false-negative metrics in Chapter 11 become guesses rather than measurements.

07

Advantages, Disadvantages & Trade-offs

Every architectural choice buys something and costs something. This chapter names both sides plainly, so it’s clear what this design is optimized for and where a different situation might justify a different choice.

Advantages of this architecture

Low added latency

Separating a cheap synchronous fast path from a rich asynchronous deep path means the vast majority of logins are never slowed down by heavy analysis.

Graceful escalation

Because decisions aren’t binary, the system can start lenient and tighten as more evidence arrives, rather than needing perfect information up front.

Reusable signals

The same event stream and feature store power fraud detection, personalization, and capacity planning — not just this one use case.

Adaptability

Because policy thresholds live in the Decision Engine rather than hard-coded in client apps, the response to new attack patterns can change without a client release.

When this full architecture is not the right choice

It’s worth being direct about the cases where building the entire system described in this guide would be over-engineering rather than good design:

  • Very early-stage products with a small, mostly trusted user base, where a simple concurrent-session limit and an email alert on new-device login covers the realistic threat level far more cheaply than a streaming pipeline and ML model.
  • Internal tools with a small, known user population, where the attacker model (external credential stuffing at scale) barely applies, and simpler access controls plus basic audit logging address the actual risk.
  • Products with no meaningful per-account value to protect, where the cost of building and operating this system would exceed any realistic fraud loss it prevents.

Recognizing when a lighter-weight approach is the right engineering call, rather than defaulting to the most sophisticated architecture available, is itself part of good system design judgment, and often exactly what a thoughtful interviewer is listening for when they ask you to design a system like this from scratch.

Disadvantages and costs

Operational complexity

A streaming pipeline, a graph database, and an ML inference service are three more systems to keep healthy, patched, and monitored, compared to a simple rules engine.

False positives are inevitable

Any system sensitive enough to catch real attacks will occasionally challenge legitimate users, especially frequent travelers, VPN users, and shared family plans.

Model drift

Attacker behavior changes over time (that’s the whole point of an adversarial problem), so models need continuous retraining or they quietly lose effectiveness.

Cold-start problem

New accounts have no history to compare against, making early-life fraud harder to detect with behavioral signals alone.

Key trade-offs, side by side

Trade-offChoosing AChoosing B
Synchronous vs. asynchronous scoringSync: catches attacks before damage, adds latencyAsync: zero added latency, damage window before response
Hard rules vs. ML scoringRules: explainable, easy to audit, brittle against adaptive attackersML: adapts to new patterns, harder to explain a single decision
Strict thresholds vs. lenient thresholdsStrict: fewer takeovers slip through, more angry real usersLenient: happier real users, more fraud gets through
Centralized decision engine vs. per-service local checksCentralized: consistent policy, single point to tune, added hopLocal: lower latency per service, policy drift across services
🎯
Practical example of the trade-off

A bank might choose strict thresholds and accept more customer friction, because a single missed takeover can mean a large financial loss and regulatory scrutiny. A casual mobile game might choose lenient thresholds, because the cost of an annoyed player quitting outweighs the modest cost of some shared accounts.

Total cost of ownership, not just infrastructure cost

It’s tempting to evaluate this system purely on cloud infrastructure spend, but the full cost of ownership includes several less obvious categories that matter just as much when deciding how much of this architecture a given business actually needs on day one.

Cost categoryWhat it includes
InfrastructureCompute, storage, and network for every layer described in this guide, scaling with traffic volume
Engineering timeOngoing development of new features, rule tuning, and model iteration, which does not stop once the system first ships
Data science and MLOpsModel training infrastructure, labeling pipelines, and the specialized skill set needed to keep models healthy over time
Human review operationsTrust & safety analysts staffing the case-management queue for ambiguous cases the automated system can’t confidently resolve on its own
Support cost from false positivesEvery incorrectly challenged or blocked legitimate user generates a support interaction, which has a real, measurable cost per ticket
Opportunity cost of frictionHarder to measure directly but real: some fraction of legitimately challenged users abandon rather than complete the challenge, representing lost engagement or revenue

This is why smaller organizations often start with a much lighter version of this architecture — simple velocity and device checks, a managed vendor for bot mitigation, and manual review for anything ambiguous — and only grow into the full streaming-plus-ML architecture described in this guide once fraud volume and business scale justify the additional engineering and operational investment. Building the entire system described here on day one, for a product with a small user base and low fraud exposure, would likely cost more in engineering time than it saves in prevented fraud.

Organizational maturity as a hidden prerequisite

Beyond raw cost, this architecture assumes a certain level of organizational maturity that’s worth naming explicitly: a functioning on-call culture for the streaming pipeline, a data science practice capable of maintaining a model over time rather than just shipping one once, and a trust and safety operations function that can actually staff a case-review queue. A team that tries to adopt the full ML-driven architecture without these supporting capabilities in place often ends up with a system that technically runs but whose model quietly degrades, unmonitored, until a fraud spike forces a painful and rushed reinvestment. Matching architectural ambition to organizational readiness is itself a design decision, not a footnote.

08

Performance & Scalability

At the scale of a large consumer platform, this pipeline may need to process tens of thousands of login and activity events per second during peak hours, with sharp spikes around events like a popular show’s release or a major sale. Let’s look at how each layer scales, and then work through a concrete capacity-planning example so the numbers stop being abstract.

Horizontal scaling of the event bus

Kafka (or a managed equivalent like Kinesis) is partitioned by account ID, so events for a given account always land on the same partition, preserving per-account ordering, while different accounts spread across many partitions for parallelism. Adding more partitions and more consumer instances lets throughput grow roughly linearly.

topic configtopic: login-events
partitions: 128
partition_key: hash(account_id)
replication_factor: 3
retention: 7 days

Stream processor scaling

Frameworks like Apache Flink scale by adding more task slots and rebalancing partition ownership across them. Because state (like “last known location per account”) is partitioned the same way as the input topic, each worker only needs the state for the accounts it currently owns, keeping memory bounded per node rather than growing linearly with total account count.

Caching strategy

Fast-path checks depend entirely on cache hits. A Redis cluster holds per-account “last known state” (location, device list, session count) with a short TTL refreshed on every event, so a lookup during login is a single-digit-millisecond operation instead of a database query.

cache pattern

Cache-aside

Auth Service checks Redis first; on a miss (rare, e.g., cache eviction), it falls back to the account database and repopulates the cache.

cache pattern

Write-through updates

Every successful login updates the Redis entry immediately, so the next event always compares against the freshest known state.

Batching and backpressure

The deep-path ML inference service uses micro-batching, grouping several scoring requests together for efficient GPU/CPU utilization, while a backpressure mechanism (bounded queues with load shedding) ensures that if the ML service falls behind during a traffic spike, low-risk events are deprioritized rather than the whole pipeline stalling.

Capacity planning — typical vs. peak

MetricTypical baselinePeak (e.g., premiere night)
Login events / second1,50012,000+
Fast-path P99 latency25 ms60 ms
Deep-path processing lag< 2 sec< 15 sec (acceptable, since async)
Kafka consumer lag alert threshold10,000 messages50,000 messages (temporary)

A worked capacity-planning example

Let’s actually size the event bus and stream-processing tier for a platform with 50 million monthly active accounts, to make the scaling discussion concrete rather than abstract.

  • Assume each active account produces, on average, 4 trackable events per day (logins plus a few high-value actions like starting playback or opening sensitive settings).
  • Daily event volume: 50,000,000 accounts × 4 events ≈ 200,000,000 events/day.
  • Average throughput: 200,000,000 / 86,400 seconds ≈ 2,315 events/sec.
  • Peak factor: traffic is rarely uniform; evening peak hours in a platform’s primary time zones commonly carry 5–8× the average rate, so peak sustained throughput might be roughly 15,000–18,000 events/sec.
  • Event size: a compact serialized event (account ID, device ID, IP, timestamp, event type) is typically a few hundred bytes; at 18,000 events/sec and ~500 bytes each, that’s roughly 9 MB/sec of raw ingest — well within the throughput a modestly sized Kafka cluster handles comfortably.
  • Partition count: to keep any single partition’s throughput manageable (a common target is a few thousand events/sec per partition for comfortable headroom), 15,000–18,000 events/sec spread across 64–128 partitions leaves each partition well under its ceiling, with room to grow before repartitioning is needed.
  • Stream processor parallelism: task slots are typically provisioned to roughly match partition count divided by the throughput each task instance can sustain, so the same 64–128 partitions might map to 20–40 processing task instances depending on how much per-event computation (feature lookups, model calls) each one performs.

The exact numbers matter far less than the method: start from real or estimated traffic, apply a realistic peak multiplier (never plan capacity against the average), size message and payload throughput, and only then decide partition and instance counts, leaving comfortable headroom for growth and unexpected spikes rather than sizing exactly to today’s peak.

Read/write ratio considerations

The fast-path Redis lookups dominate read volume (every login reads the cached last-known state), while writes happen once per event to refresh that cache and once per event onto the Kafka topic. This read-heavy, write-moderate pattern is why Redis, with its very high read throughput ceiling per node, was chosen over a database for the hot path, while Kafka, optimized for high-throughput sequential writes with configurable replication, handles the durable event log.

💬
What an interviewer may ask

“How would you keep login latency stable during a traffic spike 8× normal load?” Look for answers about autoscaling API Gateway and Auth Service instances behind the Load Balancer, keeping fast-path checks cache-only (no synchronous DB or ML calls), and shedding load on the asynchronous deep path first since it doesn’t block the user.

09

High Availability & Reliability

A fraud-detection system that goes down shouldn’t mean nobody can log in. The design philosophy here is: fail open on convenience, fail closed on catastrophe, and never let this subsystem become a single point of failure for the whole product.

Redundancy at every layer

  • Multi-AZ, multi-region deployment. API Gateway, Auth Service, and the event bus run across at least three availability zones, so a single data-center failure doesn’t take down authentication entirely.
  • Database replication. The Account DB runs as a primary with multiple read replicas; the Event Store uses a wide-column database designed for multi-node replication out of the box (e.g., Cassandra’s tunable consistency).
  • Stateless services. API Gateway and Auth Service instances hold no local state, so the Load Balancer can route around a failed instance instantly with no data loss.

Graceful degradation — what happens when a dependency fails

Failed componentFallback behavior
Risk Scoring Service (ML) unavailableFall back to fast-path rule-based score only; log the gap for backfill scoring once recovered
Feature Store (Redis) unavailableFall back to a slower direct database read with a shorter timeout; if that also fails, allow login but flag session as “unscored” for priority review
Event bus (Kafka) unavailableBuffer events locally in the Auth Service with a bounded in-memory queue and retry; login itself is never blocked by this failure
Device Graph DB unavailableSkip device fan-out feature for this scoring window; weight other features more heavily

Notice the pattern: authentication itself (can this person log in at all) is treated as more critical than fraud scoring, so scoring failures degrade gracefully rather than blocking access. This is a deliberate design decision, not an oversight, because locking out every user whenever the fraud pipeline hiccups would be worse than temporarily under-scoring risk.

💡
Real-life analogy

This mirrors how airport security works during a system outage: they don’t shut the airport, they fall back to slower manual checks, accepting more friction and less automation rather than stopping all travel.

A concrete circuit-breaker example

Because the ML Risk Scoring Service is called for every event, wrapping that call in a circuit breaker prevents a slow or failing model from cascading its slowness up through the Stream Processor. The idea is simple: after a threshold of consecutive failures, stop calling the failing dependency for a cooldown window and use the fallback path instead, then probe cautiously before fully restoring.

javapublic class RiskScoringCircuitBreaker {

    private final AtomicInteger consecutiveFailures = new AtomicInteger(0);
    private final AtomicLong openedAtEpochMs = new AtomicLong(0L);
    private static final int FAILURE_THRESHOLD = 5;
    private static final long COOLDOWN_MS = 30_000L;

    public RiskResult score(LoginEvent event, FastPathScorer fallback) {
        if (isOpen()) {
            return fallback.score(event).markDegraded("ML_CIRCUIT_OPEN");
        }
        try {
            RiskResult r = mlClient.score(event); // remote call
            consecutiveFailures.set(0);
            return r;
        } catch (Exception e) {
            int failures = consecutiveFailures.incrementAndGet();
            if (failures >= FAILURE_THRESHOLD) {
                openedAtEpochMs.set(System.currentTimeMillis());
            }
            return fallback.score(event).markDegraded("ML_UNREACHABLE");
        }
    }

    private boolean isOpen() {
        long opened = openedAtEpochMs.get();
        if (opened == 0L) return false;
        if (System.currentTimeMillis() - opened < COOLDOWN_MS) return true;
        // cooldown elapsed — half-open, allow next call through as a probe
        openedAtEpochMs.set(0L);
        consecutiveFailures.set(FAILURE_THRESHOLD - 1);
        return false;
    }
}

Consistency trade-offs across the system

This architecture doesn’t apply a single consistency model everywhere; different data has different consistency needs, and being explicit about that is a hallmark of a mature design rather than defaulting to “strongly consistent everywhere” or “eventually consistent everywhere.”

DataConsistency choiceReasoning
Password / credential verificationStrong consistencyMust always read the current, correct credential state; stale reads here are a security bug, not just an inconvenience
Concurrent session countStrong-ish, region-local consistencyNeeds to be accurate enough to catch genuine plan-limit violations, but a few-hundred-millisecond staleness across regions is an acceptable trade for lower latency
Device graph (fan-out across accounts)Eventually consistentA device seen in one region propagating to the global graph within a few seconds, rather than instantly, is an acceptable trade-off given the value of horizontal scale and lower write latency here
Historical event log for model trainingEventually consistent, replicated asynchronouslyTraining pipelines read from batch snapshots anyway, so sub-second consistency provides no real benefit but would cost meaningful write latency

This is a direct, practical application of the CAP theorem’s core insight: during a network partition, a distributed system must choose between consistency and availability for a given piece of data, and different data in the same system can reasonably make different choices based on how costly staleness actually is for that specific use case.

Disaster recovery

  • RPO (Recovery Point Objective): Event data is replicated synchronously within a region and asynchronously cross-region, targeting a data-loss window of seconds, not minutes.
  • RTO (Recovery Time Objective): Automated failover to a secondary region for stateless services (API Gateway, Auth) within minutes; databases with pre-provisioned standby replicas promote automatically or via a one-click runbook.
  • Chaos testing. Regularly killing random instances, injecting network latency, and simulating region failures in staging to verify the fallback paths actually work, not just that they exist on paper.
💬
What an interviewer may ask

“What happens if your ML risk-scoring service goes down at 2 AM?” This tests whether you design for partial failure. The right instinct: never let a fraud-detection dependency become a login outage; degrade to simpler, faster, less accurate checks and alert on-call, rather than blocking all logins.

10

Security

This system is itself security-critical infrastructure, which means it has to be defended just as carefully as the accounts it’s protecting. Two distinct concerns matter here: securing the pipeline itself, and thinking like the attacker it’s trying to catch.

Securing the pipeline

  • Encryption in transit and at rest. All traffic between services (API Gateway to Auth Service, Stream Processor to Feature Store) runs over mutual TLS; data at rest in the Account DB and Event Store is encrypted using platform-managed keys.
  • Least-privilege access. The Risk Scoring Service can read features but cannot write to the Account DB; the Case Management dashboard used by human analysts requires its own authentication and audit-logged access to any raw location or device data.
  • Secrets management. Database credentials, API keys, and signing keys live in a dedicated secrets manager, never in code or config files, and rotate automatically on a schedule.
  • Tamper-evident audit logs. Every enforcement action (session kill, forced reset) is written to an append-only audit log, both for compliance and so a user disputing a lockout can be shown exactly why it happened.

Thinking like the attacker — evasion techniques and countermeasures

Attacker techniqueCountermeasure
Routing through residential proxies in the victim’s own country to defeat location checksCombine location with device-fingerprint continuity and behavioral biometrics, not IP geography alone
Slow, low-and-slow credential stuffing (one attempt per account per hour, across huge account lists)Device-to-account fan-out detection via the graph database catches the shared infrastructure even when velocity per account looks normal
Using headless browsers or automation frameworks that mimic real devicesBot-detection signals at the WAF layer (mouse-movement entropy, TLS-fingerprint anomalies, timing patterns) feed into the same risk score
Account takeover followed by immediately changing recovery email/phone to lock the real owner outTreat sensitive account changes as their own high-risk event type requiring step-up authentication, independent of the login risk score
Session-token theft (e.g., via malware) rather than password theftBind sessions to device fingerprint and periodically re-validate; anomalous session usage triggers the same detection pipeline as a fresh login

Privacy considerations

Because this system inherently processes sensitive data — precise location history, device identifiers, behavioral patterns — it must be built with privacy regulation in mind from day one, not bolted on afterward.

  • Data minimization. Store only the location precision actually needed (city-level is usually enough; exact GPS coordinates are rarely necessary for this use case).
  • Purpose limitation. Data collected for fraud detection shouldn’t silently be repurposed for advertising without separate legal basis and user disclosure.
  • Right to deletion. User data deletion requests must cascade through the Event Store, Feature Store, and any model-training snapshots, not just the primary Account DB.
  • Transparency. When a user is challenged or locked out, they deserve a plain-language explanation, not just “suspicious activity detected,” which erodes trust.

A quick threat-modeling walkthrough

It helps to reason through this system the way a security review would — considering each major category of threat against each major component — rather than treating security as a vague afterthought.

Threat categoryConcrete risk in this systemMitigation already in the design
SpoofingAn attacker forges device fingerprint data to appear as a previously trusted deviceFingerprint generation combines many hard-to-fake signals server-side rather than trusting a client-reported ID alone; anomalous fingerprint consistency itself becomes a feature
TamperingAn internal actor or compromised service modifies risk scores or session state directly in the cacheLeast-privilege service accounts; writes to session and risk state are only accepted from authenticated internal services over mutual TLS, with audit logging
RepudiationA user disputes that they were correctly locked out, or an attacker denies having caused a takeoverTamper-evident, append-only audit logs of every enforcement decision with the reason codes that triggered it
Information disclosureBreach of the device graph database exposes account-device-location relationships for many users at onceEncryption at rest, strict access controls, data minimization on precision of stored location
Denial of serviceAn attacker floods the login endpoint to exhaust the fast-path scoring capacity, degrading service for everyoneWAF-layer bot mitigation, per-IP and per-account rate limiting at the API Gateway, autoscaling behind the load balancer
Elevation of privilegeA compromised Risk Scoring Service instance is used as a pivot to reach the Account DB directlyNetwork segmentation and least-privilege database credentials scoped so the scoring service can read features but never touch account records directly

Running through a threat model like this once at design time, and again whenever a major new component is added, is far cheaper than discovering the gap after an incident.

Common mistake

Treating this pipeline’s data stores as exempt from the same security review applied to primary user data, because “it’s just fraud signals.” A device graph database that links accounts to devices is, in itself, a rich map of user relationships and behavior, and a very attractive target if breached.

💬
What an interviewer may ask

“How would you prevent an attacker from learning your detection thresholds through trial and error?” Good answers mention rate-limiting failed attempts aggressively, avoiding overly specific error messages that leak which check failed, randomizing minor timing / response variations, and rotating thresholds and features over time so a static model of the system goes stale for the attacker.

11

Monitoring, Logging & Metrics

A detection system that isn’t itself observed is flying blind about its own effectiveness. Three categories of visibility matter here: system health, pipeline health, and detection quality — and any one of them left dark can silently undo the value of the other two.

SYSTEM HEALTH API Gateway request & error rate basic availability of the login path Fast-path P50 / P95 / P99 latency directly affects user-perceived login speed ML inference latency & error rate detect degrading or overloaded scoring Redis cache hit ratio falling ratio quietly balloons fast-path latency PIPELINE HEALTH Kafka consumer lag is the deep pipeline keeping up? Stream Processor task-slot health crash loops, restart storms, state size Feature Store read latency Redis cluster hot spots, node failures Graph DB traversal time degrades silently as graph grows DETECTION QUALITY False positive rate real users incorrectly challenged Estimated false negative rate from chargebacks & recovery cases Challenge completion rate very low = broken challenge, not high fraud Time-to-detection how long from takeover to flag? Dashboards + tiered alerting (pager for pipeline outage, ticket for slow accuracy drift) uptime ≠ effectiveness — both categories matter, not just one
Figure 4 — Observability has three distinct dimensions here, and healthy operations means covering all three, not just system uptime.

System health metrics

MetricWhy it matters
API Gateway request rate & error rateBasic availability signal for the whole login path
Fast-path P50 / P95 / P99 latencyDirectly affects user-perceived login speed
Kafka consumer lagShows whether the deep pipeline is keeping up with event volume
Redis cache hit ratioA dropping hit ratio quietly increases fast-path latency and DB load
ML inference latency & error rateDetects a degrading or overloaded scoring service before it causes a fallback storm

Detection quality metrics

These are arguably more important than raw system health, because a perfectly fast system that catches nothing (or blocks everyone) has failed at its actual job.

MetricWhat it tells you
False positive ratePercentage of real users incorrectly challenged or blocked; tracked via support-ticket correlation and self-service “this was me” confirmations
False negative rate (estimated)Approximated through confirmed fraud cases that weren’t caught early, chargebacks, and account-recovery requests citing unauthorized access
Challenge completion rateOf users challenged, what fraction successfully verify vs. abandon; a very low completion rate can indicate the challenge itself is broken, not that fraud is high
Time-to-detectionHow long between an account takeover starting and the system flagging it
Model score distribution driftSudden shifts can indicate either a genuine new attack pattern or a data-pipeline bug upstream

Logging and tracing

Every event carries a correlation ID from the moment it enters the API Gateway through every downstream hop, so a single login can be traced end-to-end across the Auth Service, event bus, stream processor, and decision engine using distributed tracing (OpenTelemetry-style spans). Structured logs (JSON, not free text) are aggregated centrally so analysts can query “show me every action taken on account X in the last 48 hours” without grepping across a dozen services.

💻
Example structured log entry

A single decision, in one JSON object, is worth more than a paragraph of prose logs when reconstructing what happened.

json{
  "event": "login_decision",
  "trace_id": "6f2a-91bd-44e7",
  "account_id_hash": "a93f...",
  "risk_score": 78,
  "risk_level": "HIGH",
  "reasons": ["IMPOSSIBLE_TRAVEL_SPEED", "NEW_DEVICE"],
  "action": "HARD_CHALLENGE",
  "fast_path_latency_ms": 22,
  "deep_path_latency_ms": 340,
  "timestamp": "2026-07-29T10:04:12Z"
}

Dashboards and alerting

A real-time operations dashboard typically shows: current login throughput, risk-score distribution over the last hour, top challenge reasons, and consumer lag, with automated alerts firing when consumer lag crosses a threshold, when the false-positive-linked support-ticket rate spikes, or when the ML service’s error rate exceeds a small percentage. Alerts route to on-call via a paging system, with severity tiers so a full pipeline outage pages immediately while a slow model-accuracy drift generates a next-business-day ticket instead.

A sample on-call runbook excerpt

Concrete runbooks matter more than abstract monitoring philosophy when someone is paged at 3 AM. Here’s what a short excerpt might look like for one common alert:

runbookALERT: kafka_consumer_lag_critical
Trigger: login-events consumer group lag > 50,000 messages for 5+ minutes

Step 1: Check Stream Processor task health.
  -> If task instances are crash-looping, check recent deploys;
     roll back the last release if it correlates with lag onset.

Step 2: Check downstream dependency latency (Risk Scoring Service, Feature Store).
  -> If ML inference latency has spiked, the circuit breaker should already
     have engaged. Confirm fallback to rule-based scoring is active.
  -> If Redis feature store is degraded, check for a recent traffic spike
     or a node failure in the cluster.

Step 3: If lag is purely volume-driven (e.g., unexpected traffic spike),
  scale out Stream Processor task instances via the autoscaler,
  or manually override minReplicas if autoscaling hasn't caught up.

Step 4: If lag persists beyond 30 minutes, escalate to the risk
  engineering on-call lead and open an incident channel.
  Login availability is NOT affected by this alert (fast path is
  independent of stream lag), so this is a data-freshness incident,
  not a customer-facing outage, and should be triaged accordingly.

Notice the explicit last line: this runbook goes out of its way to remind the on-call engineer that this particular alert, unlike an API Gateway outage, does not mean users can’t log in. That distinction matters enormously for how urgently and how the incident gets escalated, and baking it directly into the runbook prevents unnecessary panic during an already stressful page.

Reviewing detection quality on a regular cadence

Beyond automated alerts, a healthy team runs a recurring review — often weekly or biweekly — that looks specifically at detection quality trends rather than only system health: how many confirmed false positives came through support this period, how the challenge-completion rate is trending, whether any new evasion pattern has appeared in confirmed fraud cases, and whether the current thresholds still reflect the business’s risk tolerance as conditions change (a holiday shopping season, a new market launch, or a widely publicized unrelated data breach that spikes credential-stuffing attempts across the industry are all reasons thresholds might need temporary adjustment).

💬
What an interviewer may ask

“How do you know your fraud model is actually working, not just running?” This is testing whether you distinguish uptime from effectiveness. Mention feedback loops: confirmed fraud cases and confirmed false positives both need to flow back into metrics and retraining data, not just into a dashboard nobody reviews.

12

Deployment & Cloud Considerations

Deploying a detection system like this safely is as much a design choice as the architecture itself. What follows is how the same architecture we’ve been drawing is actually made to roll out, scale, and roll back without breaking anything downstream.

Containerization and orchestration

Each microservice — API Gateway, Auth Service, Stream Processor, Risk Scoring Service, Decision Engine — is packaged as an independent container and deployed on an orchestrator such as Kubernetes. This allows each component to scale independently: the Risk Scoring Service might need GPU-backed nodes and scale differently from the lightweight, CPU-only API Gateway.

kubernetes# Simplified Kubernetes deployment snippet for the Risk Scoring Service
apiVersion: apps/v1
kind: Deployment
metadata:
  name: risk-scoring-service
spec:
  replicas: 6
  selector:
    matchLabels:
      app: risk-scoring
  template:
    spec:
      containers:
        - name: risk-scoring
          image: registry.internal/risk-scoring:2026.07.1
          resources:
            requests:
              cpu: "2"
              memory: "4Gi"
            limits:
              cpu: "4"
              memory: "8Gi"
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: risk-scoring-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: risk-scoring-service
  minReplicas: 6
  maxReplicas: 40
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 65

Multi-region strategy

Deploying the full stack (API Gateway through the Decision Engine) independently in each major region reduces latency and improves resilience, but introduces a hard question: does risk data need to be globally consistent, or can each region operate on its own view? Most systems accept eventual consistency for the deep-path risk signals (a device seen in one region propagates to the global graph within seconds, not instantly) while keeping session state strongly consistent within a region to avoid double-login race conditions.

Blue-green and canary deployments

Because a bug in the Decision Engine’s policy logic could either lock out legitimate users or let fraud through silently, changes here are rolled out via canary deployment: a small percentage of traffic (e.g., 5%) is routed to the new version, key metrics (false-positive rate, challenge rate) are compared against the baseline for a defined soak period, and the rollout only proceeds if metrics stay within expected bounds. A bad canary is rolled back automatically by the deployment pipeline without waiting for a human to notice.

Cost considerations

Cost driverOptimization lever
ML inference computeBatch low-priority scoring requests; use cheaper CPU inference for simple cases, reserving GPU for complex model calls
Event bus storage & replicationTune retention windows to the minimum needed for feature computation and compliance, not indefinitely
Cross-region data transferKeep hot session state regional; only replicate the smaller, aggregated risk signals globally
Graph database queriesCache frequent device-fan-out lookups; avoid recomputing full graph traversals on every event
💬
What an interviewer may ask

“How would you safely roll out a change to the risk-scoring model without risking a mass false-positive incident?” Look for canary rollout, shadow mode (running the new model alongside the old one, logging its decisions without acting on them, to compare before cutover), and automated rollback triggers tied to real metrics.

13

Databases, Caching & Load Balancing

There is no single database that fits every need here, which is why the architecture uses several, each chosen for a specific access pattern. This chapter dives into the storage, caching, load balancing, and rate limiting layers in the depth they deserve, since they’re the plumbing that makes the rest of the system possible.

Choosing the right database for each job

StoreTypeUsed forWhy this type fits
Account DBRelational (PostgreSQL)Account records, plan type, billing statusStrong consistency and relational integrity matter for billing-linked data
Event StoreWide-column (Cassandra / DynamoDB)High-volume login / activity event historyOptimized for very high write throughput and time-range queries per account
Feature Store (hot)In-memory (Redis)Last known state per account for fast-path scoringSub-millisecond reads at massive concurrency
Device Graph DBGraph (Neo4j / Neptune-style)Account-device-IP relationship traversalFan-out and relationship queries are natural in graph form, awkward in relational joins
Cold archiveObject storageLong-term compliance retentionCheapest durable storage for rarely-accessed data

Sharding strategy

The Account DB and Event Store are both sharded by account ID (typically via consistent hashing), so all data for a given account lives on a predictable, small set of nodes, letting per-account queries stay fast even as the total dataset grows into billions of rows. Cross-account queries (like the graph traversal for device fan-out) are the reason a dedicated graph database exists instead of trying to force this pattern into the sharded relational store.

Graph traversal in depth

The device graph is the reason the deep path can catch coordinated attacks that the fast path structurally can’t. Modeling accounts, devices, and IPs as nodes with edges representing observed logins lets the pipeline answer “how many distinct accounts has this device fingerprint touched in the last hour?” as a single-hop query, and “are two suspicious devices linked through a shared IP that also touches known bad accounts?” as a two-hop query, both of which would be recursive-join nightmares in a relational store.

javapublic class DeviceGraphFanoutLookup {

    // Returns how many distinct accounts this device fingerprint has touched
    // within the given trailing window. High values imply shared attack infra.
    public int distinctAccountsForDevice(String deviceFingerprintId,
                                        Duration lookback) {
        String cypher =
            "MATCH (d:Device {fp: $fp})-[r:LOGIN_ATTEMPT]->(a:Account) " +
            "WHERE r.ts >= $since " +
            "RETURN count(DISTINCT a) AS n";
        Map<String, Object> params = Map.of(
            "fp", deviceFingerprintId,
            "since", Instant.now().minus(lookback).toEpochMilli()
        );
        try (Session s = graphDriver.session()) {
            Record row = s.run(cypher, params).single();
            return row.get("n").asInt();
        }
    }
}

This kind of query becomes a feature in the model (device_fanout_1h, device_fanout_24h), rather than a rule on its own, so the ML model can learn the correct threshold for suspicion instead of relying on a hard-coded cutoff that an attacker eventually learns to stay just under.

Caching layers, in depth

L1

Session cache

Per-account last-known state in Redis, TTL a few hours, refreshed on every event. Powers the fast path.

L2

Feature cache

Precomputed aggregate features (e.g., 7-day device count) refreshed on a schedule, read by both fast and deep paths.

L3

Model cache

Cached inference results for identical feature vectors within a short window, reducing redundant ML calls during retry storms.

Load balancing in depth

Two distinct layers of load balancing exist in this architecture, and it’s worth being precise about the difference:

  • Global (DNS / L4) load balancing routes a user to their nearest healthy region, factoring in both network latency and regional capacity.
  • Regional (L7) load balancing distributes requests across API Gateway and downstream service instances within a region, using health checks and a load-aware algorithm (least-connections rather than simple round-robin, since request cost varies — a fast-path-only login is cheap; a request that triggers deep synchronous checks is more expensive).

The event bus itself acts as a third, implicit form of load distribution: partitioning spreads write and read load across many brokers and consumer instances without any component needing to know about the others’ current load directly.

Rate limiting, in depth

Rate limiting deserves its own close look, since it’s one of the cheapest and most effective defenses against credential stuffing, and it lives right at the API Gateway and the Redis-backed rate-limit cache mentioned earlier in the architecture.

AlgorithmHow it worksTrade-off
Fixed windowCount requests in a fixed time window (e.g., per minute); reset the counter at the window boundarySimple and cheap, but allows a burst right at the window boundary (e.g., 2× the limit across two adjacent windows)
Sliding window logTrack exact timestamps of recent requests and count how many fall within the trailing windowVery accurate, but memory cost grows with request volume per key
Sliding window counterApproximate the sliding window by weighting counts from the current and previous fixed windowsGood accuracy-to-cost balance; a common production default
Token bucketEach account / IP has a bucket that refills at a steady rate; each request consumes a tokenNaturally allows short bursts while enforcing a steady average rate, matching how real user behavior actually looks

This system layers rate limits at multiple keys simultaneously: per-IP (catching a single attacking machine), per-account (catching repeated attempts against one target, regardless of source IP), and per-device-fingerprint (catching an attacker rotating IPs but reusing the same automation toolchain). No single key alone is sufficient, since a sophisticated attacker can defeat any one of them individually by rotating that one dimension while holding the others fixed.

Testing strategy for this system

Testing a fraud-detection pipeline goes well beyond typical unit tests, since the thing being tested is a statistical decision process, not a deterministic function with one obviously correct output.

  • Unit tests cover deterministic pieces: the haversine distance calculation, feature extraction logic, and policy threshold mapping, all of which do have a single correct answer for a given input.
  • Integration tests verify that events actually flow correctly end-to-end through a test instance of the pipeline, from a simulated login through to a resulting notification, catching wiring bugs that unit tests can’t see.
  • Replay testing feeds historical, labeled traffic (including known past fraud incidents) through a candidate model or rule change to confirm it would have caught what it should have, before the change ever reaches production.
  • Adversarial testing / red-teaming has a dedicated team actively try to evade detection using known and novel techniques, treating the detection system the way an attacker would, to find blind spots before real attackers do.
  • Load testing confirms the fast path holds its latency budget under simulated peak traffic, and that graceful degradation paths actually trigger correctly when a dependency is deliberately made to fail during the test.
💬
What an interviewer may ask

“Why is the device graph in a separate database instead of just another table in the relational DB?” Because relationship-heavy, multi-hop queries (find all accounts sharing a device with this account, then all devices those accounts have used) become expensive recursive joins in a relational model but are near-native operations in a graph database, and this query pattern is central, not occasional, to the detection logic.

14

APIs & Microservices

Authentication, risk scoring, and enforcement have very different scaling profiles, release cadences, and failure tolerances, which is exactly the situation microservices are meant for. This chapter shows what the public and internal contracts look like, and why each one is shaped the way it is.

Why microservices fit this problem

The Auth Service needs to be rock-solid and rarely changes; the Risk Scoring Service iterates constantly as models improve; the Decision Engine’s policy might change weekly based on new fraud patterns. Bundling them into one deployable would force the slowest-changing, most critical piece (auth) to redeploy every time a policy tweak ships, which is the wrong optimization for both stability and iteration speed.

Representative API contracts

public

POST /v1/auth/login

User-facing login endpoint. Versioned, defensively validated, and rate-limited at the API Gateway before it ever reaches the Auth Service.

internal

POST /internal/v1/risk/score

Called by the Stream Processor. Not exposed publicly. Deals with raw behavioral features that shouldn’t be queryable from outside the service mesh.

internal

POST /internal/v1/action/enforce

Called by the Decision Engine. Executes the chosen action (kill sessions, force reset, open case) as a saga with retryable, idempotent steps.

public

POST /v1/auth/verify-challenge

Completes a soft or hard challenge started earlier. Its own rate limiting and its own audit log entry, because this is where a legitimate user proves they’re the real owner.

public login APIPOST /v1/auth/login
Request:
{
  "username": "user@example.com",
  "password_hash": "...",
  "device_fingerprint": "fp_9a3e...",
  "client_ip": "203.0.113.42"
}

Response (200):
{
  "session_token": "eyJhbGciOi...",
  "risk_action": "ALLOW",
  "risk_score": 12
}

Response (403, challenged):
{
  "risk_action": "SOFT_CHALLENGE",
  "challenge_type": "OTP",
  "reason_code": "NEW_DEVICE_UNUSUAL_LOCATION"
}
internal risk scoring APIPOST /internal/v1/risk/score   (called by Stream Processor, not public)
Request:
{
  "account_id": "acc_88213",
  "features": {
    "implied_speed_kmh": 1450,
    "device_known": false,
    "device_fanout_7d": 3,
    "concurrent_sessions": 2
  }
}

Response:
{
  "risk_score": 78,
  "risk_level": "HIGH",
  "top_contributing_features": ["implied_speed_kmh", "device_known"]
}

Internal vs. external APIs

Notice the two examples above: the login API is public-facing, versioned, and defensively validated against malformed or malicious input. The risk-scoring API is internal-only, reachable exclusively from the Stream Processor over the internal service mesh, never exposed to the public internet, since it deals with raw behavioral features that shouldn’t be queryable by anyone who can reach a public endpoint.

Communication patterns used

PatternWhere usedWhy
Synchronous REST / gRPCAPI Gateway to Auth Service, fast-path scoringNeeds an immediate response to answer the login request
Asynchronous event streaming (Kafka)Auth Service to Stream Processor, deep-path pipelineDecouples producers from consumers, absorbs traffic spikes, allows independent scaling
Request/response over internal service meshStream Processor to Risk Scoring ServiceLow-latency internal call, not meant to be durable or replayed like the event log

API Gateway responsibilities recap

Since the API Gateway appears throughout this design, it’s worth listing precisely what it owns, so it’s clear this isn’t just a generic proxy: authentication token validation, per-account and per-IP rate limiting, request routing to the correct microservice version, request/response logging with correlation IDs, and a first coarse IP-reputation check before any heavier logic runs downstream.

Idempotency and retries at the enforcement boundary

Enforcement actions (kill session, force reset, open case) can be retried if a partial failure happens mid-saga, so they must be idempotent. In practice, this means every enforcement call carries a unique action ID, and the Action Service records that ID as “already applied” the first time it succeeds, so a retry with the same ID is a no-op rather than a duplicate. Skipping this detail is a classic way to accidentally kill a user’s brand-new legitimate session that they just recovered, simply because a retry from the Decision Engine arrived after the first attempt had already succeeded.

💬
What an interviewer may ask

“Would you expose the risk scoring API publicly so third-party integrators could use it?” A thoughtful answer weighs the value (partners could pre-screen their own users) against the risk (exposing scoring internals helps attackers reverse-engineer thresholds), typically concluding that any external exposure should be a deliberately simplified, rate-limited, and separately versioned API, not the internal one.

15

Design Patterns & Anti-patterns

Many of the choices in this design have names in the distributed-systems literature. Naming them explicitly makes them easier to reason about, easier to teach to a new team member, and easier to notice when a well-known anti-pattern is quietly creeping into a design review.

Patterns used in this system

pattern

Event sourcing (partial)

Login and activity events are the source of truth for behavioral history; current risk state is derived by replaying/aggregating this event log rather than being independently maintained.

pattern

CQRS

Writes (raw events) go through the event bus; reads (fast-path risk lookups) go through a separately optimized, denormalized cache. Different shapes for writing versus reading.

pattern

Circuit breaker

Calls from the Stream Processor to the ML Risk Scoring Service trip a circuit breaker after repeated failures, falling back to rule-based scoring instead of cascading failure upstream.

pattern

Strangler pattern (for rollout)

New detection logic is introduced in shadow mode alongside the old rules engine, gradually taking over decisions as confidence builds, rather than a risky big-bang cutover.

pattern

Saga

A “lock down account” action is really a sequence of steps (kill sessions, force reset, notify user, log case); each step is tracked so a partial failure can be retried or compensated rather than leaving the account in an inconsistent state.

pattern

Bulkhead isolation

Resource pools (thread pools, connection pools) for the ML service call are isolated from those used for core auth, so a slow ML dependency can’t exhaust resources needed for basic login.

Anti-patterns to avoid

Anti-pattern — single hard-coded threshold rule

“Block if country changes within 1 hour” as the only detection logic. This is brittle, easily learned and evaded by attackers, and produces poor experiences for real travelers. Multiple weighted signals feeding a score are far more resilient.

Anti-pattern — synchronous chain of heavy checks in the login path

Calling the graph database, the ML model, and an external IP-reputation API all synchronously before returning a login response turns a 20 ms operation into a multi-hundred-millisecond one, and makes the entire login flow only as reliable as its slowest, most fragile dependency.

Anti-pattern — silent, unexplainable blocks

Locking a user out with no reason code and no path to self-recovery generates support load, erodes trust, and, in some jurisdictions, may run afoul of consumer-protection expectations around automated decision-making.

Anti-pattern — treating the model as “set and forget”

Deploying a risk model once and never retraining it. Attacker behavior evolves; a static model’s effectiveness decays, often invisibly, until a spike in fraud losses reveals the gap.

💬
What an interviewer may ask

“How does the circuit-breaker pattern apply here specifically?” Expect you to name the exact dependency it protects (the ML Risk Scoring Service call from the Stream Processor) and the exact fallback (revert to fast-path rule-based scoring), not just the pattern’s textbook definition.

16

Best Practices & Common Mistakes

Every long-running detection system develops a set of hard-won practices its team wishes it had known on day one. Below are the ones that tend to matter most across teams and industries, followed by the mistakes that most frequently derail teams building their first version of this system.

Best practices

  1. Score, don’t just rule. Combine many weak signals into a single weighted or ML-derived score rather than relying on any one hard-coded check.
  2. Separate detection latency tiers deliberately. Keep the user-facing path fast and cache-only; let the deep, expensive analysis happen asynchronously and act on its findings moments later if needed.
  3. Make every enforcement action explainable and reversible. Store the reason codes behind every challenge or block, and give users a clear self-service path to prove it’s really them.
  4. Treat this as an adversarial, evolving problem. Build retraining, feedback loops, and threshold review into the team’s regular cadence, not as a one-time launch task.
  5. Design for partial failure everywhere. Every external call in the pipeline needs a defined fallback; authentication itself should never go down because a fraud-scoring dependency did.
  6. Respect data minimization. Store the coarsest location precision that still does the job, and build deletion into the pipeline from day one rather than retrofitting it under regulatory pressure.
  7. Instrument detection quality, not just system health. False-positive and false-negative rates deserve the same dashboard prominence as latency and uptime.

Common mistakes

MistakeConsequenceBetter approach
Using IP country alone as the location signalMobile carriers and VPNs make this wildly inaccurateCombine IP geolocation with device signals and historical account behavior
Applying identical thresholds to every account typeA shared family plan and a solo single-device account need different baselinesPersonalize thresholds using each account’s own historical pattern
Blocking first, asking questions neverHigh support cost, user churn, reputational damagePrefer graduated response: soft challenge before hard block wherever risk allows
Ignoring the cold-start problem for new accountsFresh accounts have no baseline, making both false positives and false negatives more likelyUse broader population-level baselines for new accounts, tightening as history accumulates
Letting the fraud team and the product team tune thresholds independentlyInconsistent user experience, conflicting incentives (fraud loss vs. conversion)Shared ownership of policy thresholds with visibility into both fraud and friction metrics

Team structure and ownership

A system this cross-cutting tends to fail organizationally before it fails technically if ownership isn’t clear, so it’s worth designing the team boundaries as deliberately as the service boundaries.

AreaTypical owning teamWhy this boundary makes sense
API Gateway, Auth Service, Session ServiceCore platform / identity teamThese are foundational, high-availability infrastructure shared by the entire product, not specific to fraud detection alone
Stream processor, feature store, risk modelTrust & safety / risk engineering teamRequires close collaboration between data scientists and backend engineers, with deep domain knowledge of fraud patterns
Decision engine policy thresholdsJointly owned by risk engineering and product/business stakeholdersThreshold tuning is fundamentally a business risk-tolerance decision, not a purely technical one, and needs both perspectives at the table
Case management dashboardTrust & safety operations teamBuilt for and by the human analysts who use it daily, informed by their real workflow needs

A common organizational anti-pattern is letting the risk-engineering team own thresholds unilaterally, optimizing purely for fraud reduction without visibility into the resulting friction and churn — or the reverse, letting a growth-focused product team set thresholds without visibility into resulting fraud losses. Neither incentive alone produces a well-balanced system; the two need a shared dashboard and a shared review cadence.

A pre-launch readiness checklist

Before turning on any new detection component in production, it helps to run through a compact readiness checklist rather than trusting that “it looked fine in staging” will hold up under real load and real attackers.

  • Fast-path latency budget verified under simulated peak load, with headroom.
  • Graceful degradation paths tested by deliberately failing each downstream dependency in staging.
  • Shadow-mode comparison run for at least a full traffic cycle (day/night, weekday/weekend) before any real decisions are made.
  • Runbooks written and rehearsed for the top 5 expected alert types, including at least one drill with someone unfamiliar with the system.
  • User-facing challenge and lockout flows reviewed with support, legal, and localization for tone and clarity in every supported language.
  • Data retention and deletion pipelines tested end-to-end against a synthetic user-deletion request.
  • Rollback plan documented for every new component, not just the ones considered risky.
💬
What an interviewer may ask

“What’s the single most common mistake teams make when first building a system like this?” A strong answer: over-relying on one strong-looking signal (like IP country) instead of combining several weaker signals, because any single strong signal is exactly what attackers learn to evade first.

17

Real-World Examples, a Worked Incident & Glossary

Abstract architecture only carries a system-design discussion so far. This chapter grounds everything in three concrete forms: how different industries actually apply this design, a worked walkthrough of a single incident traced end-to-end through every component, and a compact glossary of the vocabulary used throughout the guide.

Industry examples

streaming

Streaming platforms

Major video streaming services have discussed publicly that they evaluate signals such as IP addresses, device identifiers, and location/account activity to distinguish a household sharing a subscription across a couple of devices from sharing far beyond a single home, generally responding by prompting the account owner to add paid members rather than an immediate ban — reflecting the “sharing” branch of this problem rather than the “takeover” branch.

banking

Banking & payments

Card networks and banks run real-time fraud scoring on every transaction and login, using velocity checks (impossible travel between a card-present transaction and an online transaction), device fingerprinting, and behavioral biometrics, generally with a much lower tolerance for false negatives given direct financial exposure and regulatory obligations around unauthorized transactions.

enterprise SaaS

Enterprise identity providers

Enterprise identity platforms offer “impossible travel” and “anomalous login” detection as a built-in feature for corporate customers, often combined with conditional access policies (e.g., requiring multi-factor authentication automatically whenever a login risk score crosses a threshold) — showing how this same detection core generalizes beyond consumer subscription businesses into workplace identity security.

gaming

Gaming platforms

Online gaming platforms apply similar concurrent-session and device-fingerprint detection primarily to catch account takeover (valuable in-game items and currency are a common theft target), as well as to enforce account-sharing policies tied to licensing terms.

Across all these industries, the underlying architecture is remarkably consistent: an edge layer (load balancer, API gateway) in front of an authentication service, a real-time event pipeline for enrichment, and a scoring/decision layer that outputs a graduated response. What differs by industry is mainly the response policy: how aggressive the thresholds are, and how much friction the business is willing to impose on real users in exchange for catching more fraud.

What each industry teaches the others

  • From banking: graduated response and explainability aren’t optional extras once real money or regulatory obligations are involved; every automated decision needs a defensible, loggable reason.
  • From streaming: not every anomaly is malicious, and a system that treats all anomalies as attacks will alienate a large, profitable share of legitimate users; sometimes the correct response to an anomaly is a friendly upsell, not a lockout.
  • From enterprise identity platforms: making risk-based authentication configurable as policy (rather than hard-coded) lets very different customers — a five-person startup and a fifty-thousand-employee bank — tune the same underlying detection engine to their own risk tolerance.
  • From gaming: where the protected asset is virtual (in-game currency, rare items) rather than directly financial, the fraud economics still justify serious investment in detection, because a marketplace for stolen accounts and items can rival real-world financial fraud in scale.

A worked incident, traced end-to-end through every component

Reading a list of components is one thing; watching them cooperate to catch a real incident is another. Let’s walk through a single, concrete scenario from start to finish, naming the exact component responsible at each step.

1 Attacker → WAF Credential-stuffing tool tries Arjun's leaked u/p pair from a data-center IP range. WAF flags source as known DC infra, contributing moderate risk pre-gateway. 2 Auth Service → fast-path score Password correct, but fast path sees: unknown device FP, DC-flagged IP, location nowhere near Bengaluru. Score = MEDIUM → OTP soft challenge. 3 Attacker fails the OTP No access to Arjun's phone, OTP fails. Session denied. But the login attempt is still published to the event bus, because high-risk attempts are logged too. 4 Stream Processor → graph DB Consumes the event, checks device FP against the Device Graph. Discovers same FP hit 40 unrelated accounts in the past hour — strong fan-out signal invisible per-account. 5 Decision Engine → escalation Combines individual failed attempt + campaign fan-out. Raises rate-limit stringency for that IP range at the gateway. Opens single case for the campaign, not 40 unrelated incidents. 6 Notification → the real Arjun Email to Arjun: “We blocked a suspicious sign-in. If this wasn't you, please change your password.” Nudges rotation before an attacker with a fresher list tries again. What this walkthrough shows about the design as a whole • No single component "caught" this incident on its own — the WAF, the fast path, the graph DB, and the decision engine each contributed one piece of evidence that only made sense in combination. • The fast path did its job — blocked in real time — without needing the full campaign picture. • The deep path turned an isolated failed login into an understood, mitigated campaign across 40 accounts. • The user got a proactive, plain-language notification instead of a silent block — trust preserved. • A single case was opened for a coordinated campaign, not 40 disconnected tickets — scalable T&S ops. • Rate-limit stringency for the offending IP range was raised automatically for every subsequent attempt, without any human in the loop, protecting accounts 41 through N before they were even targeted. • Every step above emitted structured, correlation-ID-tagged logs, so the full incident is replayable end-to-end for postmortem and for feeding the model's training pipeline as high-quality labeled data.
Figure 5 — A worked incident. Each numbered step is owned by a named component; the outcome shows how weak signals combine into a strong, campaign-level response without slowing down real users.

The scenario

An account holder, Arjun, has a stable pattern: logins almost always from Bengaluru, always from one of two devices (his phone and his laptop), on his home broadband ISP. His password appeared in a breach dump three weeks ago on an unrelated forum he had signed up for years earlier, and he reused the same password on this account.

Step 1 — The attacker’s first attempt

A credential-stuffing tool, running from a data-center IP range, tries Arjun’s leaked username/password pair along with thousands of others. The WAF / Bot Mitigation layer flags the source IP as belonging to known data-center infrastructure rarely associated with real consumer logins, adding a moderate risk contribution before the request even reaches the API Gateway.

Step 2 — Authentication succeeds, but risk is already elevated

The password is correct, so the Auth Service authenticates the request. But its synchronous fast-path check, reading Arjun’s cached state from the Session Service (Redis), sees: unfamiliar device fingerprint, data-center-flagged IP, and a location (based on IP geolocation) nowhere near Bengaluru. The fast-path score lands in the “medium” band, triggering a soft challenge: an OTP sent to Arjun’s registered phone number.

Step 3 — The attacker fails the challenge, but the event still matters

The attacker’s tooling has no access to Arjun’s phone, so the OTP challenge fails and the session is denied. This might look like the story ends here, but the login attempt itself was already published as an event by the Event Producer onto the Kafka Event Bus, because the system logs both successful and failed high-risk attempts for downstream analysis.

Step 4 — The deep pipeline connects the dots across accounts

The Stream Processor consumes this event and, through the Device Fingerprint Service, checks the same device fingerprint against the Device Graph Database. It discovers this exact fingerprint (or the same source IP range) has attempted logins against 40 other unrelated accounts in the past hour — a strong device-to-account fan-out signal that no single-account view could have revealed. This gets written back into the graph and feature store, raising the baseline risk associated with this device/IP combination for every account it touches next.

Step 5 — Escalation and case creation

The Decision Engine, now seeing both Arjun’s individual failed high-risk attempt and the broader fan-out pattern from the Risk Scoring Service, escalates this from an isolated event to a suspected coordinated credential-stuffing campaign. It automatically raises rate-limiting stringency for the associated IP range at the API Gateway and opens a case in the Case Management dashboard for the trust and safety team to review the broader campaign, rather than treating each of the 40 affected accounts as forty separate, unrelated incidents.

Step 6 — Protecting the real Arjun

Separately, because Arjun’s account specifically saw a high-risk failed attempt, the Notification Service emails him proactively: “We blocked a suspicious sign-in attempt on your account. If this wasn’t a trip you’re currently on, we recommend changing your password.” This nudges him to rotate his reused password before an attacker with a fresher, unbreached credential list tries again.

🎯
Why this example matters

Notice that no single component “caught” this incident. The WAF contributed a weak signal, the fast path caught it well enough to challenge in real time, and the deep pipeline’s cross-account graph view is what turned an isolated failed login into an understood campaign, informing a much stronger, platform-wide response than any single account’s data could justify alone.

Glossary of terms used throughout this guide

TermPlain-English meaning
Impossible travelA pair of login locations and timestamps that implies a speed of travel no real person could achieve
Device fingerprintA derived identifier built from a device’s browser, OS, hardware, and network characteristics, used to recognize a returning device without needing a stored cookie
Credential stuffingAutomatically trying large lists of previously breached username/password pairs against a different service, hoping for password reuse
Account takeover (ATO)An attacker gaining unauthorized control of a legitimate user’s account
Fast path / deep pathThe two-tier pattern used in this system: a cheap synchronous check inline with login, and a richer asynchronous check that follows moments later
Feature storeA specialized storage layer holding precomputed signals (features) ready for fast lookup by both the live system and model training pipelines
Device-to-account fan-outHow many distinct accounts a single device or IP has been used to access — a strong signal of shared attack infrastructure when unusually high
Step-up authenticationAsking for extra proof of identity (like an OTP) only when risk is elevated, rather than for every login
Shadow modeRunning a new model or rule in production, scoring real traffic, without letting its output affect any real decision, purely to observe its behavior safely
Training-serving skewA mismatch between how a feature is computed during model training versus during live production serving, which can silently degrade model accuracy
💬
What an interviewer may ask

“Walk me through what actually happens, component by component, when a credential-stuffing campaign hits your platform.” A strong answer traces the same story above — WAF, fast path, event bus, stream processor, graph database, decision engine, action, notification — naming both the role and the concrete component at each step, and closing with how the deep path’s cross-account view turns the incident from “forty unrelated events” into “one understood campaign.”

18

FAQ — Frequently Asked Questions

These are the questions that keep coming up in design reviews, system-design interviews, and post-incident retrospectives on systems like this one. Each answer is meant to be usable both as a quick reference and as a starting point for a deeper conversation.

Q1. Does this system need machine learning, or can rules alone work?

Simple rules can work for a first version and catch the most obvious cases (extreme impossible travel), but they plateau quickly against adaptive attackers and produce more false positives than a properly weighted, learned model. Most mature systems use both: rules for clear-cut policy (e.g., legal or compliance constraints) and ML for the nuanced, evolving parts of the score.

Q2. How do you avoid punishing legitimate frequent travelers?

By personalizing the baseline to each account’s own history (a road warrior with a two-year history of frequent country changes has a very different “normal” than someone who has never left their home city), and by combining location with device continuity, so a familiar device in a new country scores very differently than an unfamiliar device in a new country.

Q3. What’s the very first check that should run, if a team can only build one thing?

A simple impossible-travel velocity check using cached last-known location, combined with a device fingerprint comparison. This pair alone catches a large share of both credential stuffing and casual sharing, and it can be built without a full streaming pipeline or ML infrastructure.

Q4. How is this different from a generic rate limiter?

A rate limiter caps the volume of requests regardless of identity or location; this system reasons specifically about whether the pattern of usage is consistent with a single legitimate user, using identity, location, and device context — not just request counts.

Q5. Should the same system handle both sharing and takeover, or should they be separate?

They should share the same detection signals and pipeline (since the raw evidence overlaps heavily) but diverge at the Decision Engine, where policy for sharing (nudge, upsell) is deliberately much gentler than policy for suspected takeover (lock down, alert, force reset).

Q6. Can users on legitimate VPNs avoid getting constantly flagged?

Yes, with good design: a VPN’s IP alone should be a mild feature, not an automatic trigger, and consistency matters more than any single reading. An account that always connects from the same VPN endpoint builds its own stable baseline over time, so the system learns “this is normal for this user” rather than treating every VPN connection as suspicious forever.

Q7. How long does it typically take to build a first working version of a system like this?

A minimal version — fast-path velocity and device checks with simple threshold rules, without any streaming pipeline or ML — can be built by a small team in a few weeks, since it mainly needs a cache of last-known state and a scoring function inline with login. The full architecture described here, with a streaming pipeline, graph database, and trained ML models, is typically a multi-quarter investment built incrementally, starting from that simpler rules-based core and layering in richer signals as false-positive and false-negative rates from the simple version reveal where the gaps are.

Q8. What happens to a user who is incorrectly flagged, from their point of view?

In a well-designed system, very little friction: a soft challenge like an OTP that takes seconds to complete, with a clear, plain-language reason (“we noticed a sign-in from a new location”) rather than a cryptic error. Only genuinely high-confidence, high-risk cases should ever result in a full account lockout, and even then, a self-service recovery path (identity verification, account recovery flow) should exist so a legitimate user is never permanently stuck without human intervention.

Q9. Does adding more signals always make the system better?

Not automatically. Each additional signal adds engineering and operational cost, and if it’s highly correlated with signals already in use, it may add little real predictive value while still adding latency and complexity. Mature teams evaluate new signals by how much they improve precision and recall beyond what the existing signal set already captures, not simply by whether the new signal sounds intuitively useful.

Q10. How does this system interact with multi-factor authentication (MFA) more broadly?

Risk-based, step-up MFA (asking for a second factor only when risk is elevated) is generally considered better for user experience than always-on MFA for every single login, since it concentrates friction where it’s actually justified by risk. This system is what makes that possible: the risk score computed here is exactly the input that decides whether to prompt for a second factor at all, turning MFA from a blunt, always-present hurdle into a targeted, context-aware one.

💬
What an interviewer may ask

“If you had to pick the single hardest problem in this system to get right, which would it be?” A strong answer typically lands on threshold tuning and the false-positive/false-negative balance — the technical parts are hard, but the judgment call of how much friction to impose on real users in exchange for how much fraud caught is where the system most obviously either earns or loses its keep.

19

Summary & Key Takeaways

Detecting account sharing and credential leaks is, at its core, a probabilistic, graduated-response, adversarial problem hiding under an authentication flow. The solution is a two-tier pipeline — a cheap synchronous fast path inline with login, and a rich asynchronous deep path that follows moments later — feeding a shared feature store, a machine-learning risk model, and a Decision Engine whose policy can be tuned without a code deploy.

What makes this problem genuinely interesting, and a favorite in system design interviews, is that it sits at the intersection of several classic distributed-systems ideas — caching, consistency trade-offs, load balancing, failure isolation, event streaming, graph databases, ML inference, and human-in-the-loop operations — applied to a single, very concrete, very relatable scenario that almost anyone can picture: one account, suddenly used from two places at once. A candidate (or an engineer) who can walk through this scenario methodically, starting from “why the two symptoms look identical but need different responses” through to “here’s a layered, tested, observable, explainable system that acts in milliseconds without blocking legitimate users,” is demonstrating exactly the kind of systems thinking that separates a system that merely runs from one that keeps working correctly under the exact conditions it was built to survive.

Key takeaways
  • Account sharing and credential leaks are solved by the same signals — impossible travel, device fingerprints, concurrent sessions — but require very different response policies.
  • A two-tier pipeline (fast synchronous + deep asynchronous) balances low login latency against thorough analysis, and this split between cheap-and-immediate versus rich-and-delayed processing is a pattern worth recognizing well beyond this specific problem.
  • Every architectural box — load balancer, API gateway, event bus, feature store, graph database — exists to serve either speed, accuracy, or resilience, and understanding which is a strong signal of design maturity.
  • Detection should be probabilistic and graduated, not a single binary rule, because the cost of false positives and false negatives are both real and asymmetric.
  • This is an adversarial, moving-target problem: retraining, threshold review, and monitoring detection quality (not just system uptime) are ongoing operational responsibilities, not one-time launch tasks, because whatever works well against today’s attackers will eventually be studied and worked around by tomorrow’s.
  • Fail open on convenience, fail closed on catastrophe: a fraud-detection outage must never become a login outage, but a high-confidence takeover must never quietly succeed.
  • Privacy and explainability aren’t optional extras; they’re core requirements given the sensitivity of location and behavioral data this system necessarily touches.
💭
Final thought

The hard part of this system was never detecting that something unusual happened — a simple distance-over-time calculation does that in a few lines of code. The hard part is deciding, calmly and consistently, what to do about it, in a way that protects real users from real attackers without turning every ordinary traveler, VPN user, or family plan into a suspect. Every architectural choice in this guide — the split between fast and deep paths, the graduated response ladder, the emphasis on explainability and graceful degradation — ultimately exists in service of that one balancing act, and it’s worth returning to that framing whenever a new signal, threshold, or component is being considered for the system.