Skip to content

Implement wallet transfer service#90

Open
grawat465 wants to merge 2 commits into
Robustrade:mainfrom
grawat465:solution/govind-singh
Open

Implement wallet transfer service#90
grawat465 wants to merge 2 commits into
Robustrade:mainfrom
grawat465:solution/govind-singh

Conversation

@grawat465

Copy link
Copy Markdown

Wallet-to-wallet transfer service built on Spring Boot 3.2 + PostgreSQL. Supports idempotent transfers, double-entry ledger
recording, transfer state machine (PENDING → PROCESSED / FAILED), and safe concurrent execution via pessimistic row locking.

AI Disclosure

  1. Tool: Claude Code (Anthropic)

  2. How I used it: Directed Claude to produce a full implementation plan (schema, idempotency strategy, concurrency strategy,
    layering, testing strategy) before any code was written. Implemented layer by layer — migrations → domain → repository → service →
    handler → tests — running mvn test after each layer. Reviewed every diff before accepting it.

  3. Transcript: See docs/AI_SESSION_TRANSCRIPT.md in the repo.

Schema Design

Three tables: wallets, transfers, ledger_entries.

Key constraints:

  • wallets.balance NUMERIC(19,4) CHECK (balance >= 0) — balance can never go negative at DB level
  • transfers.idempotency_key UNIQUE — idempotency enforced durably in the database
  • transfers.amount CHECK (amount > 0) — positive amounts only
  • transfers.from_wallet_id <> to_wallet_id — self-transfers rejected at DB level
  • ledger_entries UNIQUE(transfer_id, entry_type) — exactly one DEBIT + one CREDIT per transfer
  • ledger_entries.transfer_id FOREIGN KEY — no orphan ledger rows ever possible

Idempotency Strategy

No separate idempotency_records table — the transfers row itself is the idempotency record. transfers.idempotency_key has a UNIQUE
constraint. A request_fingerprint column (SHA-256 of fromWalletId|toWalletId|amount) detects when the same key is reused with a
different payload (409 conflict vs replay).

Flow: look up key first (no lock held) → if absent, run locked transaction → if DuplicateKeyException from a concurrent duplicate,
re-read and return as replay. Durable across process restarts.

Concurrency Strategy

Pessimistic row locking (SELECT ... FOR UPDATE). Wallets are always locked in ascending UUID order regardless of transfer direction —
prevents deadlocks on opposite-direction concurrent transfers between the same wallet pair.

The transfer row is inserted after both wallet locks are held. Inserting before locking caused a real, reproducible Postgres deadlock
(implicit FOR KEY SHARE FK locks conflicting with FOR UPDATE upgrades under concurrency). Caught and fixed by the concurrency test
suite.

How to Run

docker run -e POSTGRES_PASSWORD=password -e POSTGRES_DB=wallet -p 5432:5432 postgres:16-alpine
mvn spring-boot:run

How to Test

mvn test

Tradeoffs / Assumptions

  • No separate idempotency_records table — fewer moving parts, no risk of records disagreeing
  • Insufficient funds = committed FAILED transfer (HTTP 201), not a 4xx — it's a business outcome
  • No wallet-creation endpoint — wallets pre-seeded via V2__seed_wallets.sql
  • Pessimistic over optimistic locking — wallet transfers are write-heavy; row locks give predictable blocking over retry storms

Checklist

  • Tests pass (23/23)
  • README or notes updated
  • PR description explains schema, idempotency, and concurrency

Complete implementation of wallet-to-wallet transfer API with idempotency,
double-entry ledger, transfer state machine, and concurrency safety via
pessimistic row locking with deadlock-safe lock ordering.

Co-Authored-By: Govind Singh <grawat465@gmail.com>
Copilot AI review requested due to automatic review settings June 18, 2026 09:54
@grawat465
grawat465 requested a review from amitlambakulu as a code owner June 18, 2026 09:54

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.

⚠️ Not ready to approve

There are correctness gaps around service-layer input validation and mismatches between documented run instructions and default datasource configuration that should be resolved before approval.

Pull request overview

Implements a wallet-to-wallet transfer service in Spring Boot with PostgreSQL persistence, including idempotent transfer creation, a transfer state machine, and double-entry ledger recording, plus integration tests and design documentation.

Changes:

  • Added PostgreSQL schema + seed data via Flyway migrations (wallets, transfers, ledger_entries) with key constraints for correctness.
  • Implemented handler/service/repository/domain layers for creating and reading transfers/wallets, including pessimistic locking and idempotency-key replay handling.
  • Added Testcontainers-backed integration tests and extensive design/API documentation.
File summaries
File Description
src/test/java/com/wallet/service/TransferServiceIntegrationTest.java Integration tests for transfer semantics, idempotency, and concurrency.
src/test/java/com/wallet/handler/TransferControllerIntegrationTest.java MockMvc integration tests for HTTP contract and status codes.
src/test/java/com/wallet/domain/WalletTest.java Unit tests for wallet balance comparison helper.
src/test/java/com/wallet/domain/TransferTest.java Unit tests for transfer state transitions and invariants.
src/test/java/com/wallet/AbstractIntegrationTest.java Base test configuration using Testcontainers Postgres + Spring Boot.
src/main/resources/db/migration/V2__seed_wallets.sql Seed wallets for manual testing/demo.
src/main/resources/db/migration/V1__init_schema.sql Initial schema with constraints for balances, idempotency, and ledger integrity.
src/main/resources/application.yml Spring Boot datasource/Flyway configuration defaults.
src/main/java/com/wallet/WalletTransferApplication.java Spring Boot application entrypoint.
src/main/java/com/wallet/service/WalletService.java Wallet read service for GET /wallets/{id}.
src/main/java/com/wallet/service/TransferService.java Orchestrates idempotency lookup/replay and delegates transactional execution.
src/main/java/com/wallet/service/TransferResult.java Service-layer result object distinguishing replay vs new execution.
src/main/java/com/wallet/service/TransferExecutor.java Transactional transfer execution with deterministic lock ordering and ledger writes.
src/main/java/com/wallet/repository/WalletRepository.java JDBC repository for wallet reads, FOR UPDATE locking, and balance updates.
src/main/java/com/wallet/repository/TransferRepository.java JDBC repository for transfer insert/lookup/status updates (idempotency backed by UNIQUE).
src/main/java/com/wallet/repository/LedgerEntryRepository.java JDBC repository for ledger entry insert and lookup by transfer id.
src/main/java/com/wallet/handler/WalletResponse.java HTTP response DTO for wallet reads.
src/main/java/com/wallet/handler/WalletController.java GET /wallets/{id} endpoint.
src/main/java/com/wallet/handler/TransferResponse.java HTTP response DTO for transfers.
src/main/java/com/wallet/handler/TransferRequest.java HTTP request DTO with bean validation rules.
src/main/java/com/wallet/handler/TransferController.java POST /transfers + GET /transfers/{id} endpoints with replay status handling.
src/main/java/com/wallet/handler/RequestIds.java Shared UUID parsing utility for controllers.
src/main/java/com/wallet/handler/GlobalExceptionHandler.java Centralized exception-to-HTTP mapping with consistent error response shape.
src/main/java/com/wallet/handler/ErrorResponse.java Standard error response body.
src/main/java/com/wallet/exception/WalletNotFoundException.java 404 error for missing wallet.
src/main/java/com/wallet/exception/TransferNotFoundException.java 404 error for missing transfer.
src/main/java/com/wallet/exception/InvalidTransferRequestException.java 400 error for invalid request inputs.
src/main/java/com/wallet/exception/IdempotencyConflictException.java 409 error for idempotency key reused with different payload.
src/main/java/com/wallet/domain/Wallet.java Wallet domain model with normalized money scale.
src/main/java/com/wallet/domain/TransferStatus.java Transfer status enum with transition guard.
src/main/java/com/wallet/domain/Transfer.java Transfer domain model + state machine enforcement.
src/main/java/com/wallet/domain/Money.java Central money normalization helper for fixed scale.
src/main/java/com/wallet/domain/LedgerEntry.java Ledger entry domain model enforcing positive amounts.
src/main/java/com/wallet/domain/EntryType.java Debit/credit enum.
pom.xml Maven build/dependency configuration (Spring Boot, JDBC, Flyway, Testcontainers, etc.).
docs/DESIGN.md Design notes explaining schema, idempotency, concurrency, and failure modes.
docs/API_OVERVIEW.md Detailed walkthrough of request lifecycle, API contract, and guarantees.
docs/AI_SESSION_TRANSCRIPT.md AI usage disclosure transcript/documentation.
CLAUDE.md Project conventions and implementation rules.
.gitignore Ignores build output (target/, *.class).

Copilot's findings

  • Files reviewed: 39/40 changed files
  • Comments generated: 3

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.

Comment thread src/main/java/com/wallet/service/TransferService.java
Comment thread src/main/resources/application.yml Outdated
- Add null/blank guards for idempotencyKey and wallet IDs in TransferService
- Align application.yml defaults with documented docker run command
- Add ledger entry assertions to concurrency test

Co-Authored-By: Govind Singh <grawat465@gmail.com>
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.

3 participants