Thursday, February 5, 2026

Transactions & Performance (Spring Boot)

✅ 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

    • @Transactional methods called within the same class do not start a transaction.

    • Reason: Spring uses proxies.

  • Wrong layer

    • @Transactional on 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 rollbackFor when needed

  • Use REQUIRES_NEW only when truly required


2. Lazy vs Eager Loading

Default Behavior

  • @ManyToOne → EAGER

  • @OneToMany / @ManyToMany → LAZY

Common Problems

  • LazyInitializationException

  • Over-fetching with EAGER relationships

Correct Solutions

  • DTO projections (preferred)

  • JPQL fetch joins

  • EntityGraph for controlled loading

Anti-Patterns

  • Setting everything to FetchType.EAGER

  • Returning entities directly from controllers


3. Pagination & Filtering

Why It Matters

  • Prevents memory issues

  • Improves query performance

  • Protects APIs from abuse

Best Practices

  • Always use Pageable for list endpoints

  • Apply 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 pagination

  • Lazy 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_READ

  • SERIALIZABLE

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 execution

  • SQL 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

LeetCode C++ Cheat Sheet June

🎯 Core Patterns & Representative Questions 1. Arrays & Hashing Two Sum – hash map → O(n) Contains Duplicate , Product of A...