Week 5 Day 3 — Transactions Deep Dive
Goal
Today I want to understand transactions in Spring Data JPA.
Main questions:
- What is a transaction?
- Why do we need transactions?
- What does
@Transactionaldo? - Where should I put
@Transactional? - What is the persistence context?
- What is dirty checking?
- What causes rollback?
- What is the difference between checked and unchecked exceptions?
- What does
readOnly = truemean? - What are common transaction traps?
1. Quick Review from Week 5 Day 2
In Day 2, I learned:
- Spring Data JPA can create derived queries from repository method names.
- Repository method names use entity property names, not database column names.
@Querycan define JPQL or native SQL queries.- JPQL uses entity names and fields.
- Native SQL uses table names and columns.
Pageablesupports pagination.Pageincludes total count.Sliceis lighter thanPage.@Modifyingis needed for update/delete queries.
Memory sentence:
Repository methods query entities through Spring Data JPA.
Today I learn how changes are committed or rolled back.
2. What Is a Transaction?
A transaction is a unit of work.
Simple definition:
A transaction groups database operations together so they either all succeed or all fail.
Example:
1. Create invoice
2. Create invoice items
3. Update client balance
4. Write audit log
If step 3 fails, I usually do not want steps 1 and 2 to stay saved.
I want:
all operations succeed
or:
all operations roll back
Memory sentence:
A transaction protects consistency by making a group of operations succeed or fail together.
3. Why Do We Need Transactions?
Without transactions, data can become inconsistent.
Example: money transfer
1. Subtract 100€ from Account A
2. Add 100€ to Account B
If step 1 succeeds but step 2 fails:
money disappears
With a transaction:
if both steps succeed -> commit
if one step fails -> rollback
Memory sentence:
Transactions protect the database from half-finished changes.
4. ACID
Transactions are often described with ACID.
| Letter | Meaning | Simple explanation |
|---|---|---|
| A | Atomicity | all or nothing |
| C | Consistency | data remains valid |
| I | Isolation | transactions do not break each other |
| D | Durability | committed data stays saved |
For certification, the most important word is:
Atomicity = all or nothing
5. What Is @Transactional?
@Transactional tells Spring:
Run this method inside a transaction.
Example:
@Service
public class TaskService {
private final TaskRepository taskRepository;
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
@Transactional
public TaskDto create(CreateTaskRequest request) {
TaskEntity task = new TaskEntity(request.title(), "OPEN");
TaskEntity saved = taskRepository.save(task);
return toDto(saved);
}
}
Spring starts a transaction before the method and commits it after the method succeeds.
If the method fails with a rollback-triggering exception, Spring rolls back the transaction.
Memory sentence:
@Transactionalcreates a transaction boundary around a method.
6. Transaction Boundary
Transaction boundary means:
where the transaction starts and where it ends
Example:
@Transactional
public void completeTask(Long taskId) {
TaskEntity task = taskRepository.findById(taskId).orElseThrow();
task.complete();
}
Boundary:
transaction starts before completeTask()
transaction commits after completeTask() returns successfully
transaction rolls back if completeTask() throws rollback exception
Memory sentence:
A transaction boundary wraps a unit of work.
7. Where Should @Transactional Usually Go?
Usually on service methods.
Good:
@Service
public class TaskService {
@Transactional
public TaskDto completeTask(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
task.complete();
return toDto(task);
}
}
Why service layer?
service contains business operation
service may call multiple repositories
service defines the unit of work
controller should stay HTTP-focused
repository should stay persistence-focused
Memory sentence:
Put transactions around business use cases, usually in the service layer.
8. Controller Should Usually Not Define Transactions
Less ideal:
@RestController
public class TaskController {
@Transactional
@PostMapping("/api/tasks/{id}/complete")
public TaskDto complete(@PathVariable Long id) {
return taskService.completeTask(id);
}
}
Better:
@RestController
public class TaskController {
@PostMapping("/api/tasks/{id}/complete")
public TaskDto complete(@PathVariable Long id) {
return taskService.completeTask(id);
}
}
Service:
@Service
public class TaskService {
@Transactional
public TaskDto completeTask(Long id) {
// business transaction here
}
}
Why?
controller handles HTTP
service handles business transaction
9. Repository Methods Already Have Transactions?
Spring Data repository methods often already have transaction behavior.
For example:
taskRepository.save(task);
can run transactionally.
But in real apps, service methods should still define transactions for business use cases.
Why?
one service method may call multiple repository methods
all operations should commit or rollback together
service defines business unit of work
Example:
@Transactional
public InvoiceDto createInvoice(CreateInvoiceRequest request) {
ClientEntity client = clientRepository.findById(request.clientId()).orElseThrow();
InvoiceEntity invoice = invoiceRepository.save(new InvoiceEntity(client));
auditLogRepository.save(new AuditLogEntity("INVOICE_CREATED"));
return toDto(invoice);
}
All database changes belong to one transaction.
10. What Happens Internally with @Transactional?
Spring uses AOP proxies.
Simplified flow:
1. Client calls service method.
2. Call goes through Spring proxy.
3. Proxy starts transaction.
4. Real service method runs.
5. If method returns normally, proxy commits transaction.
6. If method throws rollback exception, proxy rolls back transaction.
Memory sentence:
@Transactionalworks through Spring AOP proxy.
11. Transaction Proxy Example
I write:
@Service
public class TaskService {
@Transactional
public void completeTask(Long id) {
// business logic
}
}
Spring creates a proxy around it:
TaskService proxy
↓
starts transaction
↓
calls real TaskService.completeTask()
↓
commits or rolls back
This is why some traps happen, especially self-invocation.
12. Self-Invocation Trap
Bad:
@Service
public class TaskService {
public void outerMethod(Long id) {
completeTask(id);
}
@Transactional
public void completeTask(Long id) {
// transactional logic
}
}
Problem:
outerMethod() calls completeTask() inside the same class.
The call does not go through the Spring proxy.
@Transactional may not be applied.
Memory sentence:
@Transactionalworks when the call goes through the Spring proxy.
Better:
put @Transactional on outer public service method
move transactional method to another service
call through another Spring bean
Best simple fix:
@Transactional
public void outerMethod(Long id) {
completeTaskInternal(id);
}
private void completeTaskInternal(Long id) {
// logic
}
13. Method Visibility Trap
@Transactional is normally used on public methods.
Good:
@Transactional
public void completeTask(Long id) {
}
Risky/confusing:
@Transactional
private void completeTask(Long id) {
}
Because Spring proxy-based transaction management usually applies to externally called methods through the proxy.
Exam-safe sentence:
Put
@Transactionalon public service methods that are called from outside the bean.
14. Persistence Context
Persistence context is one of the most important JPA concepts.
Simple definition:
The persistence context is a first-level cache and tracking area for managed entities inside a transaction.
When JPA loads an entity inside a transaction, the entity becomes managed.
Example:
@Transactional
public TaskDto completeTask(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
return toDto(task);
}
Inside the transaction:
task is managed by the persistence context
JPA tracks changes to it.
Memory sentence:
Persistence context tracks managed entities.
15. Managed Entity
A managed entity is an entity currently tracked by the persistence context.
Example:
TaskEntity task = taskRepository.findById(id).orElseThrow();
Inside a transaction, this entity is usually managed.
If I change it:
task.complete();
JPA notices the change.
At commit time, JPA can update the database.
Memory sentence:
Managed entity changes can be saved automatically at commit time.
16. Dirty Checking
Dirty checking means:
JPA automatically detects changes to managed entities and updates the database at flush/commit time.
Example:
@Transactional
public void completeTask(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
}
No explicit save() call.
Still, database can be updated because:
task is managed
JPA tracks changes
transaction commits
dirty checking detects change
SQL update is executed
Memory sentence:
Dirty checking saves changes to managed entities without calling
save()again.
17. Dirty Checking Example
Entity:
@Entity
@Table(name = "tasks")
public class TaskEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String status;
public void complete() {
this.status = "DONE";
}
}
Service:
@Transactional
public void completeTask(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
task.complete();
}
Possible SQL at commit:
update tasks set status = 'DONE' where id = ?
Even though I did not call:
taskRepository.save(task);
18. Do I Need save() After Updating a Managed Entity?
Inside a transaction:
@Transactional
public void updateTitle(Long id, String title) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.changeTitle(title);
}
Usually no save() is needed.
Why?
task is managed
dirty checking tracks changes
commit flushes changes
But for new entities:
TaskEntity task = new TaskEntity("New task", "OPEN");
taskRepository.save(task);
You need save() to persist the new entity.
Memory sentence:
New entity needs
save(). Managed existing entity can update through dirty checking.
19. Flush
Flush means:
Synchronize persistence context changes with the database.
Flush can happen:
before transaction commit
before certain queries
when calling flush()
when repository saveAndFlush() is used
Important:
Flush sends SQL to the database, but commit makes it final.
Memory sentence:
Flush sends changes. Commit finalizes changes.
20. Commit vs Rollback
Commit:
make transaction changes permanent
Rollback:
undo transaction changes
Example:
@Transactional
public void createTaskAndFail() {
taskRepository.save(new TaskEntity("A", "OPEN"));
throw new RuntimeException("fail");
}
Because a runtime exception is thrown:
transaction rolls back
task is not saved permanently
21. Default Rollback Rules
By default, Spring rolls back on:
RuntimeException
Error
By default, Spring does not roll back on:
checked Exception
Example rollback:
@Transactional
public void createTask() {
taskRepository.save(new TaskEntity("A", "OPEN"));
throw new IllegalStateException("Something failed");
}
IllegalStateException is a RuntimeException.
Result:
rollback
Memory sentence:
By default, runtime exceptions roll back; checked exceptions do not.
22. Checked vs Unchecked Exceptions
Unchecked exception:
RuntimeException
Examples:
IllegalArgumentException
IllegalStateException
NullPointerException
ResourceNotFoundException if it extends RuntimeException
Checked exception:
Exception
but not RuntimeException.
Example:
IOException
Checked exceptions must usually be declared or caught.
23. Checked Exception Trap
Example:
@Transactional
public void importTasks() throws IOException {
taskRepository.save(new TaskEntity("A", "OPEN"));
throw new IOException("file failed");
}
Default result:
transaction may commit
Why?
IOException is a checked exception.
Spring does not roll back checked exceptions by default.
Fix:
@Transactional(rollbackFor = IOException.class)
public void importTasks() throws IOException {
taskRepository.save(new TaskEntity("A", "OPEN"));
throw new IOException("file failed");
}
Memory sentence:
Use
rollbackForif a checked exception should roll back.
24. rollbackFor
Example:
@Transactional(rollbackFor = Exception.class)
public void runImport() throws Exception {
// database changes
throw new Exception("fail");
}
This tells Spring:
roll back for Exception too
More specific is often better:
@Transactional(rollbackFor = ImportFailedException.class)
Avoid making everything too broad without thinking.
25. noRollbackFor
Sometimes I do not want rollback for a specific exception.
Example:
@Transactional(noRollbackFor = NotificationFailedException.class)
public void createTaskAndNotify() {
taskRepository.save(new TaskEntity("A", "OPEN"));
notificationService.send();
// if notification fails, maybe task should still be saved
}
Meaning:
Do not roll back for NotificationFailedException.
Use carefully.
Memory sentence:
rollbackForadds rollback rules.noRollbackForexcludes rollback rules.
26. Catching Exceptions Trap
Bad:
@Transactional
public void createTask() {
try {
taskRepository.save(new TaskEntity("A", "OPEN"));
externalCall();
} catch (RuntimeException ex) {
log.warn("Ignored error", ex);
}
}
Problem:
exception is caught and not rethrown
method returns normally
Spring commits the transaction
If I want rollback, I must:
rethrow the exception
or mark transaction rollback-only
Better:
@Transactional
public void createTask() {
taskRepository.save(new TaskEntity("A", "OPEN"));
externalCall();
}
or:
@Transactional
public void createTask() {
try {
taskRepository.save(new TaskEntity("A", "OPEN"));
externalCall();
} catch (RuntimeException ex) {
log.warn("Error", ex);
throw ex;
}
}
Memory sentence:
If you catch and swallow the exception, Spring may commit.
27. readOnly = true
Example:
@Transactional(readOnly = true)
public TaskDto findById(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
return toDto(task);
}
Meaning:
This method is intended only to read data.
Benefits:
can be optimization hint
can improve performance in some cases
communicates intent
helps avoid accidental write logic
Important:
readOnly = true is mainly a hint.
It is not always a strict guarantee that writes are impossible.
Memory sentence:
readOnly = truemeans this transaction is intended for reading.
28. Read-Only Transaction Trap
Bad:
@Transactional(readOnly = true)
public void completeTask(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
}
Problem:
method modifies data but transaction is marked read-only
behavior may be provider/database dependent
changes may not flush as expected
this is wrong design
Correct:
@Transactional
public void completeTask(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
}
Memory sentence:
Do not modify data inside read-only transactions.
29. Read Methods vs Write Methods
Good service style:
@Transactional(readOnly = true)
public TaskDto findById(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
return toDto(task);
}
@Transactional
public TaskDto create(CreateTaskRequest request) {
TaskEntity task = new TaskEntity(request.title(), "OPEN");
return toDto(taskRepository.save(task));
}
@Transactional
public void complete(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
}
Memory sentence:
Read methods can be read-only. Write methods should be read-write.
30. Class-Level @Transactional
You can put @Transactional on a class.
Example:
@Service
@Transactional(readOnly = true)
public class TaskService {
public TaskDto findById(Long id) {
// read-only transaction
}
@Transactional
public TaskDto create(CreateTaskRequest request) {
// overrides class-level readOnly
}
}
Meaning:
all methods are read-only by default
write methods override with @Transactional
This is a common style.
Memory sentence:
Class-level
@Transactionalgives defaults; method-level overrides.
31. Propagation
Propagation controls what happens if a transactional method is called when a transaction already exists.
Default:
Propagation.REQUIRED
Meaning:
join existing transaction if one exists
otherwise start a new transaction
Example:
@Transactional
public void createInvoice() {
saveInvoice();
saveAuditLog();
}
If saveInvoice() and saveAuditLog() also use REQUIRED, they join the same transaction.
Memory sentence:
Default propagation is REQUIRED.
32. Common Propagation Types
| Propagation | Simple meaning |
|---|---|
REQUIRED | join existing or create new |
REQUIRES_NEW | suspend existing and start new transaction |
SUPPORTS | join if exists, otherwise run without transaction |
MANDATORY | must have existing transaction |
NOT_SUPPORTED | run without transaction |
NEVER | fail if transaction exists |
NESTED | nested transaction with savepoint if supported |
For most normal service methods:
REQUIRED is enough
33. REQUIRES_NEW
Example:
@Transactional
public void createTask() {
taskRepository.save(new TaskEntity("A", "OPEN"));
auditService.writeAuditLog("TASK_CREATED");
throw new RuntimeException("main operation failed");
}
Audit service:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAuditLog(String message) {
auditLogRepository.save(new AuditLogEntity(message));
}
Meaning:
audit log runs in its own transaction
main transaction rollback does not necessarily roll back audit transaction
Use carefully.
Memory sentence:
REQUIRES_NEWcreates an independent transaction.
34. Isolation
Isolation controls how transactions see each other’s changes.
Common isolation levels:
DEFAULT
READ_UNCOMMITTED
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE
Most apps use:
Isolation.DEFAULT
which means:
use database default isolation level
For many business apps, database default is enough.
Memory sentence:
Isolation controls visibility between concurrent transactions.
35. Timeout
Timeout defines max transaction duration.
Example:
@Transactional(timeout = 5)
public void runShortOperation() {
// must finish within 5 seconds
}
If transaction takes too long:
transaction may be rolled back
Use for operations that should not hang too long.
36. Transaction and External Calls
Be careful with external API calls inside transactions.
Bad:
@Transactional
public void createTask(CreateTaskRequest request) {
TaskEntity task = taskRepository.save(new TaskEntity(request.title(), "OPEN"));
externalApi.notify(task.getId());
}
Problem:
transaction stays open during network call
network call can be slow
database locks may be held longer
external system may succeed but DB transaction may roll back later
Better options:
commit database change first, then publish event
use outbox pattern
use async processing
use transaction synchronization carefully
Memory sentence:
Keep transactions short and avoid slow external calls inside them.
37. Transaction and Lazy Loading Preview
Lazy relationships often need an open persistence context.
Example:
@Transactional(readOnly = true)
public ClientDto getClient(Long id) {
ClientEntity client = clientRepository.findById(id).orElseThrow();
// accessing lazy relationships may work inside transaction
int taskCount = client.getTasks().size();
return toDto(client, taskCount);
}
Outside transaction, lazy loading can fail with a lazy initialization error.
We will study relationships and lazy loading later.
Memory sentence:
Lazy loading usually needs an open persistence context.
38. Open Session in View Warning
Spring Boot web apps may keep the persistence context open during web request rendering if Open Session in View is enabled.
This can hide lazy loading problems.
But it can also lead to:
database access during view/API serialization
N+1 query problems
less clear transaction boundaries
performance surprises
Good design:
load needed data in service layer
map to DTO inside transaction
return DTO to controller
Memory sentence:
Do not rely on lazy loading during JSON serialization.
39. Transactional Tests
In Spring tests, @Transactional has special behavior.
Example:
@SpringBootTest
@Transactional
class TaskServiceTest {
@Test
void createsTask() {
taskService.create(new CreateTaskRequest("A"));
}
}
In many Spring test setups:
transaction rolls back after test
This keeps database clean.
Memory sentence:
Transactional tests often roll back by default.
40. Full Example: Create Task
Service:
@Service
public class TaskService {
private final TaskRepository taskRepository;
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
@Transactional
public TaskDto create(CreateTaskRequest request) {
TaskEntity task = new TaskEntity(request.title(), "OPEN");
TaskEntity saved = taskRepository.save(task);
return toDto(saved);
}
private TaskDto toDto(TaskEntity entity) {
return new TaskDto(
entity.getId(),
entity.getTitle(),
entity.getStatus()
);
}
}
What happens:
1. Transaction starts.
2. New entity is created.
3. save() persists entity.
4. Method returns DTO.
5. Transaction commits.
6. Insert becomes permanent.
41. Full Example: Update Task with Dirty Checking
Service:
@Transactional
public TaskDto complete(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
task.complete();
return toDto(task);
}
No save() call.
What happens:
1. Transaction starts.
2. Entity is loaded and becomes managed.
3. Entity status changes to DONE.
4. Dirty checking detects change.
5. SQL update runs before commit.
6. Transaction commits.
42. Full Example: Rollback
Service:
@Transactional
public void createTwoTasksAndFail() {
taskRepository.save(new TaskEntity("Task A", "OPEN"));
taskRepository.save(new TaskEntity("Task B", "OPEN"));
throw new RuntimeException("failure");
}
Result:
Task A is rolled back.
Task B is rolled back.
Nothing is permanently saved.
Because:
RuntimeException triggers rollback by default.
43. Full Example: Read-Only Query
Service:
@Transactional(readOnly = true)
public Page<TaskDto> findOpenTasks(int page, int size) {
Pageable pageable = PageRequest.of(
page,
size,
Sort.by(Sort.Direction.DESC, "createdAt")
);
return taskRepository.findByStatus("OPEN", pageable)
.map(this::toDto);
}
Meaning:
read-only transaction
query tasks
map entities to DTOs
no data modifications intended
44. Common Exam Traps
Trap 1
@Transactional usually belongs on service methods, not controllers.
Trap 2
@Transactional works through Spring AOP proxies.
Trap 3
Self-invocation can bypass the transaction proxy.
Trap 4
Private/internal method calls are not a good place for proxy-based @Transactional.
Trap 5
Default propagation is REQUIRED.
Trap 6
Default rollback happens for RuntimeException and Error.
Trap 7
Checked exceptions do not cause rollback by default.
Trap 8
Use rollbackFor for checked exceptions if needed.
Trap 9
Catching and swallowing exceptions can cause commit.
Trap 10
Dirty checking updates managed entities without explicit save().
Trap 11
New entities need save() or persist.
Trap 12
readOnly = true is mainly a hint and should not be used for write methods.
Trap 13
Flush is not the same as commit.
Trap 14
Keep transactions short.
Trap 15
Avoid slow external calls inside transactions.
45. Real Exam Question: Transaction
Question:
What is a transaction?
Answer:
A transaction is a unit of work that groups database operations so they either all succeed together or all roll back together.
46. Real Exam Question: @Transactional
Question:
What does @Transactional do?
Answer:
@Transactional tells Spring to run a method inside a transaction. Spring starts a transaction before the method, commits it if the method succeeds, and rolls it back if a rollback-triggering exception occurs.
47. Real Exam Question: Where to Put It
Question:
Where should @Transactional usually be placed?
Answer:
Usually on service layer methods, because services define business use cases and transaction boundaries.
48. Real Exam Question: Default Rollback
Question:
Which exceptions trigger rollback by default?
Answer:
By default, RuntimeException and Error trigger rollback. Checked exceptions do not trigger rollback by default.
49. Real Exam Question: rollbackFor
Question:
How can I make a checked exception trigger rollback?
Answer:
Use rollbackFor.
@Transactional(rollbackFor = IOException.class)
50. Real Exam Question: Persistence Context
Question:
What is the persistence context?
Answer:
The persistence context is a first-level cache and tracking area for managed JPA entities. It tracks entity changes during a transaction.
51. Real Exam Question: Dirty Checking
Question:
What is dirty checking?
Answer:
Dirty checking is the JPA mechanism that automatically detects changes to managed entities and synchronizes those changes to the database during flush or commit.
52. Real Exam Question: readOnly
Question:
What does @Transactional(readOnly = true) mean?
Answer:
It means the transaction is intended for read-only work. It can be used as an optimization hint and communicates intent, but it should not be used for methods that modify data.
53. Real Exam Question: Self-Invocation
Question:
Why can self-invocation break @Transactional?
Answer:
Because @Transactional is usually applied by a Spring proxy. If a method inside the same class calls another method in the same class, the call does not go through the proxy, so the transactional advice may not apply.
54. Real Exam Question: Flush vs Commit
Question:
What is the difference between flush and commit?
Answer:
Flush synchronizes persistence context changes with the database by sending SQL. Commit makes the transaction changes permanent.
55. Interview Answer
Question:
Explain
@Transactional.
Good answer:
@Transactional defines a transaction boundary around a method. Spring uses AOP proxies to start a transaction before the method, commit it when the method completes successfully, and roll it back when a rollback-triggering exception occurs. It is usually placed on service methods because a service method represents a business unit of work.
56. Interview Answer
Question:
What is dirty checking?
Good answer:
Dirty checking is a JPA feature where changes to managed entities are automatically detected. If an entity is loaded inside a transaction and I change one of its fields, JPA tracks that change in the persistence context and sends an update SQL during flush or commit. That means I often do not need to call save() after modifying an existing managed entity.
57. Interview Answer
Question:
What causes rollback in Spring transactions?
Good answer:
By default, Spring rolls back transactions for unchecked exceptions, meaning RuntimeException, and also for Error. Checked exceptions do not cause rollback by default. If I need rollback for a checked exception, I can configure rollbackFor, for example @Transactional(rollbackFor = IOException.class).
58. Interview Answer
Question:
Why should transactions usually be in the service layer?
Good answer:
The service layer represents business use cases. A single service method may call multiple repositories, update several entities, and write audit logs. These operations should usually commit or roll back together. Controllers should focus on HTTP, and repositories should focus on persistence access, so the service layer is the best place for transaction boundaries.
59. Interview Answer
Question:
What is the self-invocation problem with
@Transactional?
Good answer:
Spring usually applies @Transactional through a proxy. If a method in the same class calls another transactional method directly, the call does not go through the proxy. Because of that, the transactional advice may not run. A common fix is to put @Transactional on the outer public service method or move the transactional method to another Spring bean.
60. Tiny Code Practice
Service:
@Service
public class TaskService {
private final TaskRepository taskRepository;
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
@Transactional
public TaskDto complete(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
task.complete();
return toDto(task);
}
}
Questions:
- Where does the transaction start?
- Is
taskmanaged? - Why is no
save()needed? - When is the database updated?
- What happens if
ResourceNotFoundExceptionextendsRuntimeException?
Answers:
- Before
complete()runs. - Yes, inside the transaction.
- Dirty checking tracks the managed entity change.
- During flush or commit.
- The transaction rolls back by default.
61. Tiny Bug Practice 1
Problem:
@Transactional(readOnly = true)
public void complete(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
task.complete();
}
Question:
What is wrong?
Answer:
The method modifies data but is marked read-only. Use normal @Transactional for write methods.
62. Tiny Bug Practice 2
Problem:
@Transactional
public void importTasks() throws IOException {
taskRepository.save(new TaskEntity("A", "OPEN"));
throw new IOException("File error");
}
Question:
What is the trap?
Answer:
IOException is a checked exception. Checked exceptions do not trigger rollback by default. Use:
@Transactional(rollbackFor = IOException.class)
if rollback is required.
63. Tiny Bug Practice 3
Problem:
public void outer() {
inner();
}
@Transactional
public void inner() {
taskRepository.save(new TaskEntity("A", "OPEN"));
}
Question:
What is wrong?
Answer:
If outer() and inner() are in the same class, the internal call does not go through the Spring proxy. The @Transactional on inner() may not apply.
Practice Questions and Answers
Question 1
What is a transaction?
Answer:
A transaction is a unit of work that groups database operations so they either all succeed or all roll back together.
Question 2
Why do we need transactions?
Answer:
Transactions prevent half-finished database changes and protect data consistency.
Question 3
What does ACID mean?
Answer:
ACID means Atomicity, Consistency, Isolation, and Durability.
Question 4
What does @Transactional do?
Answer:
@Transactional tells Spring to run a method inside a transaction, committing on success and rolling back on rollback-triggering exceptions.
Question 5
Where should @Transactional usually be placed?
Answer:
Usually on public service methods, because services define business use cases and transaction boundaries.
Question 6
Why does @Transactional work through proxies?
Answer:
Spring uses AOP proxies to apply transaction behavior around method calls.
Question 7
What is the self-invocation trap?
Answer:
Self-invocation happens when one method in a class calls another method in the same class. The call bypasses the Spring proxy, so @Transactional may not apply.
Question 8
What is the persistence context?
Answer:
The persistence context is a first-level cache and tracking area for managed JPA entities.
Question 9
What is a managed entity?
Answer:
A managed entity is an entity currently tracked by the persistence context.
Question 10
What is dirty checking?
Answer:
Dirty checking is JPA’s automatic detection of changes to managed entities. Changes are synchronized to the database during flush or commit.
Question 11
Do I need save() after changing a managed entity?
Answer:
Usually no, not for an existing managed entity inside a transaction. Dirty checking can save the changes automatically.
Question 12
What is flush?
Answer:
Flush synchronizes persistence context changes with the database by sending SQL statements.
Question 13
What is the difference between flush and commit?
Answer:
Flush sends SQL to the database. Commit makes the transaction changes permanent.
Question 14
Which exceptions trigger rollback by default?
Answer:
RuntimeException and Error trigger rollback by default.
Question 15
Do checked exceptions trigger rollback by default?
Answer:
No. Checked exceptions do not trigger rollback by default.
Question 16
How can I roll back for a checked exception?
Answer:
Use rollbackFor.
@Transactional(rollbackFor = IOException.class)
Question 17
What happens if I catch and swallow a runtime exception inside a transactional method?
Answer:
If the exception is caught and not rethrown, the method may return normally and Spring may commit the transaction.
Question 18
What does readOnly = true mean?
Answer:
It means the transaction is intended for read-only work. It can be an optimization hint and communicates intent, but should not be used for writes.
Question 19
What is default transaction propagation?
Answer:
The default propagation is REQUIRED.
Question 20
Why should transactions be kept short?
Answer:
Long transactions can hold database resources and locks, reduce performance, and cause problems if they include slow external calls.
Final Memory Sentences
- A transaction is an all-or-nothing unit of work.
@Transactionaldefines a transaction boundary.- Put transactions around business use cases, usually in services.
- Spring applies
@Transactionalthrough AOP proxies. - Self-invocation can bypass
@Transactional. - Default propagation is
REQUIRED. - Persistence context tracks managed entities.
- Dirty checking updates managed entities automatically.
- New entities need
save(). - Existing managed entities can be updated without
save(). - Flush sends SQL.
- Commit makes changes permanent.
- Runtime exceptions roll back by default.
- Checked exceptions do not roll back by default.
- Use
rollbackForfor checked exceptions. - Catching and swallowing exceptions can cause commit.
readOnly = trueis for read methods.- Do not write data in read-only transactions.
- Keep transactions short.
- Avoid slow external calls inside transactions.