Spring Boot Persistence — Transaction Notes Page 1
Spring Boot Persistence
Transactions — Comprehensive Study Notes
Based on Spring Boot Persistence Best Practices by Anghel Leonard
Chapters: 6 (Connections & Transactions) · Appendix F (Isolation) · Appendix G (Propagation)
Topic Key Annotation / Setting
Read-only query @Transactional(readOnly=true)
Write query @Transactional
New independent Tx @Transactional(propagation=REQUIRES_NEW)
Timeout (seconds) @Transactional(timeout=10)
Global timeout [Link]-timeout=10
Delay connection acquire [Link]-commit=false + provider_disables_autocommit=true
Log transactions [Link]=DEBUG
Quick-Reference Card
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 2
1. What is a Transaction?
A database transaction is a logical unit of work that groups one or more SQL statements so that either all of
them succeed (commit) or none of them persist (rollback). Transactions are governed by the ACID properties:
• Atomicity — All operations in a transaction succeed or none do.
• Consistency — The database moves from one valid state to another.
• Isolation — Concurrent transactions do not see each other's intermediate state.
• Durability — Committed data survives system failures.
In Spring / Hibernate, every SQL statement runs inside a physical database transaction. A non-transactional
context means there are no explicit boundaries (no begin/commit/rollback) — it does not mean the statement
avoids a physical transaction altogether.
✔ Best Practice: Always use explicit (declarative) transactions — even for read-only SELECT statements — to
define proper transactional contexts and avoid subtle performance and correctness pitfalls.
2. @Transactional — Deep Dive
2.1 @Transactional(readOnly = true) — How It Really Works
The readOnly flag tells Hibernate (Spring 5.1+) to load entities in read-only mode, which has meaningful
performance consequences:
Read-Write Mode (readOnly=false) Read-Only Mode (readOnly=true)
Both the entity AND its hydrated/loaded state are kept in Hydrated state is discarded immediately after load — only
the Persistence Context the entity remains
Dirty Checking runs at flush time comparing current vs Dirty Checking is disabled — no automatic UPDATE
hydrated state statements
Flush mode is AUTO — flush occurs before commit or Flush mode is MANUAL — no automatic flush at all
query
Entity status: MANAGED Entity status: READ_ONLY
Versionless Optimistic Locking available Versionless Optimistic Locking disabled
Setting readOnly=true also allows the database to apply its own optimisations (e.g., Oracle skips redo-log
writes for read-only transactions). For Spring versions below 5.1, the hydrated state is still kept in memory; only
[Link] is set — a strong reason to upgrade.
✔ Best Practice: Fetching read-only data via a DTO/Spring Projection is even better than readOnly=true — the
Persistence Context stays completely empty (zero entities managed).
Example — read-only service method:
@Transactional(readOnly = true) public void fetchAuthorReadOnlyMode() { Author author =
[Link]("Anthology"); // Entity status: READ_ONLY, Loaded
state: null // No flush will occur even if you change a field }
Example — DTO projection (Persistence Context stays empty):
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 3
public interface AuthorDto { String getName(); int getAge(); } @Transactional(readOnly =
true) public void fetchAuthorAsDto() { AuthorDto dto =
[Link]("Anthology"); // Persistence Context has 0 managed
entities }
2.2 Drawbacks of a Non-Transactional Context
Omitting @Transactional on read operations exposes the application to several risks:
• Auto-commit mode controls behaviour — varies by JDBC driver, database, and connection pool.
• With auto-commit=true, each SQL statement runs in its own physical transaction and may use a separate
connection.
• No ACID guarantee across multiple SELECT statements in the same method.
• Cannot benefit from Spring optimisations (flush mode = MANUAL, Dirty Checking skipped).
• Cannot benefit from database-specific read-only optimisations.
• Hibernate opens a JDBC transaction but never explicitly commits — behaviour is vendor-specific (MySQL
rolls back, Oracle commits).
• Methods with no explicit read-only marker can be modified by other developers to write data unintentionally.
• Cannot delay connection acquisition (Hibernate 5.2.10+) without disabling auto-commit.
3. When Spring Ignores @Transactional
@Transactional relies on Spring's AOP proxy mechanism. The proxy is bypassed — and the annotation is
silently ignored — in two situations:
• Private / protected / package-private methods — the AOP proxy cannot intercept them.
• Self-invocation — calling an annotated method from another method in the same class bypasses the proxy
entirely.
■ Warning: @Transactional ONLY works on public methods in a class that is invoked via its Spring proxy (i.e.,
called from a different Spring-managed bean).
Broken — self-invocation, proxy bypassed:
@Service public class BookstoreService { public void mainAuthor() { Author author = new
Author(); persistAuthor(author); // ← calls a method in the SAME class — proxy bypassed!
} @Transactional(propagation = Propagation.REQUIRES_NEW) private long
persistAuthor(Author author) { // ← private + same class = IGNORED
[Link](author); return [Link](); } }
Fixed — move the annotated method to a separate Spring bean:
@Service public class HelperService { @Transactional(propagation =
Propagation.REQUIRES_NEW) public long persistAuthor(Author author) { // public + separate
bean = WORKS [Link](author); return [Link](); } } @Service
public class BookstoreService { private final HelperService helperService; public void
mainAuthor() { Author author = new Author(); [Link](author); //
called via proxy — @Transactional respected } }
4. Transaction Timeout and Rollback Verification
4.1 Setting a Timeout
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 4
Spring supports three scopes for setting a transaction timeout:
Scope Mechanism Unit
Method-level @Transactional(timeout = 10) seconds
Class-level @Transactional(timeout = 10) on the class seconds
Global [Link]-timeout=10 ([Link])seconds
Query-level @QueryHint(name="[Link]", value="10") seconds
Query-level @QueryHint(name="[Link]", value="10000")
milliseconds
Programmatic [Link](int n) seconds
Testing timeout with a SLEEP query:
// Repository — use database SLEEP to simulate slow query @Query(value = "SELECT
SLEEP(15)", nativeQuery = true) // MySQL public void sleepQuery(); // Service — timeout
set to 10s, query takes 15s → triggers rollback @Transactional(timeout = 10) public void
newAuthor() { [Link](new Author(...));
[Link](); // → [Link] }
■ Warning: [Link]() after a DML statement will NOT trigger a timeout — only active query execution time
counts toward the timeout. Use a database SLEEP function inside a query instead.
4.2 Verifying Rollback via Logging
Enable transaction logging in [Link]:
[Link]=INFO [Link]=DEBUG
[Link]=DEBUG
[Link]=DEBUG
A timed-out transaction produces log output such as:
Creating new transaction with name [...] ... Opened new EntityManager [...] for JPA
transaction ... statement cancelled due to timeout or client request Initiating
transaction rollback Rolling back JPA transaction on EntityManager [...] Closing JPA
EntityManager [...] after transaction
5. @Transactional in Repository Interfaces
Placing @Transactional annotations at the right layer is critical for both correctness and performance. Here is
the recommended approach.
5.1 Built-in vs. User-Defined Query Methods
Spring Data's built-in query methods (save(), findById(), delete(), count(), …) are inherited from
SimpleJpaRepository and come with default transactional contexts — read-only for finders, read-write for
modifiers.
User-defined query methods (@Query annotations, Query Builder methods) do not receive a default
transactional context. Calling a write method without a transaction throws TransactionRequiredException.
Calling a read method silently runs outside an explicit transaction.
5.2 Recommended Repository Pattern
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 5
@Repository @Transactional(readOnly = true) // default: all methods are read-only public
interface AuthorRepository extends JpaRepository<Author, Long> { @Query("SELECT a FROM
Author a WHERE [Link] = ?1") Author fetchByName(String name); // inherits
@Transactional(readOnly=true) @Transactional // override for write operations @Modifying
@Query("DELETE FROM Author a WHERE [Link] <> ?1") int deleteByNeGenre(String genre); }
✔ Best Practice: This pattern mirrors exactly what Spring Data's SimpleJpaRepository does internally. It was
recommended by Oliver Drotbohm (Spring Data lead): annotate the interface with
@Transactional(readOnly=true) and override with plain @Transactional for modifying methods.
5.3 Service-Level vs. Repository-Level — Which Controls?
When a service method is annotated with @Transactional, each query method called inside it participates in the
existing transaction (default [Link]). Repository-level annotations become irrelevant for that
call — no new transaction or connection is acquired.
// Log output showing participation — single connection, one transaction: // Creating new
transaction with name [[Link]] // ... // Found
thread-bound EntityManager for JPA transaction // Participating in existing transaction
<-- repository annotation has no effect // select ... // delete ... // Committing JPA
transaction
5.4 The Long-Running Transaction Problem
When @Transactional is placed on a service method that contains both database operations and
time-consuming non-database business logic, the database connection is held open for the entire duration — a
long-running transaction.
■ Warning: A long-running transaction holds a database connection open the whole time, reducing pool
availability and hurting scalability. Strive for short, focused transactions.
Two mitigation strategies:
• Delay connection acquisition (Hibernate 5.2.10+): The connection is acquired only when the first SQL is
actually executed, not when the transaction opens.
• Place @Transactional at the repository level: Each query method gets its own short, independent
transaction; the service method itself holds no transaction open between queries.
Delay connection acquisition — [Link]:
[Link]-commit=false
[Link].provider_disables_autocommit=true
✔ Best Practice: For resource-local JPA transactions, always configure HikariCP to disable auto-commit and set
provider_disables_autocommit=true. Never set provider_disables_autocommit=true without also disabling
auto-commit on the pool — Hibernate will not disable it itself.
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 6
6. Common Transactional Scenarios and Best Practices
6.1 Select → Modify (no explicit save() needed)
@Transactional public void updateAuthorGenre() { Author author =
[Link](1L).orElseThrow(); [Link]("History"); // No save()
needed — Dirty Checking detects the change at flush time // Result: 1 transaction, 2 SQL
statements (SELECT + UPDATE) }
Without @Transactional on the service method, findById() and save() run in separate transactions — 2 round
trips, 3 SQL statements, no ACID guarantee.
6.2 Reorder Tasks to Avoid @Transactional
If non-database code precedes a DML call and might throw, consider reordering:
// BAD — non-DB code runs inside transaction, holding connection open @Transactional
public void foo() { [Link](author); // DML first riskyNonDatabaseTask();
// may throw — transaction held during this } // BETTER — non-DB code runs BEFORE
acquiring any transaction public void foo() { riskyNonDatabaseTask(); // throws? save()
never called, no connection used [Link](author); // short-lived
transaction for DML only }
6.3 Cascading — No Extra @Transactional Needed
// save(foo) cascades to Buzz instances — all 3 INSERTs run in one transaction
automatically public void fooAndBuzz() { Foo foo = new Foo(); [Link](new Buzz());
[Link](new Buzz()); [Link](foo); // no @Transactional needed here }
All INSERT statements generated by [Link]/PERSIST share the transaction opened by save(). If
any INSERT fails, all are rolled back automatically.
6.4 Select → Long Task → Save (Optimistic Locking + Retry)
// Two short transactions separated by a long task // + Optimistic Locking prevents lost
updates @Retry(times = 10, on = [Link]) public void
generateAndSaveReport() { Book book = [Link](id).orElseThrow(); // Tx 1
(short) byte[] pdf = [Link](book); // long task — NO active connection
[Link]("PDF"); [Link](book); // Tx 2 (short) }
✔ Best Practice: Do not apply @Retry to a method annotated with @Transactional — the retry would repeat
inside the same failed transaction. Only retry methods that manage their own short, independent transactions.
6.5 Rollback for Checked Exceptions
By default, Spring only rolls back on unchecked exceptions (RuntimeException and its subclasses). To include
checked exceptions:
@Transactional(rollbackFor = [Link]) public void riskyOperation() throws
IOException { // Checked IOException will now also trigger rollback }
7. Golden Rules — Transaction Checklist
Repository interface
• Annotate the interface with @Transactional(readOnly=true).
• Override with plain @Transactional for each modifying query method (@Modifying).
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 7
Service method
• Add @Transactional(readOnly=true) if only read-only query methods are called.
• Add @Transactional if at least one write query method is called.
• Avoid adding @Transactional at the class level — evaluate each method individually.
• Never add @Transactional at the controller class level.
Method visibility
• @Transactional only works on public methods.
• Move logic to a separate Spring bean to avoid self-invocation issues.
Transaction duration
• Strive for short transactions — avoid interleaving heavy business logic with DB calls.
• Use delayed connection acquisition (Hibernate 5.2.10+) for unavoidable long methods.
• Design service methods so non-DB tasks run outside the transaction scope.
Rollback
• By default only RuntimeExceptions trigger rollback. Use rollbackFor for checked exceptions.
• Use @Retry only on non-@Transactional methods.
• Use Versioned Optimistic Locking (@Version) to detect lost updates.
Logging & verification
• Enable [Link]=DEBUG during development to verify transaction boundaries.
• Check for 'Participating in existing transaction' to confirm methods join correctly.
• Verify timeout rollback by checking for 'Initiating transaction rollback' in logs.
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 8
8. Transaction Isolation Levels (Appendix F)
The isolation level controls how visible uncommitted changes of one transaction are to other concurrent
transactions. Set via the isolation element of @Transactional:
@Transactional(isolation = Isolation.READ_COMMITTED)
Isolation Level Dirty Read Non-Repeatable Read Phantom Read Performance
READ_UNCOMMITTED ✘ Allowed ✘ Allowed ✘ Allowed Highest
READ_COMMITTED ✔ Prevented ✘ Allowed ✘ Allowed High
REPEATABLE_READ ✔ Prevented ✔ Prevented ✘ Allowed* Medium
SERIALIZABLE ✔ Prevented ✔ Prevented ✔ Prevented Lowest
* MySQL REPEATABLE_READ prevents non-repeatable reads but still allows lost updates.
Isolation.READ_UNCOMMITTED
A transaction can read data not yet committed by another transaction — dirty reads are possible.
• Step 1: Transaction A updates price from $65,000 to $85,000 (not yet committed).
• Step 2: Transaction B reads the new price $85,000 — a dirty read.
• Step 3: Transaction A rolls back. B has acted on phantom data.
Database notes: MySQL: not supported. Oracle: not supported.
Usage: Rarely used. Avoid unless you understand the consequences.
Isolation.READ_COMMITTED
A transaction only reads committed data. Dirty reads prevented, but non-repeatable reads are possible.
• Step 1: Transaction A and B both read price as $65,000.
• Step 2: Transaction A updates to $85,000 — not yet committed.
• Step 3: Transaction B reads $65,000 (committed value) — no dirty read.
• Step 4: Transaction A commits. Transaction B now reads $85,000 — non-repeatable read.
Database notes: Default in PostgreSQL, SQL Server, Oracle.
Usage: Common default. Suitable for most OLTP applications.
Isolation.REPEATABLE_READ
A transaction reads the same value across multiple reads within the same transaction.
• Step 1: Transaction A and B both read price as $65,000.
• Step 2: Transaction A updates to $85,000 and commits.
• Step 3: Transaction B re-reads price as $65,000 — no non-repeatable read.
Database notes: Default in MySQL. Oracle: not supported natively.
Usage: Good for reports. MySQL still allows lost updates.
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 9
[Link]
Strictest level — equivalent to serial execution. All phenomena prevented (with vendor caveats).
• Step 1: Transaction A reads price and locks the row.
• Step 2: Transaction B tries to read — suspended until A commits or rolls back.
• Step 3: Transaction A commits. Transaction B reads the committed value.
Database notes: PostgreSQL and MySQL: prevents all phenomena. SQL Server: still allows write skews via
MVCC.
Usage: Highest correctness, lowest throughput. Use for financial or critical consistency scenarios.
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 10
9. Transaction Propagation (Appendix G)
Transaction propagation defines what happens when a @Transactional method is called while an existing
physical transaction may or may not already be active. Set via the propagation element:
@Transactional(propagation = Propagation.REQUIRES_NEW).
Overview of all 7 propagation types:
Propagation Existing Tx? Behaviour Rollback Impact
REQUIRED Yes Joins existing Tx Inner rollback rolls back entire physical Tx
(default) No Creates new Tx
REQUIRES_NEW Yes Suspends outer, creates new Tx Inner rollback does NOT affect outer Tx
No Creates new Tx (if exception is caught)
NESTED Yes Uses savepoint inside existing Tx Inner can roll back to savepoint independently
No Creates new Tx (Not supported by Hibernate JPA)
MANDATORY Yes Joins existing Tx Inner rollback rolls back entire physical Tx
No Throws exception
NEVER Yes Throws exception N/A
No Runs without Tx
NOT_SUPPORTED Yes Suspends outer, runs without Tx Exception propagates; outer Tx may roll back
No Runs without Tx
SUPPORTS Yes Joins existing Tx Inner rollback rolls back entire physical Tx (if joined)
No Runs without Tx
[Link] (Default)
The most common propagation. If a physical transaction already exists, the annotated method joins it as a
logical transaction. If none exists, a new physical transaction is created.
• All logical transactions share one physical transaction.
• If any inner logical transaction rolls back, the entire physical transaction is rolled back.
• Catching the exception in the outer transaction still results in an UnexpectedRollbackException because the
rollback-only marker is set.
@Transactional(propagation = [Link]) public void insertFirstAuthor() {
[Link](author1); [Link](); // joins existing
transaction }
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 11
Propagation.REQUIRES_NEW
Always creates a new independent physical transaction. The outer transaction (if any) is suspended and its
database connection remains open but idle during inner execution.
• Inner and outer physical transactions are completely independent.
• If the inner transaction rolls back, it does NOT affect the outer transaction — provided the outer transaction
catches the exception.
• If the outer rolls back after the inner committed, the inner is not affected.
• Warning: each physical transaction needs its own database connection.
@Transactional(propagation = Propagation.REQUIRES_NEW) public void insertSecondAuthor() {
[Link](author2); // This transaction commits/rolls back independently }
[Link]
Creates a savepoint within the existing physical transaction. The inner logical transaction can roll back
independently to the savepoint without affecting the outer transaction.
• Hibernate JPA does NOT support NESTED — throws NestedTransactionNotSupportedException.
• Use JdbcTemplate or a JPA provider that supports nested transactions.
• Useful when partial rollback is needed without losing the outer work.
// With JdbcTemplate: @Transactional(propagation = [Link]) public void
nestedOperation() { // Savepoint created; can rollback to here independently }
[Link]
Requires an existing physical transaction. If none exists, throws IllegalTransactionStateException. Useful for
methods that must always be called within an existing transaction.
• Throws: 'No existing transaction found for transaction marked with propagation mandatory'.
• Once inside an existing transaction, behaves identically to REQUIRED.
@Transactional(propagation = [Link]) public void mustRunInTransaction() {
// Fails if called without an active transaction }
[Link]
Forbids any existing physical transaction. If one exists, throws IllegalTransactionStateException. The method's
code executes non-transactionally.
• Throws: 'Existing transaction found for transaction marked with propagation never'.
• Methods called inside (e.g., save()) will still open their own transactions via REQUIRED.
@Transactional(propagation = [Link]) public void nonTransactionalOperation() {
// Must be called without an active transaction }
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 12
Propagation.NOT_SUPPORTED
If a physical transaction exists, it is suspended. The method runs without a transaction. The suspended
transaction is resumed automatically afterwards.
• The suspended connection stays open (active in the pool) during suspension.
• DML inside the method opens its own short transaction via REQUIRED.
• If an exception propagates to the outer transaction, the outer transaction may be rolled back.
@Transactional(propagation = Propagation.NOT_SUPPORTED) public void
operationOutsideTransaction() { // Outer transaction suspended; this runs without a
transaction }
[Link]
If a physical transaction exists, the method participates in it. If none exists, the method runs without a
transaction.
• When participating in an existing transaction: behaves like REQUIRED (rollback affects all).
• When no transaction exists: individual DML calls manage their own short transactions.
@Transactional(propagation = [Link]) public void optionallyTransactional()
{ // Joins existing transaction if present; otherwise runs non-transactionally }
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)
Spring Boot Persistence — Transaction Notes Page 13
10. Logging Transactions and Query Details
Enabling transaction logging is essential during development to verify that transactions open and close as
expected.
# [Link] — enable transaction and JPA debug logging
[Link]=INFO [Link]=DEBUG
[Link]=DEBUG
[Link]=DEBUG
Key log phrases to look for:
Log Phrase Meaning
Creating new transaction with name [...] A new physical transaction is opening
Opened new EntityManager [...] for JPA transaction Persistence Context created
Participating in existing transaction Method joined an existing transaction (REQUIRED)
Initiating transaction commit Transaction is about to commit
Initiating transaction rollback Transaction is about to rollback
Suspending current transaction Outer transaction suspended (REQUIRES_NEW / NOT_SUPPORTED)
Resuming suspended transaction Outer transaction resumed after inner completes
No isn't
Don't need to create transaction for [...]: This method transactional context — user-defined query method without @Transactional
transactional
11. Quick Decision Guide — Where to Place @Transactional
Question Answer Action
Is this a repository query method? Read-only Ensure interface has @Transactional(readOnly=true)
Is this a repository query method? Write / DML Add @Transactional to that method
Is the service method calling only reads? Yes @Transactional(readOnly=true) on service method
Is the service method calling at least one write? Yes @Transactional on service method
Does the service method have long non-DB logicYes
between DB calls? Split method, or delay connection acquisition, or move @Transactional to rep
Is @Transactional not working? Method is private / self-invoked
Make it public and move to a separate Spring bean
Need exception to trigger rollback? Checked exception @Transactional(rollbackFor = [Link])
Need independent transaction (audit log etc.)? Yes @Transactional(propagation = Propagation.REQUIRES_NEW) in a separate
Notes compiled from Spring Boot Persistence Best Practices by Anghel Leonard (Apress, 2020).
Based on: Spring Boot Persistence Best Practices — Anghel Leonard (Apress 2020)