Skip to main content

Command Palette

Search for a command to run...

Building Fraud Detection Systems: Lessons from a BNPL Platform

Updated
4 min readView as Markdown

Leading the design of fraud detection for a distributed BNPL platform taught me something I didn't fully appreciate going in: fraud detection isn't primarily a machine learning problem. The model is one piece. The harder engineering problem is building a system that can make a real-time decision, under latency pressure, without either blocking legitimate customers or letting obvious fraud through, while staying maintainable as fraud patterns keep shifting.

The latency constraint changes everything

A fraud check that takes three seconds is often worse than a slightly less accurate one that takes 200 milliseconds, because the customer is standing at a checkout screen waiting. This constraint shaped almost every architectural decision on that platform. We couldn't run every possible check for every transaction. We had to build a tiered system: fast, cheap rules that caught obvious fraud immediately, backed by a more expensive model-based check only when the cheap rules didn't produce a confident answer either way.

csharp

public async Task<FraudDecision> EvaluateAsync(Transaction transaction)
{
    var quickResult = _ruleEngine.Evaluate(transaction);
    if (quickResult.Confidence > 0.9) return quickResult.Decision;

    return await _mlScoringService.ScoreAsync(transaction);
}

This tiered approach kept the common case fast while still giving genuinely ambiguous transactions the deeper evaluation they needed.

Rules and models need to coexist, not compete

Early on there was a temptation to treat the ML model as the eventual replacement for hand-written rules. In practice, the two served different purposes and both stayed valuable. Rules were transparent, fast to update, and easy to explain to a compliance team asking why a specific transaction was flagged. The model caught subtler patterns rules couldn't easily express, but its decisions were harder to explain after the fact. Keeping both, with rules as the first line and the model as the deeper check, gave us both speed and explainability where each mattered most.

Feedback loops matter more than the initial model

Fraud patterns don't stay still. A model trained on last year's fraud data degrades against new tactics without anyone touching the code. Building a reliable feedback loop, where confirmed fraud and confirmed false positives flow back into retraining, mattered more long-term than any single model's initial accuracy. Teams that treat the model as a one-time deliverable are setting themselves up for quietly worsening detection over time.

False positives have a real cost, not just a theoretical one

It's easy to optimize purely for catching fraud and lose sight of the cost of blocking legitimate customers. A customer wrongly declined at checkout doesn't just fail one transaction, they often don't come back. We tracked false positive rate as seriously as we tracked fraud catch rate, and reviewed both together rather than treating fraud detection as a problem with only one failure mode.

Explaining decisions, even to yourself

When a transaction gets flagged, someone eventually asks why. If your system can't answer that clearly, you're flying blind on your own fraud detection, and worse, you can't improve a decision process you can't inspect. We logged the specific rule or model signal that drove each decision, not just the final outcome, because that log became the single most useful input for improving the system over time.

csharp

_logger.LogInformation("Fraud decision {Decision} for transaction {Id}, triggered by {Signal}",
    decision, transaction.Id, triggeredSignal);

Resilience matters here too

A fraud service that goes down shouldn't take checkout down with it. We built explicit fallback behavior, a conservative default when the fraud service was unreachable, rather than letting an outage in one service cascade into blocking every transaction platform-wide. This is the same resilience thinking I'd apply to any critical downstream dependency, applied specifically to a service where both failure directions, blocking everyone or blocking no one, carry real cost.

The real lesson

Fraud detection succeeds or fails on the engineering around the model as much as the model itself: latency budgets, explainability, feedback loops, and graceful degradation when the detection service itself has a bad day. The model gets most of the attention. The system around it is what actually makes it trustworthy in production.

4 views
F

The tiered architecture is the part that stuck with me, cheap rules catching the obvious cases, model only for the ambiguous middle. It's a good template for account-level checks too, not just transaction-time ones.

One rule that fits neatly into that fast tier: impossible travel detection, flagging a login if the distance and time since the account's last login imply a travel speed no human could hit. It's cheap (no model inference, just a distance calculation), and it produces exactly the kind of explainable signal your "explaining decisions, even to yourself" section argues for, the output is literally distance, elapsed time, and implied speed, not a black-box score.

It also catches something transaction-pattern rules structurally can't: an attacker using a completely clean, unflagged IP, since the only thing wrong is the login sequence, not any single login on its own.

Open sourced a small library for it if useful: github.com/Furqan-Ashraf/impossible-travel-guard

Did session or login-level signals factor into the BNPL platform at all, or was fraud detection scoped to the transaction layer entirely?