A backend service for validating financial transactions with deterministic scoring, risk classification, and reliable event publishing.
Introduction | Features | Tech Stack | Installation | Quick start | Usage | Known issues and limitations | Getting help | Contributing | License
This service exposes a set of REST API endpoints, with the primary endpoint responsible for scoring financial transactions and returning a validation result that includes a risk level and a final decision. The API is stateless, uses PostgreSQL for persistence, and provides OpenAPI documentation for straightforward integration by upstream systems.
- Extensible rule-based scoring pipeline with initial validation rules, including amount thresholds, nighttime activity, high-risk countries, high-risk merchant categories, velocity checks, and country mismatch detection.
- Persistent storage of transactions, validation results, and outbox events for audit and history tracking.
- REST API for querying validation history with pagination and filtering.
- OpenAPI specification and Swagger UI for interactive API documentation.
- Outbox-based message publishing for validation completed events.
- Java 25
- Spring Boot 3.5.x
- Spring Web (Tomcat)
- Spring Data JPA (Hibernate, HikariCP)
- Jackson
- Jakarta Bean Validation
- Maven
- PostgreSQL 17.x
- Liquibase
- Kafka
- Testcontainers
- Option A — Container run: Docker + Docker Compose
- Option B — Local run: Java 25, Maven (optional), PostgreSQL, Kafka
- Option C — Hybrid run: Java 25, Maven (optional), Docker + Docker Compose
Runs the application together with all required infrastructure in an isolated environment, with the application, database, and message broker running in separate containers.
docker compose up --buildFor this approach, PostgreSQL and Kafka must be running on the host machine, and the application connection parameters must be configured accordingly.
After the infrastructure is ready, run the application locally:
./mvnw spring-boot:runor, if Maven is installed globally:
mvn spring-boot:runPostgreSQL and Kafka are started in containers, while the application itself runs locally and connects to the containerized infrastructure.
Start PostgreSQL and Kafka using Docker Compose:
docker compose up -d postgres kafkaThen run the application locally:
./mvnw spring-boot:runor
mvn spring-boot:runRun the service and submit a transaction for validation:
curl -X POST "http://localhost:3000/api/v1/transaction/validate" \
-H "Content-Type: application/json" \
-d '{
"externalId": 1,
"userId": "c1c3a7c0-1c3b-4a2b-9e5a-7b3c1b2a9f11",
"merchantId": "9a1d6f88-0f93-4e58-9dd7-1c9d4a7d6c22",
"deviceId": "android-13-pixel-7",
"amount": 12000.00,
"currency": "USD",
"initialized": 1735689600,
"merchantCategory": "GROCERIES",
"channel": "WEB",
"ipAddress": "203.0.113.10",
"country": "NGA",
"cardFingerprint": "fp_9d82kdk29d"
}'Example response:
{
"id": 843,
"transactionId": 1021,
"score": 90,
"riskLevel": "HIGH",
"decision": "BLOCK",
"validationResults": [
{
"code": "VERY_HIGH_AMOUNT",
"description": "The transaction is above 9999 USD",
"scoreDelta": 60
},
{
"code": "HIGH_RISK_COUNTRY",
"description": "The transaction was made in high-risk country NGA",
"scoreDelta": 30
}
],
"createdAt": "2026-12-01T11:22:33Z",
"updatedAt": "2026-12-01T11:22:33Z"
}All endpoints are served under the /api context path.
POST /api/v1/transaction/validate- validate a transaction and return score, risk level, decision, and triggered validators.GET /api/v1/validation/{id}- fetch a validation result by id.GET /api/v1/validation- list validation results with optional filters (userId,riskLevel,decision,from,to,page,size).GET /api/v1/utility/health/liveness- liveness probe.GET /api/v1/utility/health/readiness- readiness probe.
OpenAPI and Swagger UI:
GET /api/v1/openapi- raw OpenAPI spec.GET /api/swagger- Swagger UI.
The default pipeline applies these validators and sums their score deltas:
- High amount: amount > 4999 adds +40.
- Very high amount: amount > 9999 adds +60.
- Nighttime: 02:00-05:00 UTC adds +15.
- High-risk country: country code is in
RiskCountrylist (ISO alpha-3) adds +30. - High-risk merchant: category in
GAMBLING,CRYPTO, orADULTadds +35. - Velocity: more than 3 validations in the last 5 minutes adds +25.
- Country mismatch: user's recent history shows current country < 50% of last 30 days adds +20.
Scores are mapped to risk levels and decisions using the configured thresholds:
properties.risk.threshold.low(default 30) =>LOW/ALLOWproperties.risk.threshold.medium(default 70) =>MEDIUM/REVIEW- Above the medium threshold =>
HIGH/BLOCK
Configuration lives under src/main/resources/application*.yaml. Key settings include:
- Server: port
3000, context path/api. - Database:
spring.datasource.*(overridden by Docker Compose env vars). - Kafka:
spring.kafka.*andproperties.kafka.*. - Scoring thresholds:
properties.risk.threshold.*.
To run with production settings:
SPRING_PROFILES_ACTIVE=prod \
DB_HOST=localhost \
DB_PORT=5432 \
DB_USER=postgres \
DB_PASSWORD=postgres \
./mvnw spring-boot:runEach validation writes an outbox row and the scheduled publisher ships it to Kafka (topic transaction.validation.completed). The consumer is optional and only logs messages when properties.kafka.consumer.enabled=true.
- No load or stress testing is included.
- Authentication and authorization are not implemented.
- Some rule thresholds are currently defined in code (e.g. amount limits, velocity window, country mismatch window).
- Kafka consumer logic is intentionally minimal and intended for demonstration purposes only.
If you encounter a bug or have a question about the project, please use the repository issue tracker or reach me out directly.
Pull requests are welcome. For substantial changes, please open an issue first to discuss the proposed approach.
This project is licensed under the MIT License. See the LICENSE file for details.