Skip to content

Solutions/keshavsen#98

Open
senkeshav wants to merge 1 commit into
Robustrade:mainfrom
senkeshav:keshav-assignment
Open

Solutions/keshavsen#98
senkeshav wants to merge 1 commit into
Robustrade:mainfrom
senkeshav:keshav-assignment

Conversation

@senkeshav

Copy link
Copy Markdown

Summary

A wallet-to-wallet transfer service built with Spring Boot 3.3, PostgreSQL, and Flyway. Supports atomic balance transfers between wallets with idempotency guarantees, double-entry ledger accounting, and pessimistic locking for concurrency control.

AI disclosure

  1. Tool used: Claude Code (Anthropic's CLI agent)
  2. How I use it: Iterative development — I describe the problem/requirement, review the generated code, request fixes for issues I spot, and use it for refactoring and test generation. I verify all logic and architectural decisions myself.
  3. Transcript: Not feasible to provide as a single artifact. I intentionally used separate Claude sessions for each major problem area (schema, idempotency, concurrency, ledger immutability, retries, observability, etc.) rather than one long conversation.

The reason is to avoid context pollution. Long-running sessions tend to reinforce earlier assumptions, while fresh sessions force the model to re-evaluate the problem from scratch and are more likely to surface flaws, edge cases, and alternative approaches. As a result, the work was produced across multiple independent Claude sessions rather than a single continuous transcript.

Schema Design

erDiagram
    wallets {
        VARCHAR(64) id PK
        BIGINT balance "CHECK >= 0"
        TIMESTAMPTZ created_at
        TIMESTAMPTZ updated_at
    }

    transfers {
        VARCHAR(64) id PK
        VARCHAR(128) idempotency_key UK "UNIQUE"
        VARCHAR(64) from_wallet_id FK
        VARCHAR(64) to_wallet_id FK
        BIGINT amount "CHECK > 0"
        VARCHAR(16) status "PENDING | PROCESSED | FAILED"
        BIGINT failure_balance "nullable, audit only"
        TIMESTAMPTZ created_at
        TIMESTAMPTZ updated_at
    }

    ledger_entries {
        BIGSERIAL id PK
        VARCHAR(64) wallet_id FK
        VARCHAR(64) transfer_id FK
        VARCHAR(8) entry_type "DEBIT | CREDIT"
        BIGINT amount "CHECK > 0"
        TIMESTAMPTZ created_at
    }

    wallets ||--o{ transfers : "from_wallet_id"
    wallets ||--o{ transfers : "to_wallet_id"
    wallets ||--o{ ledger_entries : "wallet_id"
    transfers ||--|| ledger_entries : "DEBIT entry"
    transfers ||--|| ledger_entries : "CREDIT entry"
Loading

Key constraints:

  • chk_wallet_balance_non_negative — database-level guard against negative balances
  • uq_idempotency_key — UNIQUE on idempotency_key for deduplication
  • chk_transfer_different_wallets — prevents self-transfers (from != to)
  • uq_ledger_transfer_wallet_type — prevents duplicate ledger entries per transfer

Indexes: idx_transfers_from_wallet, idx_transfers_to_wallet, idx_transfers_status, idx_ledger_entries_wallet, idx_ledger_entries_transfer

Idempotency Strategy

flowchart TD
    A[Client Request with idempotency_key] --> B{Key exists in DB?}
    B -->|Yes| C{Payload matches?}
    C -->|Yes| D[Return existing result]
    C -->|No| E[409 CONFLICT]
    B -->|No| F[Begin Transaction]
    F --> G[INSERT transfer record]
    G -->|Success| H[Execute transfer logic]
    G -->|UNIQUE violation| I{Race detected}
    I --> J[Lookup winner's record]
    J --> C
    H -->|Insufficient funds| K[Mark FAILED, commit]
    H -->|Success| L[Mark PROCESSED, commit]
    K --> M[Return 422 error]
    L --> N[Return 201 Created]
Loading
  • Each request carries a client-supplied idempotency_key.
  • Fast-path: Before starting a transaction, check if the key exists. If found, validate payload and return existing result.
  • Race-path: UNIQUE constraint ensures only one concurrent insert wins. The loser catches the constraint violation and returns the winner's result.
  • Payload mismatch: Reusing a key with different parameters returns 409 CONFLICT.
  • Failed transfers: A FAILED transfer permanently consumes its key. Clients must use a new key for retries.

Concurrency Strategy

sequenceDiagram
    participant C1 as Request 1
    participant C2 as Request 2
    participant DB as PostgreSQL

    Note over C1,C2: Both transfer FROM wallet_A TO wallet_B

    C1->>DB: BEGIN
    C2->>DB: BEGIN

    Note over DB: Locks acquired in deterministic order (wallet_A < wallet_B)

    C1->>DB: SELECT wallet_A FOR UPDATE ✓
    C2->>DB: SELECT wallet_A FOR UPDATE ⏳ (blocked)

    C1->>DB: SELECT wallet_B FOR UPDATE ✓
    C1->>DB: UPDATE wallet_A (debit)
    C1->>DB: UPDATE wallet_B (credit)
    C1->>DB: COMMIT ✓

    Note over C2: Lock released, proceeds
    C2->>DB: SELECT wallet_A FOR UPDATE ✓
    C2->>DB: SELECT wallet_B FOR UPDATE ✓
    C2->>DB: UPDATE wallet_A (debit)
    C2->>DB: COMMIT ✓
Loading
  • Pessimistic locking: SELECT FOR UPDATE via @Lock(PESSIMISTIC_WRITE) with 5-second lock timeout.
  • Deadlock prevention: Wallets locked in deterministic order (lexicographic ID). Eliminates deadlock cycles.
  • Transaction management: Explicit TransactionTemplate (not @Transactional) so constraint violations can be caught outside the transaction boundary.
  • Database safety net: CHECK (balance >= 0) provides a final guard against double-spend.

How to Run

  • Start PostgreSQL: docker compose up -d
  • Run the application: ./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
  • The service starts on http://localhost:8080

How to Test

  • Run all tests (uses Testcontainers — Docker required): ./mvnw test
  • Unit tests: ./mvnw test -Dtest="WalletTest,TransferTest"
  • Integration tests: ./mvnw test -Dtest="TransferServiceIntegrationTest,TransferControllerTest"
  • Concurrency tests: ./mvnw test -Dtest="ConcurrencyTest"

Tradeoffs / Assumptions

  • BIGINT for money: Balances stored in minor units (e.g., cents). Avoids floating-point but requires clients to handle conversion.
  • Pessimistic locking over optimistic: Chosen because wallet transfers are high-contention by nature. Optimistic locking would lead to excessive retries under load.
  • No retry on FAILED: Idempotency keys are permanently consumed on failure. This prevents accidental re-execution but requires clients to generate new keys for genuine retries.
  • PostgreSQL-specific: The idempotency race recovery inspects PSQLException constraint names. Not portable to other databases without adaptation.
  • No distributed locking: Single-database deployment assumed. Horizontal scaling would require advisory locks or a distributed coordinator.
  • Seed data separated: Dev seed data uses Flyway repeatable migrations (R__) in a separate location, loaded only in the dev profile.

Checklist

  • Tests pass
  • Format check passes
  • README or notes updated
  • PR description explains schema, idempotency, and concurrency

@senkeshav
senkeshav requested a review from amitlambakulu as a code owner June 21, 2026 17:39
Copilot AI review requested due to automatic review settings June 21, 2026 17:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a Spring Boot–based wallet transfer service with PostgreSQL persistence, Flyway-managed schema, idempotent transfer execution, pessimistic locking for concurrency safety, and double-entry ledger records, plus integration and concurrency tests using Testcontainers.

Changes:

  • Added core transfer execution flow (idempotency + locking + ledger + state transitions) and query endpoints.
  • Introduced PostgreSQL schema/migrations (wallets, transfers, ledger_entries) and environment configs (default/dev/test).
  • Added integration, controller, domain, and concurrency test suites backed by Testcontainers.

Reviewed changes

Copilot reviewed 42 out of 43 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/test/resources/application-test.yml Test profile Spring/Flyway/logging configuration
src/test/java/com/wallet/transfer/WalletTransferApplicationTest.java Context-load smoke test
src/test/java/com/wallet/transfer/service/TransferServiceIntegrationTest.java Integration coverage for transfer execution/idempotency/ledger/state
src/test/java/com/wallet/transfer/service/ConcurrencyTest.java Concurrency behavior tests (double-spend/idempotency races)
src/test/java/com/wallet/transfer/domain/WalletTest.java Wallet domain behavior tests (debit/credit guards)
src/test/java/com/wallet/transfer/domain/TransferTest.java Transfer state machine tests
src/test/java/com/wallet/transfer/controller/TransferControllerTest.java API-level tests for transfers/wallet queries and error mapping
src/test/java/com/wallet/transfer/BaseIntegrationTest.java Testcontainers PostgreSQL base setup for integration tests
src/main/resources/db/seed/R__seed_wallets.sql Repeatable seed data for local/dev usage
src/main/resources/db/migration/V1__init_schema.sql Initial schema for wallets/transfers/ledger_entries + constraints/indexes
src/main/resources/application.yml Default application configuration (datasource, Flyway, logging)
src/main/resources/application-dev.yml Dev-profile configuration overrides (Flyway seed location, logging)
src/main/java/com/wallet/transfer/WalletTransferApplication.java Spring Boot application entrypoint
src/main/java/com/wallet/transfer/service/WalletQueryService.java Read-only query service for transfers/wallets
src/main/java/com/wallet/transfer/service/TransferService.java Core transfer orchestration (idempotency, locking, ledger, state)
src/main/java/com/wallet/transfer/repository/WalletRepository.java Wallet JPA repo with pessimistic lock query + timeout hint
src/main/java/com/wallet/transfer/repository/TransferRepository.java Transfer JPA repo with idempotency lookup
src/main/java/com/wallet/transfer/repository/LedgerEntryRepository.java Ledger JPA repo queries by transfer/wallet
src/main/java/com/wallet/transfer/exception/WalletNotFoundException.java Not-found exception for wallet IDs
src/main/java/com/wallet/transfer/exception/InvalidTransferRequestException.java Bad-request exception for invalid transfer requests
src/main/java/com/wallet/transfer/exception/InsufficientFundsException.java Insufficient-funds exception used for 422 mapping
src/main/java/com/wallet/transfer/exception/IdempotencyKeyConflictException.java 409 conflict exception for idempotency payload mismatch
src/main/java/com/wallet/transfer/exception/GlobalExceptionHandler.java Centralized exception → HTTP response mapping
src/main/java/com/wallet/transfer/exception/BalanceViolationException.java Domain guard exception for balance invariants
src/main/java/com/wallet/transfer/dto/WalletResponse.java Wallet API response DTO
src/main/java/com/wallet/transfer/dto/TransferResult.java Internal transfer execution result wrapper (created vs replay)
src/main/java/com/wallet/transfer/dto/TransferResponse.java Transfer API response DTO
src/main/java/com/wallet/transfer/dto/TransferRequest.java Transfer API request DTO + bean validation
src/main/java/com/wallet/transfer/dto/ErrorResponse.java Standard API error payload
src/main/java/com/wallet/transfer/domain/Wallet.java Wallet entity + debit/credit domain logic
src/main/java/com/wallet/transfer/domain/TransferStatus.java Transfer status enum
src/main/java/com/wallet/transfer/domain/Transfer.java Transfer entity + state transitions
src/main/java/com/wallet/transfer/domain/LedgerEntryType.java Ledger entry type enum
src/main/java/com/wallet/transfer/domain/LedgerEntry.java Ledger entry entity
src/main/java/com/wallet/transfer/controller/TransferController.java REST endpoints for transfers and wallet balance queries
README.md Project overview, run/test instructions, and design notes
pom.xml Maven project definition + dependencies/plugins
mvnw.cmd Windows Maven wrapper script (custom)
mvnw Unix Maven wrapper script (custom)
docker-compose.yml Local PostgreSQL container definition
design.md Extended design documentation (HLD/LLD)
.mvn/wrapper/maven-wrapper.properties Maven distribution/wrapper URLs
.gitignore Java/Maven IDE/build output ignores

open-in-view: false
flyway:
enabled: true
locations: classpath:db/migration,classpath:db/seed
Comment thread README.md
Comment on lines +38 to +44
The application does not auto-seed wallets. You can insert them directly:

```sql
INSERT INTO wallets (id, balance, version, created_at, updated_at)
VALUES ('wallet_1', 10000, 0, NOW(), NOW()),
('wallet_2', 5000, 0, NOW(), NOW());
```
Comment thread README.md
Comment on lines +89 to +90
- **wallets** — stores current balance with a `CHECK (balance >= 0)` constraint as a safety net. Uses `version` column for optimistic locking detection.
- **transfers** — the transfer record with a `UNIQUE` constraint on `idempotency_key`. Status enum: `PENDING -> PROCESSED | FAILED`.
Comment on lines +72 to +77
* Design decision — FAILED transfers and idempotency keys:
* Once a transfer is marked FAILED (e.g. insufficient funds), the idempotency key is permanently
* consumed. Retrying with the same key will return the original FAILED result. This is intentional:
* it prevents a scenario where a client retries after depositing funds and inadvertently triggers
* a transfer that was already observed as failed. Clients should use a new idempotency key for
* genuinely new transfer attempts.
Comment on lines +74 to +81
try {
latch.await();
transferService.executeTransfer(
"concurrent-" + idx, "source", dest, 200L);
successCount.incrementAndGet();
} catch (Exception e) {
failCount.incrementAndGet();
}
Comment thread mvnw.cmd
Comment on lines +4 to +10
set MAVEN_CMD=mvn

if exist "%USERPROFILE%\.m2\wrapper\dists\apache-maven-3.9.9" (
for /f "delims=" %%i in ('dir /s /b "%USERPROFILE%\.m2\wrapper\dists\apache-maven-3.9.9\bin\mvn.cmd" 2^>nul') do set MAVEN_CMD=%%i
)

%MAVEN_CMD% %*
Comment thread mvnw
Comment on lines +19 to +25
if [ ! -d "$MAVEN_HOME" ]; then
echo "Downloading Maven..."
mkdir -p "$MAVEN_HOME"
TEMP_FILE=$(mktemp)
curl -fsSL "$DIST_URL" -o "$TEMP_FILE"
unzip -q "$TEMP_FILE" -d "$MAVEN_HOME"
rm "$TEMP_FILE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants