Whenever a money transfer involves reading a balance and then writing an updated balance, there is an inherent window of time in which a second concurrent request could read the same stale balance. Without a concurrency control strategy, two simultaneous withdrawals from the same account could each see sufficient funds, both proceed, and collectively deduct more money than the account ever held. Digital Money House addresses this through a combination of declarative transactions, balance validation inside the transaction boundary, and JWT-bound sender identity.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Gianluca-X/DigitalMoney/llms.txt
Use this file to discover all available pages before exploring further.
The Problem: Race Conditions Without Locking
Consider two transfer requests that arrive at nearly the same time, both reading account A’s balance of $1,000:- Thread 1 reads balance →
$1,000. Plans to deduct$600. - Thread 2 reads balance →
$1,000(same stale value). Plans to deduct$600. - Thread 1 writes new balance →
$400. ✅ - Thread 2 writes new balance →
$400. ❌ — should be$-200(rejected), but it succeeds because it used the stale read.
$200 even though the business rule forbids negative balances. The two writes raced, and the first write was silently overwritten by the second.
Solution 1 — @Transactional Boundaries
All cash transfer logic is wrapped in a single Spring @Transactional method. The annotation on makeTransferFromCash in TransferenceServiceImpl guarantees that every database operation within the method — deducting the sender’s balance, crediting the recipient’s balance, saving the Transference record, and saving both Activity entries — either all succeed together or all roll back together:
Solution 2 — JWT-Derived Sender Identity
The sender’s account is never accepted as a client-supplied parameter for the actual deduction. Instead, the accounts-serviceJwtAuthenticationFilter resolves the authenticated user’s Account entity from the email embedded in the JWT and stores it as the Spring Security principal. Controllers then retrieve the account from the security context rather than trusting an arbitrary account ID from the request body.
This means a client cannot forge a request that debits a different user’s account — even if they know the target account ID. The JWT signature ensures the email cannot be tampered with, and the email is the authoritative link to the sender’s Account record.
Business Validations That Complement Transactions
Transaction boundaries work together with the following business-rule validations applied before any balance change is committed:| Validation | Exception thrown |
|---|---|
| Transfer amount must be greater than zero | BadRequestException |
| Sender and recipient accounts must differ | BadRequestException |
| Sender must have sufficient funds | InsufficientFundsException |
| Card used for deposit must belong to the account | UnauthorizedException |
| Sender and recipient accounts must exist | AccountNotFoundException / ResourceNotFoundException |
@Transactional boundary, so a validation failure is guaranteed to roll back any partial state changes.
Atomic Activity Recording
Every balance-changing operation records one or moreActivity entries in the same transaction:
| Operation | Activities recorded |
|---|---|
| Cash transfer out | transfer-out on sender account |
| Cash transfer in | transfer-in on recipient account |
| Card deposit | deposit on the depositing account |
@Transactional call, the activity log is always consistent with the actual balance: if the transfer rolls back, the activity record rolls back with it. There is no scenario in which a balance changes without a matching activity entry, or vice versa.
Concurrency Testing
The test suite validates transfer correctness under concurrent load usingExecutorService-based multi-threaded tests in TransferenceServiceTest. Multiple threads submit competing transfer requests simultaneously; the tests then assert that the final balances are exactly correct — confirming that the combination of atomic transactions and balance validation prevents any money from being created or destroyed by concurrent execution.
The unit tests use Mockito mocks and verify that both
accountsRepository.save() calls and both activityRepository.save() calls are made exactly once per transfer, ensuring no partial state is ever persisted.