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