A production-grade fintech payment ledger system built with Java 17 and Spring Boot 3.x, featuring strict ACID compliance, double-entry accounting, and distributed transaction support.
Hexagonal Architecture (Ports and Adapters) with clean service-layer separation
- Java 17
- Spring Boot 3.2.0 (Web, Data JPA, Validation, Kafka, Redis)
- PostgreSQL 15 - Relational database with strict integrity
- Apache Kafka - Event streaming for asynchronous reconciliation
- Redis 7 - Distributed caching and idempotency
- Docker Compose - Local infrastructure orchestration
Every transaction records two ledger entries (DEBIT and CREDIT) in a single atomic database transaction. The sum of debits must equal the sum of credits.
- JPA
@Versionannotation on Account entity REPEATABLE_READtransaction isolation level- Exponential backoff retry (max 3 attempts) on
ObjectOptimisticLockingFailureException - Prevents "Lost Update" anomalies during concurrent balance updates
- Redis-backed cache with 24-hour TTL
- Checks
X-Idempotency-Keyheader before processing - Returns cached response for duplicate requests
State machine: PENDING β AUTHORIZED β CAPTURED (or FAILED)
- Uses
@TransactionalEventListener(phase = AFTER_COMMIT) - Publishes
TransactionSettledEventto Kafka only after successful DB commit - Prevents message loss or duplicate processing
βββββββββββββββ ββββββββββββββββββββ ββββββββββββββββ
β Account β β Transaction β β LedgerEntry β
βββββββββββββββ€ ββββββββββββββββββββ€ ββββββββββββββββ€
β id (PK) ββββββ β id (PK) β βββββ id (PK) β
β user_id β β β reference_id (UK)β β β transaction β
β currency β β β amount β β β account β
β balance β β β currency β β β direction β
β version β ββββ<β source_account β β β amount β
β created_at β β dest_account β β β created_at β
β updated_at β ββ<β status β>ββββ ββββββββββββββββ
βββββββββββββββ β β idempotency_key β
β β created_at β
β β updated_at β
β ββββββββββββββββββββ
β
βββ Foreign Key
- Java 17+
- Maven 3.8+
- Docker & Docker Compose
cd payment-ledger
docker-compose up -dThis starts:
- PostgreSQL on port 5432
- Redis on port 6379
- Kafka on port 9092
- Zookeeper on port 2181
./mvnw clean install./mvnw spring-boot:runApplication will start on http://localhost:8080
POST /api/v1/transfers
Headers:
Content-Type: application/json
X-Idempotency-Key: unique-key-123
Body:
{
"sourceAccountId": 1,
"destinationAccountId": 2,
"amount": 100.50,
"currency": "USD",
"description": "Payment for invoice #12345"
}
Response (200 OK):
{
"transactionId": 1,
"referenceId": "a1b2c3d4-e5f6-...",
"amount": 100.50,
"currency": "USD",
"status": "CAPTURED",
"sourceAccountId": 1,
"destinationAccountId": 2,
"description": "Payment for invoice #12345",
"createdAt": "2025-11-27T00:15:00",
"updatedAt": "2025-11-27T00:15:00"
}GET /api/v1/transfers/{transactionId}GET /api/v1/transfers/reference/{referenceId}@Transactional(isolation = Isolation.REPEATABLE_READ)
public TransactionResponse executeTransfer(TransferRequest request, String idempotencyKey) {
// Optimistic locking prevents concurrent modification
Account sourceAccount = accountRepository.findById(sourceAccountId).orElseThrow();
Account destinationAccount = accountRepository.findById(destinationAccountId).orElseThrow();
// Version field automatically checked by JPA
sourceAccount.debit(amount);
destinationAccount.credit(amount);
// If version mismatch: ObjectOptimisticLockingFailureException β retry
}BigDecimal totalDebits = ledgerEntryRepository.sumByTransactionIdAndDirection(
transactionId, EntryDirection.DEBIT);
BigDecimal totalCredits = ledgerEntryRepository.sumByTransactionIdAndDirection(
transactionId, EntryDirection.CREDIT);
if (totalDebits.compareTo(totalCredits) != 0) {
throw new DoubleEntryViolationException("Debits must equal credits");
}Edit src/main/resources/application.yml:
idempotency:
cache-ttl-hours: 24
retry:
max-attempts: 3
initial-delay-ms: 100
multiplier: 2.0
kafka:
topics:
ledger-events: ledger.events| Error | HTTP Status | Description |
|---|---|---|
InsufficientBalanceException |
422 | Source account lacks funds |
AccountNotFoundException |
404 | Account ID not found |
DuplicateTransactionException |
409 | Idempotency key already exists |
OptimisticLockingFailureException |
409 | Concurrent modification after retries |
DoubleEntryViolationException |
500 | CRITICAL: Accounting rules violated |
MissingRequestHeaderException |
400 | X-Idempotency-Key header missing |
Health check endpoint:
curl http://localhost:8080/actuator/healthAuto-created by Hibernate on startup (spring.jpa.hibernate.ddl-auto=update)
Auto-created by KafkaConfig:
ledger.events(3 partitions, replication factor 1)
Format: idempotency:{key} with 24-hour TTL
This is a demonstration project for educational purposes.
This is a reference implementation for fintech payment systems. Feel free to adapt for your use case.
Built with β€οΈ for bank-grade reliability