A production-grade Payment Processing Backend built with Java 21 and Spring Boot 3.
Implements core financial system concepts - atomic transfers, fraud detection, double-entry ledger, and daily reconciliation.
Most payment systems fail under three conditions - concurrent requests , duplicate submissions , and fraudulent patterns . PayFlow handles all three through a strict, auditable pipeline.
Incoming Request
│
▼
┌─────────────────┐
│ JWT Auth │──── Invalid Token ──── 401 Unauthorized
└────────┬────────┘
│
▼
┌─────────────────────┐
│ Idempotency Check │──── Already Processed ──── Return Same Response
└────────┬────────────┘
│
▼
┌──────────────────────┐
│ Pessimistic Lock │──── Row-level DB lock acquired
└────────┬─────────────┘
│
▼
┌──────────────────────┐
│ Fraud Detection │──── Rule Triggered ──── BLOCKED + Alert Saved
└────────┬─────────────┘
│
▼
┌──────────────────────┐
│ Balance Check │──── Insufficient ──── FAILED + Record Saved
└────────┬─────────────┘
│
▼
┌──────────────────────┐
│ Atomic Debit+Credit │
└────────┬─────────────┘
│
▼
┌──────────────────────┐
│ Double Entry Ledger │──── DEBIT entry + CREDIT entry saved
└────────┬─────────────┘
│
▼
Response
1. Authentication and Account System
Stateless JWT authentication - no session, no state
BCrypt password encoding
Automatic SAVINGS account creation on registration - same @Transactional boundary as user creation
Account status management - ACTIVE, SUSPENDED, CLOSED
Atomic transfers - debit and credit in one database transaction. Server crash mid-transfer rolls back everything
Pessimistic locking - SELECT ... FOR UPDATE before any balance change. Two concurrent requests are serialized
Idempotency - same idempotencyKey sent twice returns the same response. Implemented via MySQL unique constraint, no Redis needed
Failed payments are recorded, not discarded - full failure reason stored
3. Fraud Detection Rules Engine
Rules live in the database , not in code
AMOUNT_THRESHOLD - block transactions above a configured amount
FREQUENCY_LIMIT - block if N+ transactions in M minutes from same account
Add a new rule via POST request - zero code change, zero redeployment
Every blocked transaction creates a FraudAlert record
Every successful payment creates exactly two ledger entries - DEBIT and CREDIT
balanceAfter tracked on every entry
Point-in-time balance query - reconstruct balance at any historical date
Same accounting principle used in every bank globally
Scheduled daily at midnight via @Scheduled(cron = "0 0 0 * * *")
Verifies Total DEBIT == Total CREDIT across all ledger entries
Logs ERROR on mismatch - ready to connect to any alerting system
Every run saved as ReconciliationReport - permanent audit history
Manual trigger API available for testing
Method
Endpoint
Auth
Description
POST
/api/auth/register
No
Register new user
POST
/api/auth/login
No
Login, receive JWT
Method
Endpoint
Auth
Description
GET
/api/accounts/my
Yes
Get own accounts
Method
Endpoint
Auth
Description
POST
/api/payments/transfer
Yes
Transfer money
GET
/api/payments/history/{accountNumber}
Yes
Transaction history
Method
Endpoint
Auth
Description
POST
/api/fraud-rules
Yes
Create rule
GET
/api/fraud-rules
Yes
List all rules
PUT
/api/fraud-rules/{id}/deactivate
Yes
Deactivate rule
Method
Endpoint
Auth
Description
GET
/api/ledger/{accountNumber}
Yes
Full ledger history
GET
/api/ledger/{accountNumber}/balance-as-of
Yes
Point-in-time balance
Method
Endpoint
Auth
Description
POST
/api/reconciliation/run
Yes
Trigger manually
GET
/api/reconciliation/reports
Yes
All reports
GET
/api/reconciliation/reports/mismatches
Yes
Mismatch reports only
Prerequisites: Docker Desktop installed
git clone https://github.com/ArpanC6/payflow.git
cd payflow
mvn clean package -DskipTests
docker-compose up --build
URL
Application
http://localhost:8081
Swagger UI
http://localhost:8081/swagger-ui.html
Key Engineering Decisions
Decision
Reasoning
BigDecimal over double
Floating point loses precision - 0.1 + 0.2 != 0.3 in Java. BigDecimal is exact
Pessimistic over optimistic locking
Payment systems cannot retry on conflict - money must not be debited twice
Idempotency via MySQL, not Redis
Same correctness guarantee, one less dependency
DB-driven fraud rules
New rule = one API call. No code change, no redeploy, no downtime
Double-entry accounting
Fundamental invariant: debits always equal credits. Violations caught daily
Immutable transaction log
Never updated or deleted. Audit trail is permanent and tamper-proof
src/main/java/com/arpan/payflow/
├── config/ Security configuration
├── controller/ REST endpoints
├── dto/ Request and response objects
├── entity/ JPA entities
├── exception/ Global exception handler
├── job/ Scheduled reconciliation job
├── repository/ Database queries
├── security/ JWT filter and utility
└── service/ Business logic
Unit tests written with JUnit 5 and Mockito , covering critical business logic in isolation.
Test Class
Coverage
PaymentServiceTest
Successful transfer, insufficient balance, idempotent duplicate requests, fraud-blocked transactions, same-account transfer rejection, inactive account rejection
FraudDetectionServiceTest
Amount threshold triggering, frequency limit triggering, transactions within limits, no active rules scenario
ReconciliationServiceTest
Balanced ledger detection, mismatch detection, null-safe handling of empty ledger
Run tests:
Entity
Purpose
User
Registered user with KYC status
Account
Bank account linked to user
Transaction
Payment record with idempotency key
FraudRule
Configurable fraud detection rule
FraudAlert
Alert created when a rule is triggered
LedgerEntry
Individual debit or credit entry
ReconciliationReport
Daily reconciliation run result