Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
139: Transaction Management in JPA
Transactions are the safety net of any data-driven application. If you're moving money between accounts or updating an order status and its inventory simultaneously, you can't afford for one operation to succeed while the other fails. In JPA, transaction management ensures that a group of operations are treated as a single unit of work: either everything commits, or everything rolls back.
Building a Simple Fund Transfer
Let's build a small service to handle transferring money between two bank accounts. I'll keep the entities lean since we've already covered mapping in previous lessons. We just need an Account entity with a balance.
public class Account {
@Id
private Long id;
private BigDecimal balance;
// Getters, setters, and a method to deduct/add funds
public void withdraw(BigDecimal amount) {
this.balance = this.balance.subtract(amount);
}
public void deposit(BigDecimal amount) {
this.balance = this.balance.add(amount);
}
}
Now, I'll create a TransferService. My goal here is to take money from one account and give it to another. This is where the transaction becomes critical.
@Service
public class TransferService {
@Autowired
private AccountRepository accountRepository;
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.withdraw(amount);
accountRepository.save(from);
// Imagine something goes wrong here...
if (amount.compareTo(BigDecimal.valueOf(10000)) > 0) {
throw new IllegalArgumentException("Transfer limit exceeded!");
}
to.deposit(amount);
accountRepository.save(to);
}
}
The Mistake: Assuming Automatic Atomicity
If I run this code as-is and trigger that IllegalArgumentException, I'm going to have a bad day. Why? Because without an explicit transaction boundary, each save() call might be treated as its own transaction. The money would leave the first account, the exception would hit, and the second account would never receive the funds. The money essentially vanishes into the ether.
I actually did this in my first professional project. I assumed the framework "just handled it." It doesn't. You have to be explicit about where the unit of work begins and ends.
Fixing it with @Transactional
To fix this, I'll use the @Transactional annotation. This tells the JPA provider (and the underlying transaction manager) to wrap the entire method in a single transaction. If a RuntimeException is thrown, the manager will trigger a rollback, returning the first account's balance to its original state.
@Service
public class TransferService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.withdraw(amount);
accountRepository.save(from);
if (amount.compareTo(BigDecimal.valueOf(10000)) > 0) {
throw new IllegalArgumentException("Transfer limit exceeded!");
}
to.deposit(amount);
accountRepository.save(to);
}
}
Now, if that exception fires, JPA ensures that the withdraw operation is never committed to the database. It's all or nothing.
A Quick Warning on Self-Invocation
Here is a nuance that trips up almost everyone. @Transactional works via AOP proxies. When a class calls its own method, it bypasses the proxy. Look at this mistake:
public void executeTransfer(Long from, Long to, BigDecimal amount) {
// Calling a @Transactional method from within the same class!
this.transfer(from, to, amount);
}
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) {
// ... logic ...
}
In the example above, executeTransfer is not transactional. Because it calls transfer using this, the @Transactional annotation on the transfer method is completely ignored. If you need a transactional boundary, the call must come from outside the bean.
📋 Practical Task
Implement a Rollback-Safe Inventory Adjustment System
You are tasked with building a WarehouseService that handles the distribution of a product to multiple store locations. You must ensure that if any single store update fails, no stores receive the stock (atomicity).
Requirements:
- Create a
Productentity (withstockQuantity) and aStoreInventoryentity (withcurrentStock). - Implement a method
distributeStock(Long productId, Map<Long, Integer> storeAllocations). - The method should:
- Deduct the total sum of all allocations from the main
Productstock. - Iterate through the map and increase the stock for each
StoreInventory. - Throw a
RuntimeExceptionif any store'scurrentStockwould exceed a maximum capacity of 500 units.
- Deduct the total sum of all allocations from the main
- Ensure that if the capacity limit is hit for the 5th store in a list of 10, the
Productstock is restored and the first 4 stores' updates are rolled back.
There are no comments for now.