What Happens When You Add @Transactional

When you attach @Transactional to a method in Spring, transaction management occurs automatically. If the method succeeds, it commits; if an unchecked exception (RuntimeException, Error) occurs, it rolls back. However, this behavior happens without writing any transaction code in the method body. How is this possible?

The answer lies in proxies. Spring creates a proxy object of the bean annotated with @Transactional, intercepts the method call, and inserts the transaction logic before and after execution. The Proxy pattern is an approach that introduces a surrogate object to control access to the real object or to provide supplementary features.


Structure of the Proxy Pattern

There are three components:

  • Subject: The interface used by the client.
  • RealSubject: The implementation class containing the actual business logic.
  • Proxy: Implements the Subject and holds a reference to the RealSubject to control invocations.

Since the client invokes through the Subject interface, it cannot distinguish between the actual object and the proxy. The proxy intercepts the call, performs supplementary functions like access control, lazy loading, or logging, and then delegates the request to the real object.

Proxies are categorized based on their usage:

  • Protection Proxy: Checks access permissions.
  • Virtual Proxy: Delays the creation of expensive objects.
  • Logging/Caching Proxy: Performs supplementary features before and after invocations.


Creating a Caching Proxy

Let’s apply a caching proxy to a service that retrieves data.

public interface ArticleService {
    Article findById(Long id);
}

class ArticleServiceImpl implements ArticleService {
    @Override
    public Article findById(Long id) {
        // In reality, this is an expensive database retrieval operation
        System.out.println("Retrieve Article from DB: " + id);
        return new Article(id, "Utilizing Proxy Pattern");
    }
}

Create the caching proxy.

class CachingArticleServiceProxy implements ArticleService {
    private final ArticleService delegate;
    private final Map<Long, Article> cache = new HashMap<>();

    public CachingArticleServiceProxy(ArticleService delegate) {
        this.delegate = delegate;
    }

    @Override
    public Article findById(Long id) {
        if (cache.containsKey(id)) {
            System.out.println("Return from cache: " + id);
            return cache.get(id);
        }
        Article article = delegate.findById(id);
        cache.put(id, article);
        return article;
    }
}

The caller remains unaware of the proxy’s existence.

ArticleService service = new CachingArticleServiceProxy(new ArticleServiceImpl());
service.findById(1L); // Retrieve Article from DB: 1
service.findById(1L); // Return from cache: 1

The first invocation delegates to the actual service, while subsequent invocations return from the cache. Not a single line of the actual service code was altered.


JDK Dynamic Proxy

If you write proxies manually as in the example above, you must create a proxy class for every interface. The JDK automates this via the java.lang.reflect.Proxy class.

ArticleService proxy = (ArticleService) Proxy.newProxyInstance(
    ArticleService.class.getClassLoader(),
    new Class[]{ArticleService.class},
    new InvocationHandler() {
        private final ArticleService target = new ArticleServiceImpl();

        @Override
        public Object invoke(Object proxyObj, Method method, Object[] args) throws Throwable {
            System.out.println("[Proxy] Invoking " + method.getName());
            Object result = method.invoke(target, args);
            System.out.println("[Proxy] Completed " + method.getName());
            return result;
        }
    }
);

proxy.findById(1L);
// [Proxy] Invoking findById
// Retrieve Article from DB: 1
// [Proxy] Completed findById

Implementing an InvocationHandler allows you to process all method calls in a single handler. Since proxy classes are generated dynamically at runtime, there is no need to create separate proxies for each interface.

However, JDK Dynamic Proxy only works if an interface is present. To proxy a class directly without an interface, a library like CGLIB is required.


Spring AOP and Proxies

Spring AOP is built upon the Proxy pattern. Annotations like @Transactional, @Cacheable, and @Async operate based on proxies.

JDK Dynamic Proxy vs. CGLIB

Spring supports two methods for generating proxies.

Criteria JDK Dynamic Proxy CGLIB
Requirements Interface required Interface not required
Proxy Target Interface Class (Bytecode manipulation)
Performance Reflection-based Relatively faster via bytecode generation
Spring Boot Default No Default since Spring Boot 2.0

Since Spring Boot 2.0, CGLIB is the default proxy mechanism. It can be changed via the spring.aop.proxy-target-class property, but unless there is a specific reason, maintaining the default is advisable.

Execution Flow of @Transactional

A simplified invocation flow of an @Transactional method is as follows.

sequenceDiagram
    participant C as Client
    participant P as Proxy Object
    participant R as Real Object

    C->>P: Call updateInventory()
    P->>P: Start transaction
    P->>R: Delegate updateInventory()
    R-->>P: Return result
    P->>P: Commit or rollback transaction
    P-->>C: Return result

One caveat is that invoking an @Transactional method from within the same class bypasses the proxy. Proxies only intercept external invocations.

@Service
public class OrderService {

    public void placeOrder() {
        updateInventory(); // Does not go through the proxy! Transaction is not applied.
    }

    @Transactional
    public void updateInventory() {
        // ...
    }
}

This problem can only be discovered if you understand the proxy-based behavior of Spring AOP. It can be bypassed via self-injection or fetching the bean from the ApplicationContext, but separating the design is generally cleaner.


Proxy vs. Decorator

The Decorator pattern and the Proxy pattern are structurally almost identical. Both implement the same interface as the original object and delegate to the original internally.

The difference lies in their intent.

  • Decorator: Its purpose is to add functionality. The client is aware of the decorator’s existence.
  • Proxy: Its purpose is to control access. Ideally, the client remains unaware of the proxy’s existence.

Spring’s @Transactional is an excellent example of the Proxy pattern. Developers do not need to be conscious of the transaction proxy; transactions are applied with just one annotation. On the other hand, Java I/O’s BufferedInputStream is a good example of a Decorator. The developer explicitly wraps the object to add buffering capabilities.


Conclusion

The Proxy pattern controls access to a real object or provides supplementary features by introducing a surrogate object. Because Spring AOP is built on this pattern, understanding proxies is a prerequisite for comprehending the operational mechanics of @Transactional, @Cacheable, and @Async.

In practice, the most frequently encountered pitfall of the Proxy pattern is self-invocation. When a transaction is not applied after calling an @Transactional method within the same class, recalling the proxy structure discussed in this article immediately reveals the cause.