Solutions/keshavsen#98
Open
senkeshav wants to merge 1 commit into
Open
Conversation
There was a problem hiding this comment.
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 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 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 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 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" |
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.
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
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"Key constraints:
chk_wallet_balance_non_negative— database-level guard against negative balancesuq_idempotency_key— UNIQUE onidempotency_keyfor deduplicationchk_transfer_different_wallets— prevents self-transfers (from != to)uq_ledger_transfer_wallet_type— prevents duplicate ledger entries per transferIndexes:
idx_transfers_from_wallet,idx_transfers_to_wallet,idx_transfers_status,idx_ledger_entries_wallet,idx_ledger_entries_transferIdempotency 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]idempotency_key.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 ✓SELECT FOR UPDATEvia@Lock(PESSIMISTIC_WRITE)with 5-second lock timeout.TransactionTemplate(not@Transactional) so constraint violations can be caught outside the transaction boundary.CHECK (balance >= 0)provides a final guard against double-spend.How to Run
docker compose up -d./mvnw spring-boot:run -Dspring-boot.run.profiles=devhttp://localhost:8080How to Test
./mvnw test./mvnw test -Dtest="WalletTest,TransferTest"./mvnw test -Dtest="TransferServiceIntegrationTest,TransferControllerTest"./mvnw test -Dtest="ConcurrencyTest"Tradeoffs / Assumptions
PSQLExceptionconstraint names. Not portable to other databases without adaptation.R__) in a separate location, loaded only in the dev profile.Checklist