Question 17 Β· Section 11

At what level can you use @Transactional?

The @Transactional annotation can be placed on a method, class, or interface. But not all options are equally good.

Language versions: English Russian Ukrainian

🟒 Junior Level

The @Transactional annotation can be placed on a method, class, or interface. But not all options are equally good.

The most precise approach. You control each method individually. Why: each method can have its own propagation, isolation, readOnly. This gives maximum flexibility without side effects on neighboring methods.

@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        userRepository.save(user);
    }

    @Transactional(readOnly = true)
    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

Class level

All public methods of the class become transactional. Why: convenient when the entire class is a service with a single responsibility (only CRUD). But if there are non-transactional methods β€” unnecessary overhead on every call.

@Service
@Transactional  // All public methods are transactional
public class UserService {
    public void createUser(User user) { ... }
    public User findById(Long id) { ... }
}

@Transactional on an interface works only with JDK Dynamic Proxy. If Spring uses CGLIB (default for classes without interfaces) β€” the annotation on the interface is ignored. Therefore best practice is to put @Transactional on the class or method.

public interface UserService {
    @Transactional  // Works only with JDK Dynamic Proxy
    User findById(Long id);
}

Where NOT to use

On controller β€” anti-pattern. Controller is responsible for HTTP, transaction for data integrity. Mixing leads to holding a DB connection during JSON serialization.

Golden rule

@Transactional β€” on the service layer (Business Logic Layer). Not on Controller, not on Repository (except custom modifying queries).

When NOT to put @Transactional on class level

  1. There are non-transactional methods β€” unnecessary overhead on every call
  2. Different propagation/isolation β€” have to override on each method, which is confusing
  3. Class contains both read and write methods β€” readOnly has to be set on each read method

🟑 Middle Level

Annotation priority

If annotation is on both class and method β€” method wins.

@Service
@Transactional(readOnly = true)  // Default for all methods
public class UserService {

    public User findById(Long id) { ... }  // readOnly = true (inherits)

    @Transactional  // Overrides β€” readOnly = false, REQUIRED
    public void updateUser(User user) { ... }
}

Repository level

Spring Data repositories are already marked with @Transactional(readOnly = true).

public interface UserRepository extends JpaRepository<User, Long> {
    // findById, findAll β€” already transactional (readOnly)

    @Modifying
    @Transactional  // Required for UPDATE/DELETE
    @Query("UPDATE User u SET u.active = false WHERE u.id = :id")
    void deactivate(@Param("id") Long id);
}

Important: If you call a @Modifying method from a service with @Transactional(readOnly = true) β€” you get TransactionRequiredException, because the service parameter overrides the repository parameter.

Why @Transactional on Controller is anti-pattern

@RestController
@Transactional  // BAD!
public class UserController {
    @PostMapping("/users")
    public User create(@RequestBody User user) {
        User saved = userService.create(user);
        return saved;  // JSON serialization inside transaction!
    }
}

Problems:

  • DB connection held during JSON serialization
  • Lazy-loading triggers N+1 queries in controller layer
  • Under load β†’ connection pool exhaustion

Level comparison

Level Precision Flexibility When to use
Method High High Always recommended
Class Medium Medium When all methods same type (only CRUD)
Interface Low Low ❌ Not recommended
Controller β€” β€” ❌ Anti-pattern

Common mistakes

Mistake What happens How to fix
@Transactional on interface + CGLIB Annotation not visible, no transaction Put on implementation class
@Modifying inside readOnly service TransactionRequiredException Override readOnly = false
Class-level readOnly = true without override Write methods also readOnly Explicitly annotate write methods
@Transactional on final method CGLIB cannot override β€” annotation ignored Remove final

When NOT to use

Situation Why Alternative
Simple SELECT by ID Excessive No @Transactional or readOnly = true
HTTP call in method Holds DB connection during network I/O Move HTTP outside transaction
Background processing Can be asynchronous @Async with separate transaction
Caching Doesn’t require atomicity No transaction

πŸ”΄ Senior Level

Annotation Resolution Order

// AbstractFallbackTransactionAttributeSource
protected TransactionAttribute findTransactionAttribute(Method method, Class<?> targetClass) {
    // 1. Check method first (highest priority)
    TransactionAttribute attr = determineTransactionAttribute(method);
    if (attr != null) return attr;

    // 2. Check method's declaring class
    attr = determineTransactionAttribute(method.getDeclaringClass());
    if (attr != null) return attr;

    // 3. Check bridge methods (interface vs implementation)
    if (ClassUtils.isUserLevelMethod(method, targetClass)) {
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        attr = determineTransactionAttribute(specificMethod);
        if (attr != null) return attr;

        attr = determineTransactionAttribute(specificMethod.getDeclaringClass());
    }
    return null;
}

Key principle: Method-level annotation ALWAYS overrides class-level. Most specific match wins.

Proxy Type Impact on Annotation Detection

JDK Dynamic Proxy (interface-based)

public interface UserService {
    @Transactional
    User findById(Long id);
}

@Service
public class UserServiceImpl implements UserService {
    // No @Transactional β€” WORKS, proxy on interface
    public User findById(Long id) { ... }
}

CGLIB Proxy (class-based)

public interface UserService {
    @Transactional  // IGNORED with CGLIB!
    User findById(Long id);
}

@Service
public class UserServiceImpl implements UserService {
    // No @Transactional β€” DOESN'T WORK with CGLIB
    public User findById(Long id) { ... }
}

CGLIB creates a subclass of UserServiceImpl, not a proxy of the interface. Interface annotation is not visible.

Solution: Always put @Transactional on implementation class/method, never on interfaces.

OSIV (Open Session in View) β€” Deep Analysis

# application.yml β€” true by default!
spring:
  jpa:
    open-in-view: true

With OSIV:

Request β†’ Filter opens Session β†’ Controller β†’ Service β†’ View render β†’ Filter closes Session

Problems:

  1. N+1 queries: Lazy loading during JSON serialization triggers additional queries
  2. Connection held: Connection occupied while view renders
  3. Hidden performance issues: Lazy loading masks inefficient fetching

Solution β€” disable OSIV:

spring:
  jpa:
    open-in-view: false
@Transactional(readOnly = true)
public UserDTO getUserWithDetails(Long id) {
    User user = userRepository.findByIdWithDetails(id);  // Eager fetch
    return UserDTO.from(user);  // Fully loaded entity
}

Edge Cases (minimum 3)

  1. Bridge methods: Java generates bridge methods with generics. Spring must correctly resolve @Transactional from bridge method to target method. Some Spring versions had bugs with @Transactional on generic methods.

  2. Inheritance with overriding: If BaseService.save() has @Transactional(timeout = 10), and DerivedService.save() β€” no annotation, behavior depends on proxy type. CGLIB may not find annotation in superClass under certain conditions.

  3. Multiple @Transactional on same method: If method inherited @Transactional from both interface and class β€” only one applies (order undefined). Spring takes the first found.

  4. @Transactional on @Configuration classes: Bean creation methods inside @Configuration classes are not automatically wrapped in transactions. @Transactional on @Bean method has no effect β€” bean created outside transaction context.

Layer Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Controller Layer                        β”‚
β”‚  - HTTP request/response                 β”‚
β”‚  - Validation                            β”‚
β”‚  - NO @Transactional                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Service Layer                           β”‚
β”‚  - Business logic                        β”‚
β”‚  - @Transactional HERE βœ“                β”‚
β”‚  - Transaction boundaries defined here   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Repository Layer                        β”‚
β”‚  - Data access                           β”‚
β”‚  - Spring Data auto-transactional        β”‚
β”‚  - Only @Transactional for @Modifying    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Rule: Transaction boundaries should match business use-case boundaries, not technical operations.

Performance Numbers

Operation Time
Annotation resolution (Spring cache) ~0.1 ΞΌs
Proxy invocation ~1-2 ΞΌs
Transaction setup ~5-10 ΞΌs
Connection acquisition (HikariCP) ~50-200 ΞΌs

Memory Implications

  • Class-level @Transactional creates proxy for all public methods
  • Each method resolves TransactionAttribute (cached)
  • ThreadLocal entries: ~1 KB per active transaction
  • Hibernate L1 cache: grows linearly with loaded entities

Thread Safety

The annotation itself is immutable and thread-safe. Transaction state stored in ThreadLocal β€” each thread isolated. Singleton bean safe under concurrent calls.

Production War Story

Microservices project: @Transactional placed on UserService interface. In dev β€” JDK Dynamic Proxy worked (interface existed). In production β€” CGLIB (Spring Boot auto-config). Result: in production transactions didn’t open, data committed without rollback on errors. Bug surfaced only under load. Fix: move @Transactional to implementation class.

Monitoring

Debug logging:

logging:
  level:
    org.springframework.transaction: DEBUG
    org.springframework.aop: TRACE

Actuator:

GET /actuator/beans  # Shows which beans are wrapped in proxy

Programmatically:

AopUtils.isAopProxy(bean)  // true if proxy created
AopUtils.isCglibProxy(bean)  // true if CGLIB

Highload Best Practices

  1. Method-level granularity: Precise control β€” only write methods with @Transactional, read methods with readOnly = true.
  2. Disable OSIV in production: spring.jpa.open-in-view: false.
  3. Never put @Transactional on interfaces: CGLIB β€” silent failure.
  4. Avoid class-level @Transactional in large services β€” some methods may not need transaction.
  5. Consider connection pool sizing: @Transactional holds connection for entire method duration. Long methods = pool exhaustion.
  6. Use @Transactional(readOnly = true) on query services: Can route to read replica via AbstractRoutingDataSource.

🎯 Interview Cheat Sheet

Must know:

  • @Transactional placed on method (recommended), class, or interface
  • Method wins over class β€” method-level annotation always overrides class-level
  • Golden rule: @Transactional on service layer, NOT on Controller or Repository
  • @Transactional on interface works only with JDK Dynamic Proxy, ignored with CGLIB
  • Spring Data repositories already have @Transactional(readOnly = true) β€” @Modifying requires override
  • OSIV (Open Session in View) true by default β€” causes N+1 queries, recommended to disable

Common follow-up questions:

  • Why @Transactional on Controller is anti-pattern? β€” Connection held during JSON serialization, lazy loading N+1
  • What is OSIV and why disable it? β€” Session open until render view, masks N+1, holds connection
  • When is class-level @Transactional justified? β€” When all methods same type (only CRUD)
  • What happens with @Modifying method inside readOnly service? β€” TransactionRequiredException

Red flags (DO NOT say):

  • β€œ@Transactional on interface always works” β€” CGLIB silently ignores
  • β€œController with @Transactional β€” normal practice” β€” connection pool exhaustion risk
  • β€œOSIV = good” β€” masks performance issues, N+1 queries

Related topics:

  • [[16. What is @Transactional annotation]]
  • [[21. What is readonly transaction]]
  • [[22. What happens when calling @Transactional method from another method of the same class]]
  • [[13. What is Propagation in Spring]]