Skip to content

Solution/bhavik patel - #130

Open
Bhavik237 wants to merge 9 commits into
Robustrade:mainfrom
Bhavik237:main
Open

Solution/bhavik patel#130
Bhavik237 wants to merge 9 commits into
Robustrade:mainfrom
Bhavik237:main

Conversation

@Bhavik237

Copy link
Copy Markdown

Summary

Describe your solution briefly.

A Spring Boot service that transfers money between two wallets with four guarantees: idempotent request handling (a retried request never applies twice and always replays the original result), a double-entry ledger (every processed transfer writes exactly one DEBIT and one CREDIT row), correct balance tracking (failed transfers leave both wallets untouched), and safe concurrent execution (pessimistic row locks in a deterministic order, plus a two-layer idempotency guard, prevent overdrafts, lost updates, deadlocks, and double-processing). The schema is four Postgres tables managed by Flyway (wallets, transfers, ledger_entries, idempotency_records); the API is a single POST /transfers write endpoint plus two read endpoints for wallet balance and history.

AI disclosure

Detail how you used AI to help with your submission (including the tools you used, how
you used them and what your prompts were).
Include these points in detail

  1. What tool you used (Cursor, Claude Code, Antigratvity etc.) - claude code
  2. How you generally use the tool for your work. - Discuss my understanding with Claude, identify any issues or gaps, and come up with a suitable solution to address the problem.
  3. A transcript of your entire session with your AI tool of choice. You can add this to the repo or email it to us with your submission. If for some reason, this is not possible, give us all the prompts that you used with the AI.

Schema Design

Describe the tables, constraints, and indexes you introduced.

refer - doc/design.md

Idempotency Strategy

Explain how duplicate requests are handled safely.

Two layers, because an application-level check-then-insert alone has a race window:

Fast path. Before doing anything else, look up idempotency_records by key. If found, deserialize and return the cached response — the transfer is never re-attempted. Cheap, handles the common case (sequential retries).
Race guard. If no cached record exists, the attempt proceeds inside TransferAttemptExecutor.attemptTransfer (@transactional), which creates the Transfer row with status = PENDING and saveAndFlushs it immediately — this is the moment idempotency_key's UNIQUE constraint is actually enforced. If two requests with the same key both pass the fast-path check simultaneously, only one insert can succeed; the other throws DataIntegrityViolationException. The whole attempt transaction for the loser rolls back (no partial wallet mutation persists), and the loser re-reads idempotency_records in a fresh transaction/persistence context (reusing the failed one is unsafe — its state is undefined per the JPA spec after a flush failure) to replay the winner's already-committed response. Postgres blocks the losing insert until the winner's transaction finishes, so that re-read is guaranteed to see the result.

An idempotency_records row is written only after the attempt fully resolves (processed or failed), so it always caches the final outcome, never an intermediate one.

Concurrency Strategy

Explain how you prevent race conditions and double spending.

  • Single transactional boundary. Wallet locks, balance mutation, ledger writes, transfer state change, and the idempotency-record write all run in one @transactional method on a separate Spring bean (TransferAttemptExecutor) — not a private method on TransferService, since a self-invoked @transactional call bypasses Spring's proxy and silently runs with no transaction. This keeps the attempt all-or-nothing.
  • Row-level locking. Both wallets are locked with SELECT ... FOR UPDATE (@lock(LockModeType.PESSIMISTIC_WRITE)) before their balances are read or mutated, so concurrent transfers touching the same wallet serialize on the lock instead of racing on a stale in-memory balance. Existence is checked first with a plain unlocked read, so a request against a missing wallet fails fast (404) without taking a lock.
  • Deterministic lock ordering. Locks are always acquired in ascending wallet-id order, regardless of which side is from/to, so two transfers moving money in opposite directions between the same pair of wallets can never deadlock on each other.
  • Overdraft protection. The fromWallet balance is checked only after its lock is held; insufficient funds marks the transfer FAILED with no balance change and no ledger entries, so a failed transfer is a true no-op.

How to Run

  • Start a Postgres instance (or rely on the Testcontainers-managed instance used by the integration tests).
  • ./mvnw flyway:migrate (or let the application run migrations on startup) to apply V1__init_schema.sql.
  • ./mvnw spring-boot:run to start the service.
  • Exercise the API: POST /transfers, GET /wallets/{id}, GET /wallets/{id}/transfers.

How to Test

  • ./mvnw test runs the full suite:

  • WalletApiIntegrationTest — wallet read endpoints, including 404s.

  • TransferApiIntegrationTest — happy path, idempotent replay (identical and differing retry bodies), insufficient funds, invalid-request rejection (no Transfer row created).

  • TransferServiceTest — unit test simulating a lost idempotency_key insert race via mocked DataIntegrityViolationException.

  • ConcurrentTransferTest — real Testcontainers Postgres with genuine thread-pool concurrency: 20 simultaneous transfers from one wallet never overdraw or lose an update; 20 simultaneous requests with the same idempotency key produce exactly one Transfer row and one identical response across all callers.

Tradeoffs / Assumptions

  1. idempotencyKey is mandatory (@notblank) — no fire-and-forget path without one.
  2. Same key + different payload → still replays the original response, no validation that the retry matches the original request (silent, not a 409/422 conflict).
  3. No currency conversion / no currency field — single, implicit currency across all wallets and transfers.
  4. No wallet-creation API — wallets are fixed seed data (wallet_1/2/3); transfers only move money between these three.
  5. No auth/authorization — any caller can transfer between any two wallet IDs; no ownership or identity concept.

Checklist

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

Copilot AI lite review requested due to automatic review settings August 16, 2026 13:00

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

Note

Copilot was unable to run its full agentic suite in this review.

Adds a Spring Boot wallet transfer service backed by PostgreSQL with Flyway migrations, idempotent transfer processing, and concurrency-safe balance updates, plus integration/unit tests and local dev tooling.

Changes:

  • Introduces wallet/transfer/ledger/idempotency domain model with JPA repositories and service layer implementing idempotency + deterministic wallet locking.
  • Adds Flyway schema + seed migrations and Spring Boot configuration for app and tests (Testcontainers).
  • Adds integration/unit/concurrency tests, plus Docker/Maven/Sonar setup for local runs and CI analysis.

Reviewed changes

Copilot reviewed 40 out of 41 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/test/resources/application.properties Test Spring config enabling Flyway/JPA validation and quieter logs for containers/Hibernate.
src/test/java/com/api/wallet/service/TransferServiceTest.java Unit test for idempotency race-loser replay path using mocks.
src/test/java/com/api/wallet/controller/WalletApiIntegrationTest.java Integration tests for wallet balance + transfer history endpoints with Testcontainers DB.
src/test/java/com/api/wallet/controller/TransferApiIntegrationTest.java Integration tests for transfers: ledger balance, idempotent replay, validation and failure modes.
src/test/java/com/api/wallet/concurrency/ConcurrentTransferTest.java Multi-threaded integration tests asserting correctness under concurrent transfer requests.
src/test/java/com/api/wallet/TestcontainersConfiguration.java Springs Testcontainers @ServiceConnection PostgreSQL container config.
src/main/resources/db/migration/V2__seed_wallets.sql Seeds initial wallets.
src/main/resources/db/migration/V1__init_schema.sql Creates schema for wallets, transfers, ledger entries, and idempotency records.
src/main/resources/application.properties App datasource/Flyway/JPA configuration.
src/main/java/com/api/wallet/service/WalletService.java Read-only wallet balance lookup service.
src/main/java/com/api/wallet/service/TransferService.java Transfer API service with idempotency replay and race handling.
src/main/java/com/api/wallet/service/TransferAttemptExecutor.java Transactional transfer execution with wallet row locks and ledger writes.
src/main/java/com/api/wallet/repository/WalletRepository.java Wallet JPA repository with pessimistic locking query.
src/main/java/com/api/wallet/repository/TransferRepository.java Transfer JPA repository with history query.
src/main/java/com/api/wallet/repository/LedgerEntryRepository.java Ledger entry repository with transfer and wallet queries.
src/main/java/com/api/wallet/repository/IdempotencyRecordRepository.java Idempotency record repository.
src/main/java/com/api/wallet/exception/WalletNotFoundException.java Custom exception for missing wallets.
src/main/java/com/api/wallet/exception/GlobalExceptionHandler.java Maps common exceptions to structured error responses.
src/main/java/com/api/wallet/dto/WalletResponse.java Wallet DTO mapping.
src/main/java/com/api/wallet/dto/TransferResponse.java Transfer response DTO mapping.
src/main/java/com/api/wallet/dto/ErrorResponse.java Standard error response DTO.
src/main/java/com/api/wallet/dto/CreateTransferRequest.java Validated transfer request DTO.
src/main/java/com/api/wallet/domain/Wallet.java Wallet entity with balance mutation helpers.
src/main/java/com/api/wallet/domain/TransferStatus.java Transfer state machine + HTTP mapping.
src/main/java/com/api/wallet/domain/Transfer.java Transfer entity and state transitions.
src/main/java/com/api/wallet/domain/LedgerEntry.java Ledger entry entity and factories.
src/main/java/com/api/wallet/domain/IdempotencyRecord.java Idempotency replay cache entity.
src/main/java/com/api/wallet/domain/EntryType.java Ledger entry type enum.
src/main/java/com/api/wallet/controller/WalletController.java Wallet read endpoints controller.
src/main/java/com/api/wallet/controller/TransferController.java Transfer creation endpoint controller.
src/main/java/com/api/wallet/WalletApplication.java Spring Boot application entry point.
sonar-project.properties SonarQube settings for sources/tests/binaries and coverage paths.
pom.xml Maven build with Spring Boot, JPA, Flyway, Testcontainers, and JaCoCo.
mvnw.cmd Maven wrapper script (Windows).
mvnw Maven wrapper script (Unix).
docker-compose.yml Local Docker Compose for Postgres + the app.
doc/design.md Architecture/design documentation describing schema, flows, and concurrency strategy.
Dockerfile Multi-stage build and runtime image for running the service.
.mvn/wrapper/maven-wrapper.properties Maven wrapper distribution configuration.
.gitignore Ignores build output and IDE metadata.
.dockerignore Excludes build/IDE/git artifacts from Docker build context.
Suppressed comments (2)

src/test/java/com/api/wallet/concurrency/ConcurrentTransferTest.java:1

  • ready.await() has no timeout and can hang the test indefinitely if any task fails before counting down. Also, executor.shutdown() without awaitTermination (and possibly shutdownNow on failure) can leave worker threads running when a test fails or times out. Add time-bounded awaits and ensure the executor is terminated reliably to reduce test flakiness and hangs.
    src/test/java/com/api/wallet/concurrency/ConcurrentTransferTest.java:1
  • ready.await() has no timeout and can hang the test indefinitely if any task fails before counting down. Also, executor.shutdown() without awaitTermination (and possibly shutdownNow on failure) can leave worker threads running when a test fails or times out. Add time-bounded awaits and ensure the executor is terminated reliably to reduce test flakiness and hangs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docker-compose.yml Outdated
volumes:
- wallet-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U wallet -d wallet"]
Comment on lines +15 to +16
public class
TransferController {
Comment thread doc/design.md Outdated
- Persist an `idempotency_records` row caching the resulting `TransferResponse` (and HTTP status) for future replays.
- The transaction commits (or fully rolls back) as a single unit.
5. **Concurrent-duplicate race.** If a second request with the same key committed first between step 3's check and step 4's insert, the `saveAndFlush` throws `DataIntegrityViolationException`. `TransferService` catches it, re-reads `idempotency_records` for that key (guaranteed visible now, since Postgres blocks the losing insert until the winning transaction finishes) and replays the winner's response.
6. **Response.** The controller returns the transfer's own `status`-derived HTTP code (`201` for `PROCESSED`, `422` for `FAILED`) with the `TransferResponse` body.
Comment on lines +8 to +11
public boolean canTransit(TransferStatus nextStatus){
return switch (this) {
case PENDING -> nextStatus == PROCESSED || nextStatus== FAILED;
case FAILED,PROCESSED -> false;
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