A production-grade, internal transaction ledger service built with Java 21, Spring Boot 3.3.4, and an in-memory H2 Database.
This service solves the classic double-spending and duplicate ingestion problems encountered when payment gateways or distributed webhooks send duplicate or concurrent debit requests.
- Strict Idempotency: Guarantees that duplicate requests with the exact same
transactionIdmodify the wallet balance exactly once, returning HTTP409 Conflicton duplicates. - Database-Level Pessimistic Locking: Prevents negative balances and race conditions across concurrent debit requests targeting the same wallet using
SELECT ... FOR UPDATE(@Lock(LockModeType.PESSIMISTIC_WRITE)). - Zero External Dependencies: Uses an in-memory H2 database. No Docker, Redis, MySQL, or PostgreSQL setup required. Runs directly out of the box in IntelliJ or via Maven CLI.
- Deterministic Concurrency Test Suite: Multi-threaded integration tests with
ExecutorServiceandCountDownLatchdemonstrating real concurrency handling. - Clean Architecture & Precision Math: Uses standard Spring Boot layered architecture and
BigDecimalwith 2 decimal scale for exact monetary calculations.
| Technology | Version / Specification |
|---|---|
| Language | Java 21 LTS |
| Framework | Spring Boot 3.3.4 |
| Persistence | Spring Data JPA / Hibernate |
| Database | H2 Database (In-Memory jdbc:h2:mem:ledgerdb) |
| Testing | JUnit 5, AssertJ, Spring Boot Test (TestRestTemplate) |
| Build Tool | Maven |
┌─────────────────────────────────────────┐
│ Wallet │
├─────────────────────────────────────────┤
│ id : UUID (PK) │
│ userId : UUID (Unique Index) │
│ balance : BigDecimal(19, 2) │
│ createdAt : Instant │
└─────────────────────────────────────────┘
▲
│ 1
│
│ *
┌─────────────────────────────────────────┐
│ Transaction │
├─────────────────────────────────────────┤
│ id : UUID (PK) │
│ transactionId : String (Unique Index) │
│ userId : UUID │
│ walletId : UUID │
│ amount : BigDecimal(19, 2) │
│ type : Enum (DEBIT) │
│ status : Enum (SUCCESS, FAILED) │
│ createdAt : Instant │
└─────────────────────────────────────────┘
POST /api/v1/transactions/process
Content-Type: application/json{
"transactionId": "b7b4116b-c860-4596-ab61-7acd1d582e0f",
"userId": "e8c3453a-c75d-4a1a-a0c1-484ac7e1d67f",
"amount": 250.00,
"type": "DEBIT"
}{
"transactionId": "b7b4116b-c860-4596-ab61-7acd1d582e0f",
"userId": "e8c3453a-c75d-4a1a-a0c1-484ac7e1d67f",
"walletId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"amount": 250.00,
"type": "DEBIT",
"status": "SUCCESS",
"remainingBalance": 750.00,
"createdAt": "2026-09-16T13:25:37.618Z"
}-
Duplicate / Idempotent Conflict (
HTTP 409 Conflict):{ "timestamp": "2026-09-16T13:25:37.628Z", "status": 409, "error": "Conflict", "message": "Transaction with ID 'b7b4116b-c860-4596-ab61-7acd1d582e0f' has already been processed.", "transactionId": "b7b4116b-c860-4596-ab61-7acd1d582e0f" } -
Insufficient Funds (
HTTP 422 Unprocessable Entity):{ "timestamp": "2026-09-16T13:25:37.832Z", "status": 422, "error": "Unprocessable Entity", "message": "Insufficient funds for user 8e135dcb-498f-4079-b5be-5c3cc4db089d: current balance is 0.00, requested debit is 100.00", "transactionId": null } -
Bad Request / Validation Failure (
HTTP 400 Bad Request):{ "timestamp": "2026-09-16T13:25:37.900Z", "status": 400, "error": "Bad Request", "message": "amount: amount must be greater than zero", "transactionId": null }
- Pessimistic Row-Level Lock: When a debit is initiated, the service acquires an exclusive lock on the
Wallettable row (SELECT ... FOR UPDATE). All concurrent requests targeting the same wallet are serialized at the database level. - Double-Checked Idempotency:
- Fast check before lock acquisition (
existsByTransactionId) allows stale retries to exit without lock overhead. - Re-check after lock acquisition guarantees that no competing request with the same
transactionIdcompleted and committed while the current thread was waiting for the lock.
- Fast check before lock acquisition (
- Database Unique Constraint:
transactions.transaction_idis indexed with aUNIQUEconstraint, acting as an immutable guard against duplicate ledger entries.
- Java 21+
- Apache Maven 3.9+ (or use IntelliJ's built-in Maven)
mvn spring-boot:runThe server starts on port 8080.
H2 Console is accessible at: http://localhost:8080/h2-console (JDBC URL: jdbc:h2:mem:ledgerdb, Username: sa, Password: empty).
Execute the full suite using Maven:
mvn clean testOr open the project in IntelliJ IDEA, navigate to:
src/test/java/com/ledger/integration/TransactionLedgerIntegrationTest.java
Right-click and select Run 'TransactionLedgerIntegrationTest'.
The test suite contains 3 deterministic integration tests annotated with @DisplayName:
-
Happy Path Test:
@DisplayName("Processes a single valid debit transaction successfully.")- Verifies ₹250.00 debit from ₹1000.00 balance leaves exactly ₹750.00, returns HTTP 200, and persists 1 transaction.
-
Idempotency Test:
@DisplayName("Sends 3 identical transactionIDs simultaneously. Ensures the balance is only deducted once.")- 3 concurrent worker threads fire the exact same
transactionIdsimultaneously using aCountDownLatch. - Verifies exactly 1 request returns HTTP 200, 2 requests return HTTP 409, balance is deducted exactly once (₹750.00), and only 1 transaction record exists.
-
Race Condition Test:
@DisplayName("Sends 10 concurrent debit requests of ₹100 for a wallet with a ₹500 balance. Ensures the final balance is exactly ₹0 and 5 requests fail with insufficient funds.")- 10 concurrent threads fire distinct ₹100 debit requests on a ₹500 wallet simultaneously using a
CountDownLatch. - Verifies exactly 5 requests succeed (HTTP 200), exactly 5 requests fail with insufficient funds (HTTP 422), final balance is ₹0.00, and negative balance is never observed.
- H2 In-Memory Lock Granularity: In H2 MVStore mode, pessimistic row locks are supported, but in-memory table locking characteristics differ from production RDBMS engines like PostgreSQL (which support MVCC with row-level locks,
SKIP LOCKED, etc.). For this assignment, H2 provides zero-setup, high-speed deterministic execution. - Pessimistic vs. Optimistic Locking: Pessimistic locking creates lock contention under high volume on the same wallet row. For financial ledgers where debit order and balance accuracy are strictly critical, queuing requests via row locks is preferable to high optimistic abort rates.