✅ Transactions & Performance — GOOD vs BAD Practices (with Examples)
1️⃣ Where to Put @Transactional
❌ BAD PRACTICE – Transaction in Controller
@RestController
public class OrderController {
@Transactional
@PostMapping("/order")
public void placeOrder() {
orderService.placeOrder();
}
}
🚨 Why bad:
Controller shouldn’t manage transactions
Harder to test
Breaks separation of concerns
✅ GOOD PRACTICE – Transaction in Service
@Service
public class OrderService {
@Transactional
public void placeOrder() {
orderRepo.save(order);
stockRepo.updateStock();
}
}
✔ Clean architecture
✔ Easier to manage & test
2️⃣ Transaction Scope (Length)
❌ BAD – Long Transaction
@Transactional
public void processOrder() {
callPaymentGateway(); // slow
generateInvoicePdf(); // CPU heavy
sendEmail(); // external I/O
orderRepo.save(order);
}
🚨 Problems:
DB locks held too long
Poor throughput
Risk of deadlocks
Slow system under load
✅ GOOD – Short Transaction
public void processOrder() {
callPaymentGateway();
generateInvoicePdf();
saveOrder();
}
@Transactional
void saveOrder() {
orderRepo.save(order);
}
✔ Transaction only around DB work
✔ Better performance
✔ Scales well
3️⃣ Read APIs and Transactions
❌ BAD – No Read-Only Flag
@Transactional
public List<User> getUsers() {
return userRepo.findAll();
}
🚨 Issues:
Unnecessary dirty checking
More memory usage
✅ GOOD – Read-Only Transaction
@Transactional(readOnly = true)
public List<User> getUsers() {
return userRepo.findAll();
}
✔ Faster
✔ Optimized by Hibernate & DB
4️⃣ Exception Handling & Rollback
❌ BAD – Checked Exception Doesn’t Rollback
@Transactional
public void saveData() throws Exception {
repo.save(entity);
throw new Exception("Error"); // NO rollback!
}
🚨 Data gets committed unexpectedly.
✅ GOOD – Explicit Rollback Rule
@Transactional(rollbackFor = Exception.class)
public void saveData() throws Exception {
repo.save(entity);
throw new Exception("Error");
}
✔ Correct rollback behavior
5️⃣ Calling External Services in Transaction
❌ BAD
@Transactional
public void createUser() {
userRepo.save(user);
emailService.sendWelcomeMail(); // external call
}
🚨 Email failure can rollback DB
🚨 DB transaction waits on email server
✅ GOOD
@Transactional
public void createUser() {
userRepo.save(user);
}
public void postCreateUser() {
emailService.sendWelcomeMail();
}
✔ DB work isolated
✔ External failure doesn’t corrupt data
6️⃣ N+1 Query Problem
❌ BAD – N+1 Queries
List<Order> orders = orderRepo.findAll();
for (Order order : orders) {
order.getItems().size(); // each call hits DB
}
🚨 1 + N queries → very slow
✅ GOOD – JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.items")
List<Order> findAllWithItems();
✔ Single query
✔ Massive performance improvement
7️⃣ Pagination
❌ BAD – Load Everything
List<User> users = userRepo.findAll();
🚨 Memory heavy
🚨 Slow for large tables
✅ GOOD – Pagination
Page<User> users = userRepo.findAll(PageRequest.of(0, 20));
✔ Faster
✔ Scalable
8️⃣ Transaction Propagation Misuse
❌ BAD – Everything in One Transaction
@Transactional
public void process() {
saveMainData();
saveAuditLog(); // should not rollback
}
🚨 Audit log lost if main tx fails
✅ GOOD – REQUIRES_NEW for Audit
@Transactional
public void process() {
saveMainData();
auditService.saveLog();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() {
auditRepo.save(log);
}
✔ Audit always saved
✔ Proper transaction boundaries
9️⃣ Overusing @Transactional
❌ BAD
@Transactional
public void calculate() {
int x = a + b;
}
🚨 No DB → unnecessary overhead
✅ GOOD
public void calculate() {
int x = a + b;
}
✔ Simple
✔ Clean
🔟 Fetch Strategy
❌ BAD – EAGER Loading
@OneToMany(fetch = FetchType.EAGER)
private List<Item> items;
🚨 Heavy queries
🚨 Hidden performance issues
✅ GOOD – LAZY + Fetch When Needed
@OneToMany(fetch = FetchType.LAZY)
private List<Item> items;
✔ Controlled loading
✔ Better performance
🧠 Senior Dev Summary (Memorable)
❝ Transactions protect data, but bad transactions kill performance ❞
Always remember:
Keep transactions short
DB work only
Read-only where possible
Avoid external calls
Watch N+1 queries
prevent hidden performance bugs and transactional issues before production.
1. @Transactional Pitfalls
Common Issues
Self-invocation
@Transactionalmethods called within the same class do not start a transaction.Reason: Spring uses proxies.
Wrong layer
@Transactionalon controllers → ❌Correct place: Service layer
Exception handling
Rollback happens only for unchecked exceptions by default.
Catching exceptions without rethrowing → transaction commits.
Long transactions
Mixing DB logic with external API calls causes locks and timeouts.
Best Practices
Keep transactions short
One business use-case per transaction
Use
rollbackForwhen neededUse
REQUIRES_NEWonly when truly required
2. Lazy vs Eager Loading
Default Behavior
@ManyToOne→ EAGER@OneToMany/@ManyToMany→ LAZY
Common Problems
LazyInitializationExceptionOver-fetching with EAGER relationships
Correct Solutions
DTO projections (preferred)
JPQL fetch joins
EntityGraphfor controlled loading
Anti-Patterns
Setting everything to
FetchType.EAGERReturning entities directly from controllers
3. Pagination & Filtering
Why It Matters
Prevents memory issues
Improves query performance
Protects APIs from abuse
Best Practices
Always use
Pageablefor list endpointsApply filtering at the database level
Index frequently filtered and sorted columns
Rules
Never return unbounded lists
Paginate before mapping to DTOs
Avoid in-memory filtering
4. N+1 Query Problem
What It Is
One query to fetch parent entities
N additional queries for related entities
How It Happens
Accessing lazy relationships in loops
Returning entities instead of DTOs
Detection
SQL logs
Hibernate statistics
Repeating queries with different IDs
Fixes
Fetch joins
DTO queries
Batch fetching
Entity graphs
5. Performance Awareness (Lead Expectations)
Red Flags in PRs
findAll()without paginationLazy collections accessed in loops
EAGER relationships on large entities
Business logic inside transactions
Controllers returning entities
Lead Rule
Every DB call must be intentional, bounded, and observable.
6️⃣ Transaction Isolation & Locking
Why it matters
Race conditions, double payments, dirty reads.
You should know
READ_COMMITTED(default in most DBs)REPEATABLE_READSERIALIZABLE
Practical tools
Optimistic locking
@Version
private Long version;
Pessimistic locking
@Lock(LockModeType.PESSIMISTIC_WRITE)
Lead rule
If money or inventory is involved, locking must be explicit.
7️⃣ Flush vs Commit (Hibernate trap)
save()≠ SQL executionSQL runs on flush, not commit
repo.save(x);
repo.flush(); // forces SQL
Why this matters:
Constraint violations appear late
Bugs surface only at commit time
8️⃣ Read-only Transactions
@Transactional(readOnly = true)
Benefits:
Prevents accidental writes
Optimizes Hibernate dirty checking
Lead expectation:
All pure read service methods are read-only
9️⃣ Batch Operations & JDBC Performance
Problem
save() in loop → 1000 INSERTs
Solutions
Hibernate batch size
hibernate.jdbc.batch_size=50
saveAll()Periodic
flush()+clear()
🔟 Connection Pool Awareness (HikariCP)
You should understand:
Max pool size
Connection leaks
Long-running transactions blocking pool
Red flag:
App is “slow” but DB CPU is idle
1️⃣1️⃣ Index Awareness (Performance ≠ Code Only)
Index FK columns
Index columns used in:
WHERE
ORDER BY
JOIN
Lead rule:
Pagination without indexes is fake performance.
1️⃣2️⃣ Observability Basics
You should be able to:
Enable SQL logging temporarily
Spot N+1 in logs
Measure query time
Correlate API latency ↔ DB calls
No comments:
Post a Comment