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.
“The bank transfer problem”
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.
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.
All operations are grouped. On success → commit (save everything). On failure → rollback (undo everything). Database always stays in a consistent state.
“The 4 golden rules of database transactions”
“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.
“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.
“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.
“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.
“You put one annotation and Spring does all the heavy lifting”
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:
employeeService.addEmployee(dto)JpaTransactionManager to begin a transactionIf 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 does nothing! Spring's proxy can only intercept public methods. If you put @Transactional on a private or protected method, it is silently ignored.
“What should happen to the transaction when one method calls another?”
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.
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.
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.
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.
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).
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.
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.
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.
“How much 'sneaking a peek' is allowed between concurrent transactions?”
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.
| Level | Can Read Uncommitted? | Dirty Read? | Non-Repeatable Read? | Phantom Read? |
|---|---|---|---|---|
READ_UNCOMMITTED | Yes | ✅ Possible | ✅ Possible | ✅ Possible |
READ_COMMITTED | No | ❌ Prevented | ✅ Possible | ✅ Possible |
REPEATABLE_READ | No | ❌ Prevented | ❌ Prevented | ✅ Possible |
SERIALIZABLE | No | ❌ Prevented | ❌ Prevented | ❌ Prevented |
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.
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.
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.
“Spring only rolls back automatically for certain exceptions”
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!
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.
“One database vs many databases in one transaction”
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.
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.
“Even less code — Spring writes the DAO for you”
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.
What JpaRepository gives you for free:
save(entity) — Insert or Update (if ID exists, it merges)findById(id) — Returns Optional<Employee>findAll() — All recordsdeleteById(id) — Delete by primary keycount() — Total recordsexistsById(id) — Check if exists“Three ways to add your own queries”
Name your method following Spring Data JPA naming conventions, and Spring generates the query automatically from the method name.
Best for: Simple queries with 1-3 conditions.
Write your own JPQL or native SQL query directly on the method. Useful for complex queries that can't be expressed in method names.
Best for: Complex queries, aggregations, native SQL.
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.
Best for: Dynamic queries, Criteria API, full control.
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.
“The questions interviewers love to ask about transactions”
“Last-minute revision before your interview”