At What Level Can Transactional Be Used
In Spring Framework, the @Transactional annotation can be placed at three distinct structural levels:
🟢 Junior Level
In Spring Framework, the @Transactional annotation can be placed at three distinct structural levels:
- Method Level (The industry gold standard).
- Class Level (Applies to all
publicmethods within the class). - Interface Level (Legacy approach, strongly discouraged in modern Spring Boot).
The Core Concept in 30 Seconds
- Method Level: Delivers maximum granularity and control. Each operation specifies its own propagation, timeout, isolation level, read-only mode, and rollback rules.
- Class Level: Acts as a sensible baseline template for homogeneous services (e.g., query-only dictionary services where all methods are read-only).
- Interface Level: ⚠️ Dangerous Trap! In modern Spring Boot, class-based CGLIB proxying is enabled by default. Under CGLIB, annotations placed on interfaces are completely ignored and fail silently.
Where to Place @Transactional in Layered Architecture:
[ Controller / RestController ] ───► ❌ NEVER (Anti-Pattern! Monopolizes DB connection during HTTP I/O)
│
▼
[ Service Layer ] ───► ⭐️ STRICTLY HERE (Defines business transactional boundaries)
│
▼
[ Repository Layer ] ───► ⚠️ Handled by Spring Data; used only for custom @Modifying DML
Precedence Example: Method Overrides Class
@Service
@Transactional(readOnly = true) // 1. Baseline template for all public methods in the class
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// Inherits readOnly = true from class-level annotation:
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
}
// 2. Method OVERRIDES class baseline: opens a read-write transaction!
@Transactional(readOnly = false)
public void updateUserEmail(Long id, String newEmail) {
User user = findById(id);
user.setEmail(newEmail);
userRepository.save(user);
}
}
🟡 Middle Level
Annotation Resolution Hierarchy (Resolution Order)
When Spring AOP intercepts a method invocation, AbstractFallbackTransactionAttributeSource inspects transactional metadata according to a strict priority hierarchy from most specific to least specific:
TransactionAttribute Lookup Order:
1. Target class method implementation [HIGHEST PRIORITY]
└── If present ──► USE THIS METADATA
2. Target class declaration
└── If present ──► USE THIS METADATA
3. Interface method declaration
└── Works ONLY with JDK Dynamic Proxies!
4. Interface declaration [LOWEST PRIORITY]
└── Works ONLY with JDK Dynamic Proxies!
Crucial Rule: An annotation declared on a method always completely overrides any class-level annotation.
Comparison Across Placement Levels
| Placement Level | Advantages | Risks & Disadvantages | Recommended Practice |
|---|---|---|---|
| Service Method | Maximum precision; explicit read/write separation; custom timeouts | Requires more annotations | ⭐️ Industry Best Practice |
| Service Class | Reduces boilerplate in pure CRUD services | Easy to accidentally execute slow queries in write transactions | Acceptable with readOnly = true |
| Interface | Clean interface declarations | Silently ignored by CGLIB in Spring Boot | ❌ Strictly Avoid |
| Controller | None | Starves connection pool during HTTP transfer and JSON parsing | ❌ Severe Anti-Pattern |
| Repository | Spring Data defaults to readOnly = true |
Scoped to a single SQL query; cannot orchestrate business units | Only for @Modifying queries |
Why @Transactional on Controllers Destroys Highload Systems
Placing @Transactional on a @RestController or @Controller endpoint is a devastating architectural anti-pattern:
- Connection Monopolization: A worker thread checks out a database connection from HikariCP before verifying the HTTP request payload and holds it throughout the entire JSON serialization phase.
- Slow Client Vulnerability (Slowloris Effect): If a mobile client downloads a 5 MB JSON payload over a weak 3G network over 10–15 seconds, the physical database connection remains locked and idle for those entire 15 seconds.
- Connection Pool Starvation: Just 30 to 50 concurrent slow downloads will completely deplete the HikariCP pool, causing immediate
ConnectionTimeoutExceptionfailures across the entire microservice.
🔴 Senior Level
The Critical “No Merging of Attributes” Trap
One of the most dangerous misconceptions among developers:
In Spring Framework,
@Transactionalattributes DO NOT merge! A method-level annotation completely REPLACES class-level metadata.
@Service
@Transactional(
readOnly = true,
rollbackFor = Exception.class,
timeout = 10
)
public class PaymentProcessingService {
// 💥 DANGEROUS TRAP!
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processPayment() {
// The developer expects:
// - propagation = REQUIRES_NEW
// - readOnly = false (default)
// - rollbackFor = Exception.class (inherited from class?? NO!)
// - timeout = 10 (inherited from class?? NO!)
// ACTUAL RUNTIME CONFIGURATION:
// rollbackFor reverts to RuntimeException.class!
// timeout reverts to -1 (unlimited)!
}
}
If processPayment() throws a checked exception (PaymentCheckedException), the transaction will commit, because rollbackFor = Exception.class declared on the class was wiped out by the method-level declaration!
CGLIB vs. JDK Dynamic Proxy: Why Interface Annotations Fail
Spring Boot enables class-based proxying by default:
spring.aop.proxy-target-class = true
This forces Spring Boot to use CGLIB (via ByteBuddy), which generates dynamic subclasses (OrderServiceImpl$$SpringCGLIB$$0 extends OrderServiceImpl):
- According to the Java Language Specification (JLS §9.6.4.3), annotations on interfaces are never inherited by implementing classes, even if marked with
@Inherited. - CGLIB subclasses the implementation class (
OrderServiceImpl), but the compiled bytecode ofOrderServiceImplcontains no@Transactionalannotation (it was declared on the interface). - When the CGLIB proxy reflects on the implementation class, it sees an unannotated class.
- Result: No compilation errors or startup warnings are generated, but transactions simply do not open at runtime, running silently in auto-commit mode!
Interaction with Open Session in View (OSIV)
Spring Boot leaves spring.jpa.open-in-view = true enabled by default:
- An
OpenSessionInViewFilterintercepts the HTTP request and opens a HibernateSessionat the servlet boundary. - When
@Transactionalis placed on the service layer, the physical database transaction commits inside the service method. - However, the Hibernate session remains open in the controller layer, allowing Jackson during JSON serialization to navigate uninitialized lazy-loaded relationships, triggering silent N+1 query storms.
- Senior Best Practice: Always set
spring.jpa.open-in-view = falseinapplication.yml, and fetch all required associations eagerly in the service layer usingJOIN FETCHor JPA Entity Graphs within a@Transactional(readOnly = true)method.
4 Tricky Questions
1. What happens if a repository method marked with @Modifying is called from a service having class-level @Transactional(readOnly = true)?
Answer: The invocation fails at runtime with a database transaction error:
- In PostgreSQL:
org.postgresql.util.PSQLException: ERROR: cannot execute UPDATE in a read-only transaction. - In Spring/JPA:
InvalidDataAccessApiUsageException: Executing an update/delete query... TransactionRequiredException. Underlying Mechanism: By default propagation rules (REQUIRED), the repository method participates in the existing transaction opened by the service. Because the service configuredreadOnly = true, the database session was switched to read-only mode (SET TRANSACTION READ ONLY). To permit write mutations, the service method must explicitly override the class setting with@Transactional(readOnly = false).
2. Why did interface-level @Transactional work in legacy Spring Framework 3/4 projects, but fails silently in Spring Boot 2/3?
Answer:
Legacy Spring Framework defaulted to standard JDK Dynamic Proxies when a bean implemented at least one interface. JDK Dynamic Proxies are built directly on the interface and inspect interface method metadata, successfully detecting interface annotations.
Starting with Spring Boot 2.0+, Spring switched the default to spring.aop.proxy-target-class = true (enforcing CGLIB subclass proxying). Because Java does not inherit interface annotations onto implementing classes, CGLIB subclass inspection cannot see interface annotations, causing transactions to be skipped entirely.
3. Do timeout and rollbackFor attributes merge between class and method if a class declares timeout = 5 and a method declares rollbackFor = Exception.class?
Answer: No, they do not merge. Spring adheres to a full override policy. When an annotation is present on a method, Spring completely ignores class-level metadata. The method will run with:
rollbackFor = Exception.class(from the method);timeout = -1(the default value of@Transactional.timeout(), completely discarding the class-leveltimeout = 5).
4. What is the fundamental difference between @Transactional(readOnly = true) on the service layer versus relying on Spring Data JPA’s built-in repository read-only transactions?
Answer:
- At Repository Level: Each Spring Data repository method (
SimpleJpaRepository) opens a brief, isolated read-only transaction strictly for the duration of a single SQLSELECT. - At Service Level: The read-only transaction spans the entire business workflow, providing three major advantages:
- Consistent Snapshot: Multiple consecutive
SELECTstatements read from a consistent database snapshot (underRepeatable Reador via the Hibernate L1 cache). - Lazy Loading Window: Uninitialized entity relationships can be safely navigated throughout the service method without throwing
LazyInitializationException. - Dirty Checking Elimination: Hibernate sets the persistence context FlushMode to
MANUAL, completely disabling dirty checking memory scans and boosting performance.
- Consistent Snapshot: Multiple consecutive
🎯 Interview Cheat Sheet
- Supported Levels: Method level, Class level, and Interface level.
- Recommended Standard: Place
@Transactionalon public methods in the@Servicelayer. - Resolution Order: Class Method $\rightarrow$ Class $\rightarrow$ Interface Method $\rightarrow$ Interface.
- No Attribute Merging: Method-level annotations completely replace class-level metadata; unspecified attributes reset to defaults rather than inheriting class values.
- Interface Trap: CGLIB subclass proxies in Spring Boot completely ignore annotations on interfaces.
- Controller Anti-Pattern: Never annotate controllers; doing so holds database connections open during HTTP transfer and JSON parsing, exhausting the connection pool.
- OSIV Warning: Disable
spring.jpa.open-in-viewto prevent unexpected N+1 queries from executing outside service transaction boundaries.