Skip to content

Add enums for LedgerEntryType and TransferStatus; create initial appl…#103

Open
sanchaymehrotra wants to merge 1 commit into
Robustrade:mainfrom
sanchaymehrotra:solution/sanchay-mehrotra
Open

Add enums for LedgerEntryType and TransferStatus; create initial appl…#103
sanchaymehrotra wants to merge 1 commit into
Robustrade:mainfrom
sanchaymehrotra:solution/sanchay-mehrotra

Conversation

@sanchaymehrotra

Copy link
Copy Markdown

Pull Request — Wallet Transfer Service

Summary

Implemented a Wallet Transfer Service using Spring Boot 3.3.7 and Java 21.

The service supports wallet-to-wallet transfers with:

  • Idempotent transfer processing
  • Double-entry ledger recording
  • Pessimistic locking for concurrency safety
  • Transfer status flow: PENDING → PROCESSED | FAILED
  • Clean layered architecture: controller → service → repository → entity
  • Swagger/OpenAPI documentation
  • Postman collection with test scenarios
  • Integration tests, including concurrency and idempotency cases
  • JaCoCo/SonarQube configuration
  • Maven CI workflow

APIs delivered

  • POST /v1/transfers — creates an idempotent wallet-to-wallet transfer
  • POST /v1/wallet-balance — returns wallet balance
  • POST /v1/transfer-history — returns transfer history for a wallet

AI disclosure

1. Tool used

I used Cursor as an AI-assisted IDE during development.

2. How I generally used the tool

I used Cursor as a development assistant for scaffolding, boilerplate generation, review support, test expansion, documentation, and SonarQube-related improvements.

The main architecture and implementation decisions were made and reviewed by me, including:

  • Schema design
  • Idempotency strategy
  • Wallet locking approach
  • Transaction boundaries
  • Failure handling
  • Ledger design
  • Test scenarios

All generated or assisted code was manually reviewed, adjusted where needed, and validated by running the test suite.

3. Session transcript / prompts

I used Cursor through multiple short development iterations. The AI assistance was mainly used for:

  • Reading assignment requirements
  • Creating the Spring Boot project structure
  • Generating DTOs, controllers, services, repositories, and entities
  • Adding constraints and indexes
  • Applying the ResponseException pattern
  • Fixing SonarQube-related issues
  • Adding Swagger annotations
  • Creating and updating the Postman collection
  • Expanding integration tests
  • Preparing the final PR description

Cursor was used as an assistant, while the final implementation, validation, and review were done by me.


Schema Design

The service uses four main tables.

wallets

Stores wallet details and current balance.

Column Description
wallet_id Primary key
balance Current wallet balance
version Optimistic lock field
created_at Creation timestamp
updated_at Last update timestamp

Constraints:

  • wallet_id as primary key
  • balance >= 0
  • Optimistic locking using @Version

transfers

Stores transfer requests and their status.

Column Description
id Primary key UUID
idempotency_key Unique key for safe retries
from_wallet_id Source wallet
to_wallet_id Destination wallet
amount Transfer amount
status PENDING, PROCESSED, or FAILED
failure_reason Reason for failed transfer
created_at Creation timestamp
updated_at Last update timestamp

Constraints and indexes:

  • Unique constraint on idempotency_key
  • Check constraint: from_wallet_id <> to_wallet_id
  • Check constraint: amount > 0
  • Indexes on from_wallet_id, to_wallet_id, and status

ledger_entries

Stores double-entry ledger records for successful transfers.

Column Description
id Primary key
wallet_id Wallet linked to entry
transfer_id Transfer linked to entry
entry_type DEBIT or CREDIT
amount Ledger amount
created_at Entry creation timestamp

For each successful transfer:

  • One DEBIT entry is created for the source wallet
  • One CREDIT entry is created for the destination wallet

Constraints and indexes:

  • Unique constraint on (transfer_id, wallet_id, entry_type)
  • Check constraint: amount > 0
  • Indexes on transfer_id and wallet_id

idempotency_records

Stores cached responses for idempotent requests.

Column Description
id Primary key
idempotency_key Unique idempotency key
transfer_id Linked transfer
request_hash Hash of request payload
response_status Cached response status
response_code Cached response code
response_message Cached response message
response_payload Cached response body
created_at Creation timestamp
updated_at Last update timestamp

Constraints:

  • Unique constraint on idempotency_key
  • Foreign key reference to transfers

Idempotency Strategy

The transfer API supports safe retries using an idempotencyKey.

The request hash is generated from:

  • fromWalletId
  • toWalletId
  • amount

If the same idempotency key is used again:

  • Same payload → cached response is returned
  • Different payload → idempotency conflict response is returned
  • In-flight duplicate request → existing transfer is checked and replayed once completed

For a new transfer:

  1. A transfer is created with PENDING status.
  2. Wallets are locked.
  3. Balance is validated.
  4. Wallet balances are updated.
  5. Ledger entries are created.
  6. Transfer is marked PROCESSED.
  7. Response is saved in idempotency_records.

For business failures such as insufficient funds:

  1. Transfer is marked FAILED.
  2. Failure reason is stored.
  3. No ledger entries are created.
  4. Failed response is also cached.

Unique constraints on transfers.idempotency_key and idempotency_records.idempotency_key ensure concurrent duplicate requests cannot create multiple transfers.


Concurrency Strategy

Concurrency safety is handled using database row locking and transaction boundaries.

Key points

  • Wallet rows are locked using PESSIMISTIC_WRITE.
  • Wallets are locked in sorted wallet ID order to reduce deadlock risk.
  • Debit, credit, ledger creation, transfer status update, and idempotency record creation happen in a single transaction.
  • DB constraints prevent invalid balances, invalid amounts, same-wallet transfers, and duplicate ledger rows.
  • Concurrent duplicate idempotency keys are handled using unique constraints and retry lookup.

This prevents double spending when multiple transfers try to debit the same wallet at the same time.

Concurrency is verified through integration tests for:

  • Concurrent double-spend prevention
  • Concurrent duplicate idempotency key handling

How to Run

Prerequisites

  • Java 21
  • Maven 3.9+

Checkout branch

git checkout solution/sanchay-mehrotra

Run locally with H2

mvn spring-boot:run

Run with PostgreSQL profile

mvn spring-boot:run -Dspring-boot.run.profiles=postgres

Set the following environment variables:

DB_HOST=
DB_PORT=
DB_NAME=
DB_USERNAME=
DB_PASSWORD=

URLs

Base URL:

http://localhost:8080/api/wallet-transfer

Swagger UI:

http://localhost:8080/api/wallet-transfer/swagger-ui.html

Postman collection:

postman/Wallet-Transfer-Service.postman_collection.json

How to Test

Run all tests:

mvn test

Run full verification with coverage:

mvn verify

Test coverage includes

  • Successful transfer
  • Debit and credit ledger creation
  • Idempotent replay
  • Idempotency conflict
  • Insufficient funds
  • Same source and destination wallet validation
  • Wallet not found
  • Wallet balance API
  • Transfer history API
  • Concurrent double-spend prevention
  • Concurrent duplicate idempotency key handling

Manual testing can be done by importing the Postman collection and running the included scenarios.


Tradeoffs / Assumptions

  • Stored wallet balance is used for fast reads; ledger is maintained as an audit trail.
  • Ledger entries are created only for successful transfers.
  • H2 is used for local testing; PostgreSQL profile is provided for production-like execution.
  • Schema is managed through JPA entity constraints and indexes.
  • Flyway/Liquibase migrations are not included as they were outside the scope of this assignment.
  • Duplicate in-flight idempotency requests use brief lookup/retry behavior.
  • Authentication and authorization are not implemented because they were out of scope.
  • APIs follow a MedConnect-style ResponseDTO response envelope.

Checklist

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

Copilot AI review requested due to automatic review settings July 2, 2026 06:14

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 idempotent transfer processing, pessimistic wallet locking for concurrency safety, and double-entry ledger recording, plus integration tests and supporting project configuration (CI, Sonar, Postman, Swagger).

Changes:

  • Added core domain model (wallets/transfers/ledger/idempotency), repositories, services, controllers, and Swagger/OpenAPI config.
  • Implemented idempotency via request hashing + persisted cached responses; added concurrency/behavioral integration tests.
  • Added runtime/test configuration (profiles, H2/Postgres), CI workflow updates, and supporting docs/artifacts (README, Postman, Sonar, JaCoCo).

Reviewed changes

Copilot reviewed 52 out of 54 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/test/resources/application-test.yml Test profile H2 + JPA configuration
src/test/java/com/wallettransfer/utility/ResponseBuilderFactoryTest.java Unit tests for response builder behavior
src/test/java/com/wallettransfer/utility/LogSanitizerTest.java Unit tests for log sanitization
src/test/java/com/wallettransfer/TransferIntegrationTest.java Behavioral integration tests incl. idempotency + concurrency
src/main/resources/data.sql Seed wallets for local runs
src/main/resources/application.yml Base Spring Boot configuration (profiles, JPA, management, springdoc)
src/main/resources/application-production.yml Production profile overrides
src/main/resources/application-postgres.yml Postgres profile datasource + JPA config
src/main/resources/application-local.yml Local H2 profile datasource + console + JPA config
src/main/java/com/wallettransfer/WalletTransferServiceApplication.java Spring Boot application entrypoint
src/main/java/com/wallettransfer/utility/TransferMapper.java Entity → response DTO mapping
src/main/java/com/wallettransfer/utility/ServiceResponseSupport.java Wrapper for consistent service exception-to-response behavior
src/main/java/com/wallettransfer/utility/RequestHashUtility.java Request hashing + JSON (de)serialization utilities
src/main/java/com/wallettransfer/utility/LogSanitizer.java Sanitizes log message content
src/main/java/com/wallettransfer/service/walletservice/WalletService.java Balance + history read APIs
src/main/java/com/wallettransfer/service/transferservice/TransferService.java Transfer API orchestration + idempotency handling entrypoint
src/main/java/com/wallettransfer/service/transferservice/TransferExecutor.java Transactional transfer execution (locking, balance updates, ledger, idempotency persistence)
src/main/java/com/wallettransfer/service/transferservice/IdempotencyResponseLookup.java Cached-response and existing-transfer replay lookup
src/main/java/com/wallettransfer/service/loggerservice/LogServiceImpl.java Logging implementation
src/main/java/com/wallettransfer/service/loggerservice/LogService.java Logging interface
src/main/java/com/wallettransfer/repo/WalletRepository.java Wallet persistence + pessimistic lock queries
src/main/java/com/wallettransfer/repo/TransferRepository.java Transfer persistence + idempotency lookup
src/main/java/com/wallettransfer/repo/LedgerEntryRepository.java Ledger entry persistence + history queries
src/main/java/com/wallettransfer/repo/IdempotencyRecordRepository.java Idempotency record persistence + key lookup
src/main/java/com/wallettransfer/exception/ResponseException.java Domain exception carrying response code + optional payload
src/main/java/com/wallettransfer/enums/TransferStatus.java Transfer status enum
src/main/java/com/wallettransfer/enums/LedgerEntryType.java Ledger entry type enum
src/main/java/com/wallettransfer/entity/Wallet.java Wallet entity (balance/version/timestamps + constraint)
src/main/java/com/wallettransfer/entity/Transfer.java Transfer entity (status machine, constraints, indexes)
src/main/java/com/wallettransfer/entity/LedgerEntry.java Ledger entry entity (double-entry record + constraint/indexes)
src/main/java/com/wallettransfer/entity/IdempotencyRecord.java Cached response record entity
src/main/java/com/wallettransfer/dto/responsedto/WalletBalanceResponseDTO.java Balance response DTO
src/main/java/com/wallettransfer/dto/responsedto/TransferResponseDTO.java Transfer response DTO
src/main/java/com/wallettransfer/dto/responsedto/ResponseDTO.java Standard API response envelope
src/main/java/com/wallettransfer/dto/responsedto/LedgerEntryResponseDTO.java Ledger entry response DTO
src/main/java/com/wallettransfer/dto/requestdto/WalletBalanceRequestDTO.java Balance request DTO + validation
src/main/java/com/wallettransfer/dto/requestdto/TransferHistoryRequestDTO.java Transfer history request DTO + validation
src/main/java/com/wallettransfer/dto/requestdto/CreateTransferRequestDTO.java Create transfer request DTO + validation
src/main/java/com/wallettransfer/controller/WalletController.java Wallet read-only endpoints (balance/history)
src/main/java/com/wallettransfer/controller/TransferController.java Transfer creation endpoint
src/main/java/com/wallettransfer/constants/ResponseConstant.java Business response codes
src/main/java/com/wallettransfer/constants/ApplicationConstants.java Common status/message constants
src/main/java/com/wallettransfer/constants/ApiConstants.java API path constants
src/main/java/com/wallettransfer/configuration/SwaggerConfig.java OpenAPI configuration
src/main/java/com/wallettransfer/configuration/GlobalRestExceptionHandler.java Global exception handling to ResponseDTO
src/main/java/com/wallettransfer/builder/ResponseBuilderFactory.java ResponseDTO builder + responseCode parsing
sonar-project.properties SonarQube analysis configuration
README.md Project documentation and run/test instructions
postman/Wallet-Transfer-Service.postman_collection.json Postman collection for API scenarios
pom.xml Maven project definition + dependencies + JaCoCo
.vscode/settings.json Editor configuration
.idea/.gitignore IntelliJ ignore rules
.gitignore Repo ignore rules (Java/Maven artifacts)
.github/workflows/ci.yml GitHub Actions CI workflow running Maven verify
Files not reviewed (1)
  • .idea/.gitignore: Generated file

Comment on lines +4 to +5
profiles:
active: local
Comment thread sonar-project.properties
Comment on lines +12 to +13
sonar.exclusions=**/WalletTransferServiceApplication.java
sonar.test.exclusions=**/*
Comment on lines +3 to +10
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackages = {"com.wallettransfer.*"})
@EntityScan("com.wallettransfer.*")
@SpringBootApplication
Comment on lines +32 to +34
private static boolean isSuccessCode(int code) {
return code >= 20000 && code <= 100000;
}
Comment on lines +3 to +8
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;

@Service
public interface LogService {
try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
CountDownLatch ready = new CountDownLatch(2);
CountDownLatch start = new CountDownLatch(1);
List<String> responses = new ArrayList<>();
try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
CountDownLatch ready = new CountDownLatch(2);
CountDownLatch start = new CountDownLatch(1);
List<String> outcomes = new ArrayList<>();
Comment on lines +104 to +113
List<TransferResponseDTO> history =
transferIds.stream()
.map(
transferId -> {
Transfer transfer = transfersById.get(transferId);
List<LedgerEntry> entriesForTransfer =
ledgerEntryRepository.findByTransfer_Id(transferId).stream()
.sorted(
Comparator.comparing(entry -> entry.getEntryType().name()))
.toList();
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