Careeroza
Real World Example
Paytm-style Wallet Transfer
Moving money between two wallets is the textbook transaction: debit one, credit the other, record the transfer, all or nothing. A failed attempt must still be logged for fraud checks, so the attempt log uses REQUIRES_NEW.
@Service
@RequiredArgsConstructor
public class WalletService {
private final WalletRepository wallets;
private final TransferRepository transfers;
private final TransferAttemptLogger attempts; // separate bean -> REQUIRES_NEW works
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 3,
rollbackFor = InsufficientBalanceException.class)
public Transfer transfer(Long fromId, Long toId, BigDecimal amount) throws InsufficientBalanceException {
attempts.log(fromId, toId, amount); // committed even if we roll back
int debited = wallets.debitIfEnough(fromId, amount); // atomic UPDATE ... where balance >= ?
if (debited == 0) throw new InsufficientBalanceException(fromId, amount);
wallets.credit(toId, amount);
return transfers.save(new Transfer(fromId, toId, amount));
}
}
public interface WalletRepository extends JpaRepository<Wallet, Long> {
@Modifying
@Query("update Wallet w set w.balance = w.balance - :amt where w.id = :id and w.balance >= :amt")
int debitIfEnough(@Param("id") Long id, @Param("amt") BigDecimal amt);
@Modifying
@Query("update Wallet w set w.balance = w.balance + :amt where w.id = :id")
int credit(@Param("id") Long id, @Param("amt") BigDecimal amt);
}Success and Failure
Failed transfer
curl -X POST localhost:8080/api/wallet/transfers -H "Content-Type: application/json" \
-d '{"from":11,"to":42,"amount":5000}'Example output
-- wallet 11 has only Rs.1200
DEBUG o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.shopease.wallet.WalletService.transfer]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED,timeout_3,-com.shopease.wallet.InsufficientBalanceException
DEBUG o.s.orm.jpa.JpaTransactionManager : Suspending current transaction, creating new transaction with name [com.shopease.wallet.TransferAttemptLogger.log]
DEBUG org.hibernate.SQL : insert into transfer_attempts (amount,from_wallet,to_wallet) values (?,?,?)
DEBUG o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
DEBUG o.s.orm.jpa.JpaTransactionManager : Resuming suspended transaction after completion of inner transaction
DEBUG org.hibernate.SQL : update wallets set balance=(balance-?) where id=? and balance>=?
DEBUG o.s.orm.jpa.JpaTransactionManager : Initiating transaction rollback
HTTP/1.1 422 Unprocessable Entity
{"type":"about:blank","title":"Insufficient balance","status":422,"detail":"Wallet 11 cannot send Rs.5000.00"}Rules Used by Payment Teams
- One use case = one transaction, started in the service layer.
- Atomic conditional UPDATEs (or
@Version) instead of read-check-write. REQUIRES_NEWonly for records that must outlive a rollback; keep them tiny.- Explicit
rollbackForwhen business exceptions are checked. - Short timeouts, and never call external APIs (payment gateway, SMS) inside the DB transaction: call them before, or after commit with
@TransactionalEventListener(see the Spring Boot advance level).