Implement wallet transfer service#90
Open
grawat465 wants to merge 2 commits into
Open
Conversation
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>
There was a problem hiding this comment.
⚠️ 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.
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Tool: Claude Code (Anthropic)
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.
Transcript: See docs/AI_SESSION_TRANSCRIPT.md in the repo.
Schema Design
Three tables: wallets, transfers, ledger_entries.
Key constraints:
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
Checklist