⌂ Home
Unit 20 · Transactions & Custom Repos

Spring Transactions — Layman Concepts

Understand why transactions exist, what @Transactional actually does behind the scenes, all 7 propagation modes with real analogies, rollback rules, and Spring Data JPA custom repos — all in plain English.

ACID Explained Simply @Transactional Proxy Magic 7 Propagation Modes Rollback Rules Spring Data JPA Custom Repository Interview Q&A
Controller Proxy Intercepts begin() Service Method commit() / rollback()

Why Do We Need Transactions?

“The bank transfer problem”

🏦 Bank Transfer Analogy

Imagine transferring ₹10,000 from your account to a friend. Step 1: Debit your account. Step 2: Credit your friend. Now imagine the app crashes after Step 1 but before Step 2. Your money is gone but your friend never got it! A transaction fixes this: either BOTH steps succeed, or BOTH are undone. No half-done state.

Without Transaction

Each DB operation is independent. If your code crashes midway, you get a partial, corrupted state in the database. This is catastrophic for financial, medical, or any critical data.

With Transaction

All operations are grouped. On success → commit (save everything). On failure → rollback (undo everything). Database always stays in a consistent state.

ACID — What Every Transaction Must Guarantee

“The 4 golden rules of database transactions”

A — Atomicity

“All or nothing.” Either every operation in the transaction succeeds, or none of them do. Like an atom — indivisible. If one step fails, the entire transaction rolls back.

Bank example: Debit AND Credit both happen, or neither does.

C — Consistency

“Data is always valid.” A transaction takes the database from one valid state to another valid state. No transaction should leave data in an inconsistent/illegal state.

Example: Total money before and after a transfer must be the same.

I — Isolation

“Transactions don't see each other's incomplete work.” Concurrent transactions behave as if they are running one-by-one. You don't read uncommitted data from another running transaction.

Example: Two people booking the last seat simultaneously — only one succeeds.

D — Durability

“Once committed, it stays.” After a transaction commits, the data is permanently saved — even if the server crashes immediately after. It's written to disk.

Example: If your payment confirmation appeared, it's saved even if the server dies next second.

🔑 Memory trick: ACID = All or nothing, Consistency guaranteed, Isolated from others, Durable once committed.

How @Transactional Actually Works (The Proxy Magic)

“You put one annotation and Spring does all the heavy lifting”

🔐 Bodyguard / Proxy Analogy

Imagine a bodyguard wrapping around your service. When you call a @Transactional method, you never reach the method directly — the bodyguard (Spring Proxy) intercepts the call. The bodyguard says “I'll start a transaction before your method runs and commit/rollback after it finishes.” Your method never needs to know about transactions.

Here is what Spring does step-by-step when you call a @Transactional method:

Step-by-step proxy flow:

  • Step 1: You call employeeService.addEmployee(dto)
  • Step 2: The call goes to Spring's CGLIB Proxy, not the real service yet
  • Step 3: The Proxy asks JpaTransactionManager to begin a transaction
  • Step 4: The Proxy calls the actual service method
  • Step 5a (Success): Method returns normally → Proxy calls commit()
  • Step 5b (Exception): RuntimeException thrown → Proxy calls rollback()
@Service public class EmployeeServiceImpl implements EmployeeService { @Transactional // Spring wraps this method in a transaction automatically public Integer addEmployee(EmployeeDTO dto) { // No begin/commit/rollback code needed here! return employeeDAO.addEmployee(dto); } }
🚨 The Self-Invocation Trap

If method A inside the same class calls method B (also @Transactional), the proxy is bypassed! The call goes directly to B without starting a new transaction. This is the most common @Transactional bug. Fix: move B to a different Spring bean, or use ApplicationContext to get the proxied version.

🚨 @Transactional on private methods

@Transactional on private methods does nothing! Spring's proxy can only intercept public methods. If you put @Transactional on a private or protected method, it is silently ignored.

7 Transaction Propagation Modes Explained Simply

“What should happen to the transaction when one method calls another?”

🏠 House Construction Analogy

Imagine building a house (the outer transaction). You call a plumber (inner method). Propagation decides: Does the plumber join your house project? Start their own independent project? Refuse to work unless they're part of a project? Each propagation mode answers this differently.

REQUIRED (Default)
If tx exists: JOIN it
If no tx: CREATE new

The safe default. Method A calls Method B — B joins A's transaction. If B fails, A's transaction also rolls back.

Use for: Most business methods.

REQUIRES_NEW
If tx exists: SUSPEND it, start fresh
If no tx: CREATE new

Completely independent transaction. Even if A rolls back, B's transaction commits separately. Perfect for audit logs.

Use for: Audit logging, email tracking, operations that must always commit.

MANDATORY
If tx exists: JOIN it
If no tx: THROW Exception

Method DEMANDS to be called inside a transaction. If there's no active transaction, it throws IllegalTransactionStateException.

Use for: Internal helper methods that must never run alone.

NEVER
If tx exists: THROW Exception
If no tx: Run normally

Method REFUSES to run inside a transaction. Throws exception if one exists.

Use for: Methods that must not acquire DB locks (e.g., reporting methods).

NOT_SUPPORTED
If tx exists: SUSPEND it
If no tx: Run normally (non-tx)

Suspends existing transaction and runs without a transaction. More lenient than NEVER.

Use for: Long external REST API calls that should not hold a DB transaction open.

SUPPORTS
If tx exists: JOIN it
If no tx: Run non-transactionally

Flexible — works with or without a transaction. Like a freelancer who joins the project if invited, but also works independently.

Use for: Read-only utility queries.

NESTED
If tx exists: Create a SAVEPOINT (nested)
If no tx: CREATE new

Creates a JDBC savepoint inside the existing transaction. If B fails, only B's work rolls back (to the savepoint). A can still continue and commit.

Use for: Partial rollback without failing the whole transaction.

🔑 Quick exam answer: If asked "what is the default propagation?" — it's REQUIRED. If asked "which propagation ignores outer transaction failures?" — it's REQUIRES_NEW.

Isolation Levels — How Much Can Transactions See Each Other?

“How much 'sneaking a peek' is allowed between concurrent transactions?”

📑 Exam Paper Analogy

Imagine two students writing exams in the same room. Isolation level determines how much they can see each other's work. READ UNCOMMITTED = they can copy each other's unfinished answers. SERIALIZABLE = totally separated rooms, no peeking at all.

LevelCan Read Uncommitted?Dirty Read?Non-Repeatable Read?Phantom Read?
READ_UNCOMMITTEDYes✅ Possible✅ Possible✅ Possible
READ_COMMITTEDNo❌ Prevented✅ Possible✅ Possible
REPEATABLE_READNo❌ Prevented❌ Prevented✅ Possible
SERIALIZABLENo❌ Prevented❌ Prevented❌ Prevented

Dirty Read

Reading data that another uncommitted transaction has modified. If that transaction rolls back, you read data that never officially existed. Like reading a cheque that was then cancelled.

Non-Repeatable Read

You read a row, another transaction changes it and commits, you read the same row again and get different data. Like reading a menu, someone changes prices, you read again and prices changed.

Phantom Read

You run the same query twice, but the second time returns MORE rows because another transaction inserted new rows and committed. Like counting seats, someone adds more chairs.

🔑 Default isolation level in Spring is DEFAULT — meaning it uses whatever the underlying database default is. MySQL InnoDB default = REPEATABLE_READ.

Rollback Rules — What Triggers a Rollback?

“Spring only rolls back automatically for certain exceptions”

🚨 The Most Common Interview Gotcha

By default, Spring ONLY rolls back on unchecked exceptions (RuntimeException and Error). For checked exceptions (like IOException, SQLException, or any class that extends Exception directly), Spring COMMITS the transaction even if the exception is thrown, unless you explicitly tell it not to with rollbackFor!

Unchecked Exception → Auto Rollback

@Transactional // Default rollback rule public void addEmployee(EmployeeDTO dto) { dao.save(dto); throw new RuntimeException("oops"); // ROLLBACK happens automatically! }

Checked Exception → NO Rollback (by default!)

@Transactional // Still commits by default! public void addEmployee() throws Exception { dao.save(dto); throw new Exception("checked!"); // COMMIT happens! Change is SAVED despite exception! }

Fix: Use rollbackFor to force rollback on checked exceptions

@Transactional(rollbackFor = Exception.class) public void addEmployee() throws Exception { dao.save(dto); throw new Exception("checked!"); // NOW it rolls back! }
🚨 The Swallowed Exception Trap

If you catch an exception inside your @Transactional method with try-catch and do NOT rethrow it, Spring's proxy sees a normal return and commits the transaction — even though something went wrong. Always rethrow or use TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() to mark for rollback manually.

Local Transaction vs Global Transaction (JTA)

“One database vs many databases in one transaction”

Local Transaction (JpaTransactionManager)

Manages a transaction against one single database. Used in 95% of web apps. Simple, performant, and configured with Spring's JpaTransactionManager.

Example: Your employee app uses one MySQL database.

Global Transaction (JTA / JtaTransactionManager)

Coordinates a transaction across multiple different resources (e.g., two different databases + a JMS message queue). Uses a two-phase commit (2PC) protocol. Complex, needs a JTA provider like Atomikos.

Example: A payment app that writes to an Oracle DB AND sends a message to RabbitMQ atomically.

What is Spring Data JPA? (vs Plain JPA)

“Even less code — Spring writes the DAO for you”

🤖 Auto-generated code analogy

Plain JPA: You write all DAO methods yourself (findById, save, deleteById, etc.) using EntityManager. Spring Data JPA is like having a robot write all those methods for you. You just define an interface, extend JpaRepository, and Spring generates the implementation automatically at runtime. You write zero implementation code.

// Plain JPA DAO — you write ALL of this manually: public Employee findById(int id) { return em.find(Employee.class, id); } public void save(Employee e) { em.getTransaction().begin(); em.persist(e); em.getTransaction().commit(); } // ... 5 more methods for CRUD ... // Spring Data JPA — just this interface, no implementation needed! public interface EmployeeRepository extends JpaRepository<Employee, Integer> { // save, findById, findAll, deleteById, count, existsById — ALL FREE! }

What JpaRepository gives you for free:

Custom Repositories — When Free Methods Are Not Enough

“Three ways to add your own queries”

1. Derived Query Methods

Name your method following Spring Data JPA naming conventions, and Spring generates the query automatically from the method name.

// Spring reads the name and generates: // SELECT * FROM employee WHERE role = ? List<Employee> findByRole(String role); // SELECT * WHERE salary > ? AND role = ? List<Employee> findBySalaryGreaterThanAndRole( double sal, String role);

Best for: Simple queries with 1-3 conditions.

2. @Query Annotation

Write your own JPQL or native SQL query directly on the method. Useful for complex queries that can't be expressed in method names.

@Query("SELECT e FROM Employee e WHERE e.salary > :minSal") List<Employee> findHighEarners( @Param("minSal") double minSal); // Native SQL @Query(value = "SELECT * FROM employee WHERE YEAR(joining_date) = :yr", nativeQuery = true) List<Employee> findByJoinYear(@Param("yr") int yr);

Best for: Complex queries, aggregations, native SQL.

3. Custom Implementation

Create a separate interface + implementation class for methods that need full EntityManager access or very complex logic. Spring Data JPA auto-discovers and merges it.

interface EmployeeCustomRepo { List<Employee> findByDynamicCriteria(...); } class EmployeeCustomRepoImpl implements EmployeeCustomRepo { @PersistenceContext private EntityManager em; // full EntityManager access here }

Best for: Dynamic queries, Criteria API, full control.

🚨 Custom Impl Naming Rule

The implementation class for a custom repo interface MUST be named [InterfaceName]Impl. Spring Data JPA looks for this naming convention automatically. If you name it anything else, Spring won't find it and you'll get an error.

Top Interview Questions with Answers

“The questions interviewers love to ask about transactions”

What is a transaction and why is it needed?
A transaction is a group of database operations that must all succeed or all fail together. It ensures data consistency. Without transactions, a crash midway through a multi-step operation leaves the database in a corrupted partial state.
What does @Transactional actually do?
Spring creates a proxy around your class. When a @Transactional method is called, the proxy intercepts the call, starts a transaction, invokes your method, then commits on success or rolls back on RuntimeException. Your method code contains no transaction management code.
When does Spring roll back a transaction automatically?
Only on unchecked exceptions (RuntimeException and its subclasses, and Error). For checked exceptions (Exception, IOException, SQLException), Spring commits by default. Use rollbackFor = Exception.class to force rollback on checked exceptions.
What is transaction propagation? What is the default?
Propagation decides what happens to a transaction when one transactional method calls another. The default is REQUIRED: if a transaction exists, join it; if not, create a new one. REQUIRES_NEW always creates an independent transaction regardless.
What is the difference between REQUIRES_NEW and NESTED?
REQUIRES_NEW completely suspends the outer transaction and creates a fully independent transaction. If the outer rolls back, REQUIRES_NEW's commit is unaffected. NESTED creates a savepoint inside the outer transaction. If nested rolls back, only its work is undone (to the savepoint). If the OUTER rolls back, NESTED's work also rolls back.
What is the self-invocation problem with @Transactional?
If method A in the same class calls method B (also @Transactional), the Spring proxy is bypassed because A was already called directly, not through the proxy. Result: B's @Transactional annotation is ignored. Fix: restructure so B is in a different Spring bean, which ensures B is called through its proxy.
What is Spring Data JPA and how does it differ from plain JPA?
Plain JPA requires you to write all DAO implementations manually using EntityManager. Spring Data JPA lets you define a repository interface extending JpaRepository and Spring auto-generates the implementation. It provides save, findById, findAll, delete, and supports derived query methods from method names.
What are the 4 isolation levels?
READ_UNCOMMITTED (allows dirty reads), READ_COMMITTED (prevents dirty reads), REPEATABLE_READ (prevents non-repeatable reads too), SERIALIZABLE (prevents everything including phantom reads, but slowest). MySQL InnoDB default is REPEATABLE_READ.

⚡ Memory Cheat Sheet

“Last-minute revision before your interview”

ACID
Atomicity, Consistency,
Isolation, Durability
Default Propagation
REQUIRED
(join or create)
Auto Rollback
Only RuntimeException
NOT checked exceptions
@Transactional works via
CGLIB Proxy
(public methods only)
REQUIRES_NEW
Independent tx
For audit logs
NESTED
Savepoint inside outer tx
Partial rollback only
Spring Data JPA
Interface + JpaRepository
= free CRUD methods
Derived Query
findByRoleAndSalary
Spring auto-generates SQL
Self-invocation bug
@Transactional ignored
when called within same class
↑ Back to top