Fraud Decision Engine is a portfolio-ready, local-first Java 21 + Spring Boot application that simulates how a fintech backend could evaluate transactions in real time. It accepts payment-like requests, applies configurable antifraud rules, routes through a versioned decision flow, calculates a risk score, returns a final decision, and persists the full audit trail needed to explain why the outcome happened.
The project stays intentionally practical:
- one Spring Boot application
- zero mandatory cloud services
- H2 by default for frictionless local startup
- PostgreSQL profile available for a more production-like run
- a built-in browser dashboard for realistic demos without a separate frontend stack
This repo is designed to show backend engineering depth, not just API scaffolding.
It demonstrates:
- clean package separation across HTTP, orchestration, rules, persistence, audit, and events
- configurable rule weights, flags, thresholds, and versioned decision flow nodes
- idempotent request handling with replay for duplicate
transactionIdsubmissions - transactional outbox persistence with a stable
TransactionEvaluatedV1schema - structured logs, correlation/trace propagation, Micrometer metrics, and latency percentiles
- a polished local UX so the system can be explored like an internal risk tooling product
POST /api/v1/fraud/evaluatefor real-time fraud evaluationGET /modern local dashboard for demos, testing, and exploration- versioned rule configuration stored in the database
- versioned decision flow stored in the database
- idempotent duplicate handling with deterministic replay
- conflict detection for reused
transactionIdwith different payloads - H2 + Flyway local runtime, PostgreSQL profile ready
- structured JSON errors
- structured ECS-style logs
- Actuator + Micrometer metrics
- evaluation timing, per-rule timing, and latency percentile visibility
- local outbox table and versioned event envelope
- Postman collection and curl examples included
- Architecture notes: docs/architecture.md
- Postman collection: postman/fraud-decision-engine.postman_collection.json
- Maven wrapper included for zero-setup startup
- Java 21
- Spring Boot 3.5
- Maven + Maven Wrapper
- Spring Web
- Spring Validation
- Spring Data JPA
- Flyway
- H2 Database
- PostgreSQL driver
- Spring Boot Actuator
- Micrometer
- JUnit 5 + Mockito
The repo is meant to feel complete when cloned locally:
- start the service with one command
- open
http://localhost:8080/ - run scenarios through a modern SaaS-style dashboard
- inspect decisions, events, configuration, health, and metrics
- tweak runtime config in H2 without recompiling Java
There is no separate frontend build step. The dashboard is a static UI served directly by Spring Boot.
The codebase is organized by responsibility:
controller: REST endpoints and UI entrypointdto: request/response contractsapplication/service: orchestration, evaluation, queries, metrics, and cachingdomain: core decision models and enumsflow: versioned decision flow models and execution tracerules: isolated antifraud rule implementationsentity: JPA persistence modelsrepository: Spring Data + query repositoriesmapper: mapping between entities, domain, and DTOsaudit: audit record creation and structured reasoningevent: event envelope, outbox payload, and publication metadataexception: global error handling and custom exceptionsconfig: application configuration, seed data, and runtime wiring
flowchart LR
User[Browser / Postman / curl]
subgraph App[Fraud Decision Engine]
Dashboard[Static Dashboard]
InfoApi[API Info]
EvalApi[FraudEvaluationController]
ConfigApi[FraudConfigurationController]
EvalService[FraudEvaluationService]
FlowEngine[DecisionFlowEngine]
Rules[Fraud Rules]
QueryServices[Decision / Event / Config Query Services]
Audit[FraudAuditService]
Metrics[Micrometer + Actuator]
Outbox[FraudEventService]
Cache[Local Cache]
end
subgraph DB[(H2 Local / PostgreSQL Profile)]
Transactions[(transactions)]
Decisions[(fraud_decisions)]
Events[(fraud_events)]
KnownDevices[(known_devices)]
BlacklistedIps[(blacklisted_ips)]
RuleConfigs[(rule_configurations)]
Thresholds[(decision_threshold_configurations)]
FlowConfigs[(decision_flow_configurations)]
Flyway[(flyway_schema_history)]
end
User --> Dashboard
User --> InfoApi
User --> EvalApi
User --> ConfigApi
EvalApi --> EvalService
ConfigApi --> QueryServices
InfoApi --> QueryServices
EvalService --> Cache
EvalService --> FlowEngine
FlowEngine --> Rules
EvalService --> Audit
EvalService --> Metrics
EvalService --> Outbox
EvalService --> Transactions
EvalService --> Decisions
Outbox --> Events
QueryServices --> Events
QueryServices --> Decisions
QueryServices --> RuleConfigs
QueryServices --> Thresholds
QueryServices --> FlowConfigs
Cache --> KnownDevices
Cache --> BlacklistedIps
More implementation notes are available in docs/architecture.md.
The current seeded rule set includes:
HIGH_AMOUNT- adds the configured weight when
amount > amountThreshold
- adds the configured weight when
NEW_DEVICE- adds the configured weight when the
deviceIdwas not seen before for the sameuserId
- adds the configured weight when the
FOREIGN_COUNTRY- adds the configured weight when
country != homeCountry
- adds the configured weight when
RAPID_REPEAT_TRANSACTIONS- adds the configured weight when the user already crossed the configured velocity threshold in the configured time window
BLACKLISTED_IP- adds the configured weight when the IP is found in the suspicious IP table
MISSING_ATA_DOCUMENTATION- adds the configured weight when the decision flow routes an
ATAtransaction through documentary checks and the required flag is absent
- adds the configured weight when the decision flow routes an
score >= rejectThreshold->REJECTscore >= reviewThresholdand< rejectThreshold->REVIEWscore < reviewThreshold->APPROVE
The system does not hardcode a single flat execution path anymore.
The active flow is versioned and stored in the database. The default seeded flow:
- routes by
customerType - sends
ATAtraffic through documentary checks first - runs shared base fraud rules
- computes the final decision through score thresholds
That means customer-segment branching can evolve by changing the active flow configuration, without recompiling Java, as long as the engine is still using supported node types and existing rule identifiers.
You can change behavior locally without touching code:
rule_configurations- enable or disable rules
- change weights
- edit rule parameters
decision_threshold_configurations- change review and reject thresholds
decision_flow_configurations- activate another flow version
- replace the JSON definition for the active flow
Local H2 console:
http://localhost:8080/h2-console
The project persists:
transactionsfraud_decisionsfraud_eventsknown_devicesblacklisted_ipsrule_configurationsdecision_threshold_configurationsdecision_flow_configurations
Flyway manages schema creation under src/main/resources/db/migration, and Hibernate runs in validation mode rather than mutating the schema on startup.
Windows:
.\mvnw.cmd spring-boot:runmacOS / Linux:
./mvnw spring-boot:runThen open:
- Dashboard:
http://localhost:8080/ - API info:
http://localhost:8080/api/v1/info - Health:
http://localhost:8080/actuator/health - Metrics:
http://localhost:8080/actuator/metrics - H2 Console:
http://localhost:8080/h2-console
.\mvnw.cmd spring-boot:run -Dspring-boot.run.profiles=postgresEnvironment variables:
FRAUD_DB_URLFRAUD_DB_USERNAMEFRAUD_DB_PASSWORD
Windows:
.\mvnw.cmd testmacOS / Linux:
./mvnw testThe built-in dashboard is intentionally part of the product story, not an afterthought.
It includes:
- scenario presets for approve, review, reject, ATA, and velocity cases
- replay and conflict actions to demonstrate idempotency behavior
- modern decision summary with risk score, reasons, latency, correlation id, and trace id
- rule trace and decision flow trace pulled from the persisted
TransactionEvaluatedV1event - recent decisions and outbox events
- operational snapshot cards for health, latency percentiles, and slowest rules
- live view of active thresholds, rules, and decision flow nodes
This makes the repo far easier to demo than raw JSON alone while still keeping the architecture as a single backend app.
POST /api/v1/fraud/evaluate
Example request:
{
"transactionId": "tx-1001",
"userId": "user-55",
"amount": 250000,
"currency": "ARS",
"merchantId": "MERC-01",
"channel": "WEB",
"country": "AR",
"deviceId": "dev-888",
"ipAddress": "192.168.0.10"
}Example response:
{
"transactionId": "tx-1001",
"configurationVersion": 1,
"flowId": "customer-segment-fraud-flow",
"flowVersion": 1,
"decision": "REVIEW",
"riskScore": 50,
"reasons": [
"HIGH_AMOUNT",
"NEW_DEVICE"
],
"evaluatedAt": "2026-04-15T10:22:00",
"evaluationTimeMs": 14
}Optional routing fields:
customerType: defaults toSTANDARDdocumentFlags: optional object, for example{ "ATA_DOCUMENTATION": false }
GET /-> dashboardGET /api/v1/info-> project/about metadata and endpoint discoveryPOST /api/v1/fraud/evaluate-> evaluate one transactionGET /api/v1/fraud/decisions/{transactionId}-> fetch one persisted decisionGET /api/v1/fraud/decisions?page=0&size=20-> list recent decisionsGET /api/v1/fraud/events/{eventId}-> fetch one stored outbox eventGET /api/v1/fraud/events?status=PENDING&page=0&size=20-> list outbox eventsGET /api/v1/fraud/transactions/{transactionId}/latest-event-> fetch latest event for one transactionGET /api/v1/fraud/configuration/active-> inspect active thresholds, rules, and flow definitionGET /actuator/health-> health endpointGET /actuator/metrics-> discover metricsGET /actuator/caches-> inspect local caches
Evaluate a transaction:
curl -X POST http://localhost:8080/api/v1/fraud/evaluate \
-H "Content-Type: application/json" \
-H "X-Correlation-Id: demo-curl-001" \
-d '{
"transactionId":"tx-1001",
"userId":"user-55",
"amount":250000,
"currency":"ARS",
"merchantId":"MERC-01",
"channel":"WEB",
"country":"AR",
"deviceId":"dev-888",
"ipAddress":"192.168.0.10"
}'Get a stored decision:
curl http://localhost:8080/api/v1/fraud/decisions/tx-1001Get the latest event for a transaction:
curl http://localhost:8080/api/v1/fraud/transactions/tx-1001/latest-eventInspect active configuration:
curl http://localhost:8080/api/v1/fraud/configuration/activeCheck health:
curl http://localhost:8080/actuator/healthInspect latency metrics:
curl http://localhost:8080/actuator/metrics/fraud.evaluation.durationEach evaluation records:
- the received request
- the configuration version and flow version used
- rule-by-rule evaluation details
- final score and decision
- evaluation duration
- correlation id and trace id
- a versioned outbox event
Structured JSON logs are enabled through Spring Boot structured logging support.
Micrometer / Actuator expose, among others:
fraud.evaluation.durationfraud.evaluation.phase.durationfraud.evaluation.totalfraud.evaluation.replay.totalfraud.rule.evaluations.totalfraud.rule.triggered.totalfraud.rule.durationfraud.flow.execution.totalfraud.flow.node.executions.total
Latency percentiles are available for the evaluation timer:
- p50
- p95
- p99
Performance is part of the project, not a footnote.
Built-in instrumentation covers:
- total evaluation latency via
evaluationTimeMs - per-rule execution time in microseconds
- per-phase latency metrics
- slow-rule and slow-evaluation structured warning logs
- local caches for high-frequency lookups
.\mvnw.cmd "-Dfraud.benchmark.enabled=true" "-Dfraud.benchmark.iterations=500" "-Dfraud.benchmark.warmup=100" "-Dtest=FraudEvaluationBenchmarkTest" testThis benchmark is intentionally lightweight. It is useful as a local code-path baseline, not as an end-to-end SLA.
- Flyway-managed schema instead of
ddl-auto=update - local cache for active configuration and frequent lookups
- per-rule latency measurement
- per-phase latency measurement
- optimized raw SQL path for the rapid repeat lookup
- simple transactional outbox kept local but future-ready
Events are stored in fraud_events using a local transactional outbox pattern.
The persisted event envelope is stable and versioned:
{
"metadata": {
"eventId": "7d3d0c27-3167-4c35-8f2e-1a13cb0b7d5d",
"eventType": "TransactionEvaluatedV1",
"eventVersion": 1,
"aggregateType": "TRANSACTION",
"aggregateId": "tx-1001",
"occurredAt": "2026-04-15T10:22:00",
"correlationId": "corr-123",
"traceId": "trace-456"
},
"payload": {
"transactionId": "tx-1001",
"userId": "user-55",
"configurationVersion": 1,
"flowId": "customer-segment-fraud-flow",
"flowVersion": 1,
"reviewThreshold": 40,
"rejectThreshold": 70,
"riskScore": 72,
"decision": "REJECT",
"reasons": ["HIGH_AMOUNT", "NEW_DEVICE"],
"ruleEvaluations": [],
"decisionFlowTrace": {
"flowId": "customer-segment-fraud-flow",
"flowVersion": 1,
"steps": []
},
"evaluatedAt": "2026-04-15T10:22:00",
"evaluationTimeMs": 14
}
}Current publication states:
PENDINGPUBLISHEDFAILED
- the engine computes a deterministic fingerprint from the normalized request
- same
transactionId+ same payload -> replay the persisted decision - same
transactionId+ different payload ->409 Conflict - concurrent requests for the same
transactionIdare serialized inside this single-node app through an in-memory execution lock
This is a good local and monolith-friendly design, while still making the next production step obvious.
- Monolith over microservices: chosen to keep local setup simple and keep the focus on backend design quality.
- H2 over PostgreSQL by default: chosen for zero-friction startup, while PostgreSQL remains available as a profile.
- Flyway over
ddl-automutation: chosen so schema evolution is explicit and reviewable. - Database outbox over Kafka/RabbitMQ: chosen to keep async-readiness without introducing infrastructure noise.
- Local cache over external cache infrastructure: chosen to improve hot-path lookups without adding Redis in the first version.
- In-memory idempotency lock over distributed coordination: intentionally sufficient for a single-node demo, but not the final production answer.
- move PostgreSQL to the default runtime and validate the rapid repeat query plan with production-like data
- introduce a background outbox publisher and connect it to Kafka, SNS/SQS, or another transport
- replace in-memory idempotency locking with a multi-instance-safe strategy
- add authentication, authorization, and rate limiting
- expose OpenAPI documentation
- wire OpenTelemetry for distributed tracing export
- add alerting and dashboards around latency, errors, replay rate, and slow rules
- expand the fraud model with merchant risk, behavioral features, and manual review workflow
- add load testing against a production-like database profile