Machine Coding Problem

Fraud Detection Rules Engine

maco60macoAllsecuritypredicate-logicthresholds
Commonly Asked By:StripePayPalCoinbaseRobinhood

Requirements & System Scope

Functional Scope (In-Scope)

  • Stateless Predicate Logic Rules: Decoupled and independent stateless rules evaluating specific components of transactional context.
  • Aggregated Risk Score: Composing individual rule outcomes into a singular, normalized risk score (0โ€“100) using a weighted average.
  • Configurable Action Thresholds: Mapping transaction risk scores dynamically to ALLOW (score < 35), CHALLENGE (35โ€“75), or BLOCK (score >= 75).
  • Transparent Auditing Logs: Attaching explanations to decisions highlighting exactly which rules triggered and their relative contributions.

Explicit Boundaries (Out-of-Scope)

  • Statistical Model Pipelines: Real-time statistical offline ML updates are decoupled and processed asynchronously.
  • Persistent Storage Layer: Focuses purely on low-latency rules evaluation in-memory, bypassing deep DB transaction writes.

Class Diagram & Entity Relationships

Entity structure delineating the composition of rules inside the active RulesEngine:

Loading...
  • Atomic rule references: Allows hot-swapping active rules rulesets without stopping current evaluations.
  • Contextual variables: Auxiliary historical datasets (velocity checks, location histories) are fed from high-speed cached stores.

Design Patterns & SOLID Principles

  • Strategy Pattern (Stateless Checks): Each `Rule` encapsulates a unique rule algorithm, enabling the engine to execute any arbitrary check uniformly, keeping code highly extensible.
  • Single Responsibility Principle (SRP): The scoring math, policy action assignment, and transaction structures are partitioned to support focused testing.
  • Dependency Inversion Principle (DIP): The `RulesEngine` depends on the `Rule` interface abstractions rather than concrete rule implementations.

Core Execution Workflows

Transaction Evaluation Sequence

  1. An incoming transaction hits the checkout API pipeline.
  2. The system populates `Transaction` fields and fetches precomputed features from the cache context.
  3. The `RulesEngine` fetches the atomic reference copy of currently active rules.
  4. The engine iterates through all active rules in parallel or sequence, evaluating the transaction context.
  5. Weighted scores are aggregated: Score = Sum(RawScore * Weight) / Sum(Weights).
  6. Resulting scores are mapped against the set thresholds to generate a final `Decision`.
  7. Explanations are appended, the decision is returned, and exposure is logged.

Concurrency & Thread Safety Strategy

Operating safely under ultra-low-latency high-concurrency rules evaluation:

  • Atomic Reference Hot-Reloading: We utilize atomic reference updates (or copy-on-write pointers) to allow hot-reloading configurations seamlessly without any downtime or locks.
  • Stateless Evaluation Flow: Transactions and rule contexts are passed strictly as immutable parameters to avoid thread synchronization overhead.

Complete Clean Code Blueprint

Clean reference designs demonstrating deterministic rules engine in Java and Python:

// โ”€โ”€โ”€ JAVA BLUEPRINT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;

enum FraudAction { ALLOW, CHALLENGE, BLOCK }

// Immutable transaction payload
class Transaction {
    private final String id;
    private final String userId;
    private final double amount;
    private final String location;
    private final String deviceId;
    private final long timestamp;

    public Transaction(String id, String userId, double amount, String location, String deviceId, long timestamp) {
        this.id = id;
        this.userId = userId;
        this.amount = amount;
        this.location = location;
        this.deviceId = deviceId;
        this.timestamp = timestamp;
    }

    public String getId() { return id; }
    public String getUserId() { return userId; }
    public double getAmount() { return amount; }
    public String getLocation() { return location; }
    public String getDeviceId() { return deviceId; }
    public long getTimestamp() { return timestamp; }
}

// Interface for stateless rule evaluation
interface Rule {
    String getName();
    double evaluate(Transaction txn, Map<String, Object> context);
}

// Rule 1: High Transaction Amount Check
class HighAmountRule implements Rule {
    private final double limit;

    public HighAmountRule(double limit) {
        this.limit = limit;
    }

    @Override public String getName() { return "HIGH_AMOUNT_CHECK"; }

    @Override
    public double evaluate(Transaction txn, Map<String, Object> context) {
        if (txn.getAmount() > limit) {
            return 100.0; // Max risk score for this rule
        }
        return 0.0;
    }
}

// Rule 2: Geo-location Anomaly relative to last transaction
class LocationAnomalyRule implements Rule {
    @Override public String getName() { return "LOCATION_ANOMALY"; }

    @Override
    public double evaluate(Transaction txn, Map<String, Object> context) {
        String lastLocation = (String) context.get("last_location");
        if (lastLocation != null && !lastLocation.equalsIgnoreCase(txn.getLocation())) {
            return 80.0; // High risk contribution
        }
        return 0.0;
    }
}

// Rule 3: Velocity Check (too many transactions in a short window)
class VelocityAnomalyRule implements Rule {
    private final int maxCount;
    private final long timeWindowMs;

    public VelocityAnomalyRule(int maxCount, long timeWindowMs) {
        this.maxCount = maxCount;
        this.timeWindowMs = timeWindowMs;
    }

    @Override public String getName() { return "VELOCITY_CHECK"; }

    @Override
    public double evaluate(Transaction txn, Map<String, Object> context) {
        @SuppressWarnings("unchecked")
        List<Long> transactionHistory = (List<Long>) context.get("transaction_timestamps");
        if (transactionHistory == null) return 0.0;

        long thresholdTime = txn.getTimestamp() - timeWindowMs;
        long recentCount = transactionHistory.stream()
            .filter(t -> t >= thresholdTime)
            .count();

        if (recentCount >= maxCount) {
            return 90.0;
        }
        return 0.0;
    }
}

// Rule wrapper carrying a configurable weight
class RuleWeight {
    private final Rule rule;
    private final double weight;

    public RuleWeight(Rule rule, double weight) {
        this.rule = rule;
        this.weight = weight;
    }

    public Rule getRule() { return rule; }
    public double getWeight() { return weight; }
}

// Audit record containing individual rule contribution details
class RuleEvaluationResult {
    private final String ruleName;
    private final double rawScore;
    private final double weightedContribution;

    public RuleEvaluationResult(String ruleName, double rawScore, double weightedContribution) {
        this.ruleName = ruleName;
        this.rawScore = rawScore;
        this.weightedContribution = weightedContribution;
    }

    public String getRuleName() { return ruleName; }
    public double getRawScore() { return rawScore; }
    public double getWeightedContribution() { return weightedContribution; }
}

// Decision outcome wrapper
class Decision {
    private final String transactionId;
    private final double finalRiskScore;
    private final FraudAction action;
    private final List<RuleEvaluationResult> explanations;
    private final long evaluatedAt;

    public Decision(String transactionId, double finalRiskScore, FraudAction action, List<RuleEvaluationResult> explanations) {
        this.transactionId = transactionId;
        this.finalRiskScore = finalRiskScore;
        this.action = action;
        this.explanations = explanations;
        this.evaluatedAt = System.currentTimeMillis();
    }

    public String getTransactionId() { return transactionId; }
    public double getFinalRiskScore() { return finalRiskScore; }
    public FraudAction getAction() { return action; }
    public List<RuleEvaluationResult> getExplanations() { return explanations; }
    public long getEvaluatedAt() { return evaluatedAt; }
}

// Thread-safe rules engine utilizing copy-on-write hot-swappable arrays
class RulesEngine {
    private final AtomicReference<List<RuleWeight>> activeRules = new AtomicReference<>(new ArrayList<>());
    private double challengeThreshold = 35.0;
    private double blockThreshold = 75.0;

    public void updateRules(List<RuleWeight> newRules) {
        activeRules.set(Collections.unmodifiableList(new ArrayList<>(newRules)));
        System.out.println("[ENGINE] Rule set hot-swapped successfully.");
    }

    public void setThresholds(double challengeThreshold, double blockThreshold) {
        this.challengeThreshold = challengeThreshold;
        this.blockThreshold = blockThreshold;
    }

    public Decision evaluateTransaction(Transaction txn, Map<String, Object> context) {
        List<RuleWeight> rules = activeRules.get();
        List<RuleEvaluationResult> explanations = new ArrayList<>();
        
        double totalWeightedScore = 0.0;
        double sumWeights = 0.0;

        for (RuleWeight rw : rules) {
            double rawScore = rw.getRule().evaluate(txn, context);
            double contribution = rawScore * rw.getWeight();
            totalWeightedScore += contribution;
            sumWeights += rw.getWeight();
            
            explanations.add(new RuleEvaluationResult(rw.getRule().getName(), rawScore, contribution));
        }

        double finalScore = sumWeights > 0 ? (totalWeightedScore / sumWeights) : 0.0;
        finalScore = Math.min(100.0, Math.max(0.0, finalScore));

        FraudAction action = FraudAction.ALLOW;
        if (finalScore >= blockThreshold) {
            action = FraudAction.BLOCK;
        } else if (finalScore >= challengeThreshold) {
            action = FraudAction.CHALLENGE;
        }

        return new Decision(txn.getId(), finalScore, action, explanations);
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("=== INITIALIZING FRAUD ENGINE ===");
        RulesEngine engine = new RulesEngine();

        // Standard setup rules
        List<RuleWeight> initialRules = Arrays.asList(
            new RuleWeight(new HighAmountRule(5000.0), 3.0),
            new RuleWeight(new LocationAnomalyRule(), 2.0),
            new RuleWeight(new VelocityAnomalyRule(3, 10000), 4.0) // max 3 tx in 10s
        );
        engine.updateRules(initialRules);

        // Seed customer context
        Map<String, Object> customerContext = new ConcurrentHashMap<>();
        customerContext.put("last_location", "US");
        customerContext.put("transaction_timestamps", new CopyOnWriteArrayList<>(Arrays.asList(
            System.currentTimeMillis() - 8000,
            System.currentTimeMillis() - 5000,
            System.currentTimeMillis() - 2000
        )));

        System.out.println("\n=== 1. EVALUATING SUSPICIOUS HIGH-VELOCITY TRANSACTION ===");
        Transaction txn1 = new Transaction("tx-101", "u-99", 250.0, "US", "d-88", System.currentTimeMillis());
        Decision d1 = engine.evaluateTransaction(txn1, customerContext);
        
        System.out.println("Decision Result: " + d1.getAction() + " | Final Risk Score: " + d1.getFinalRiskScore());
        for (RuleEvaluationResult explanation : d1.getExplanations()) {
            System.out.println("  - Rule: " + explanation.getRuleName() + 
                               " | Raw Score: " + explanation.getRawScore() + 
                               " | Contribution: " + explanation.getWeightedContribution());
        }

        System.out.println("\n=== 2. EVALUATING MASSIVE AMOUNT & LOCATION ANOMALY TRANSACTION ===");
        // Add location check failure
        customerContext.put("last_location", "FR");
        Transaction txn2 = new Transaction("tx-102", "u-99", 7500.0, "US", "d-88", System.currentTimeMillis());
        Decision d2 = engine.evaluateTransaction(txn2, customerContext);
        System.out.println("Decision Result: " + d2.getAction() + " | Final Risk Score: " + d2.getFinalRiskScore());

        System.out.println("\n=== 3. HOT-SWAP TO LOOSER RISK TOLERANCE AND RE-EVALUATE ===");
        // Disable high amount check
        List<RuleWeight> looserRules = Arrays.asList(
            new RuleWeight(new LocationAnomalyRule(), 2.0)
        );
        engine.updateRules(looserRules);

        // Same tx-102 evaluated again
        Decision d2Swapped = engine.evaluateTransaction(txn2, customerContext);
        System.out.println("Post hot-swap Decision Result: " + d2Swapped.getAction() + 
                           " | Final Risk: " + d2Swapped.getFinalRiskScore());
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...