Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Idempotent Transaction Ledger Service

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.


Key Features

  • Strict Idempotency: Guarantees that duplicate requests with the exact same transactionId modify the wallet balance exactly once, returning HTTP 409 Conflict on 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 ExecutorService and CountDownLatch demonstrating real concurrency handling.
  • Clean Architecture & Precision Math: Uses standard Spring Boot layered architecture and BigDecimal with 2 decimal scale for exact monetary calculations.

Tech Stack

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

Domain Model & Schema

┌─────────────────────────────────────────┐
│                 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                 │
└─────────────────────────────────────────┘

REST API Specification

Endpoint: Process Transaction

POST /api/v1/transactions/process

Request Headers

Content-Type: application/json

Request Payload

{
  "transactionId": "b7b4116b-c860-4596-ab61-7acd1d582e0f",
  "userId": "e8c3453a-c75d-4a1a-a0c1-484ac7e1d67f",
  "amount": 250.00,
  "type": "DEBIT"
}

Success Response (HTTP 200 OK)

{
  "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"
}

Error Responses

  • 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
    }

Concurrency & Idempotency Strategy

  1. Pessimistic Row-Level Lock: When a debit is initiated, the service acquires an exclusive lock on the Wallet table row (SELECT ... FOR UPDATE). All concurrent requests targeting the same wallet are serialized at the database level.
  2. 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 transactionId completed and committed while the current thread was waiting for the lock.
  3. Database Unique Constraint: transactions.transaction_id is indexed with a UNIQUE constraint, acting as an immutable guard against duplicate ledger entries.

How to Run the Project

Prerequisites

  • Java 21+
  • Apache Maven 3.9+ (or use IntelliJ's built-in Maven)

Run the Application

mvn spring-boot:run

The 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).


How to Run the Tests

Execute the full suite using Maven:

mvn clean test

Or open the project in IntelliJ IDEA, navigate to: src/test/java/com/ledger/integration/TransactionLedgerIntegrationTest.java Right-click and select Run 'TransactionLedgerIntegrationTest'.


Expected Test Scenarios & Results

The test suite contains 3 deterministic integration tests annotated with @DisplayName:

  1. 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.
  2. Idempotency Test:

    • @DisplayName("Sends 3 identical transactionIDs simultaneously. Ensures the balance is only deducted once.")
    • 3 concurrent worker threads fire the exact same transactionId simultaneously using a CountDownLatch.
    • 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.
  3. 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.

Design Trade-offs & Limitations

  • 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages