Machine Coding Problem

Fraud Detection Rules Engine

maco60macoAllsecuritypredicate-logicthresholds
Commonly Asked By:StripePayPalCoinbaseRobinhood

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.

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());
    }
}