I have a REST application written using Spring Boot, in which I have a wallet. The wallet has methods like addAmount, deductAmount and so on. Here's the code:
WalletController.java
public class WalletController {
    public final LoadDatabase loadDatabase;
    private final WalletRepository repository;
    @Autowired
    public WalletController(LoadDatabase loadDatabase, WalletRepository repository) {
        this.loadDatabase = loadDatabase;
        this.repository = repository;
    }
    @GetMapping("/addAmount")
    @ResponseBody
    public void addAmount(@RequestParam Long custId, @RequestParam Long amount){
        try{
            Wallet wallet = repository.findWalletsByCustId(custId).get(0);
            wallet.balance = wallet.balance+amount;
            repository.save(wallet);
        }catch(IndexOutOfBoundsException e){
            //handle exception
        }
    }
    @GetMapping("/deductAmount")
    @ResponseBody
    public boolean deductAmount(@RequestParam Long custId, @RequestParam Long amount){
        try{
            Wallet wallet = repository.findWalletsByCustId(custId).get(0);
            if(wallet.balance < amount)
                return false;
            wallet.balance = wallet.balance-amount;
            repository.save(wallet);
            return true;
        }catch(IndexOutOfBoundsException e){
            return false;
        }
    }
    // some other methods.
 
This is going to be concurrently accessed and hence, I want to make both addAmount and deductAmount atomic in nature.
To check this, I wrote a shell script which adds and deducts some amount concurrently.
wallet_test.sh
#! /bin/sh
# Get the balance of Customer 201 before.
balanceBefore=$(curl -s "http://localhost:8082/getBalance?custId=201")
echo "Balance Before:" $balanceBefore
sh wa1 & sh wa2
wait    
# Get the balance of Customer 201 afterwards.
balanceAfter=$(curl -s "http://localhost:8082/getBalance?custId=201")
echo "Balance After" $balanceAfter
where wa1 and wa2 are as follows:
for i in {0..10};
do
#   echo "Shell 1:" $i
    resp=$(curl -s "http://localhost:8082/addAmount?custId=201&amount=100")
done
for i in {0..10};
do
#   echo "Shell 2:" $i
    resp=$(curl -s "http://localhost:8082/deductAmount?custId=201&amount=100")
done
The output, as expected due to concurrent access, is something of the form:
Balance Before: 10000
Balance After 9900
Balance Before: 10000
Balance After 10300
Balance Before: 10000
Balance After 9600
The expected output for me is that the balance before and after should remain the same i.e 10000.
Now, I've read that to make it atomic, we can make use of the @Transactional annotation and that we can add it to both the methods, or to the entire class. I tried doing both, and yet, I am not getting the results I desire.
I added it at the method level i.e
    @GetMapping("/deductAmount")
    @ResponseBody
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public boolean deductAmount(@RequestParam Long custId, @RequestParam Long amount){
and same for deductAmount, which didn't work.
I tried adding it at the class level i.e
@Controller
@Transactional(isolation = Isolation.SERIALIZABLE)
public class WalletController {
    public final LoadDatabase loadDatabase;
    private final WalletRepository repository;
and this didn't work either.
Is @Transactional not meant to be used this way? Should I use some other locking mechanism in order to accomplish what I want?
EDIT:
As mentioned, I tried adding Pessimistic locks as well.
    import static javax.persistence.LockModeType.PESSIMISTIC_WRITE;
    @PersistenceContext
    private EntityManager em;
    @GetMapping("/addAmount")
    @ResponseBody
    @Transactional
    synchronized public void addAmount(@RequestParam Long custId, @RequestParam Long amount){
        try{
            Wallet wallet = repository.findWalletsByCustId(custId).get(0);
            em.lock(wallet, PESSIMISTIC_WRITE);
            wallet.balance = wallet.balance+amount;
            repository.save(wallet);
        }catch(IndexOutOfBoundsException e){
            //handle exception
        }
    }
    @GetMapping("/deductAmount")
    @ResponseBody
    @Transactional
    synchronized public boolean deductAmount(@RequestParam Long custId, @RequestParam Long amount){
        try{
            Wallet wallet = repository.findWalletsByCustId(custId).get(0);
            em.lock(wallet, PESSIMISTIC_WRITE);
            if(wallet.balance < amount)
                return false;
            wallet.balance = wallet.balance-amount;
            repository.save(wallet);
            return true;
        }catch(IndexOutOfBoundsException e){
            return false;
        }
    }
 
    