🍃 Section 5 · Question #14

When does Spring create a proxy

Spring does not create proxies for all beans by default. Plain beans without cross-cutting behavior remain standard, unproxied Java instances stored directly in the DefaultLista...


🟢 Junior Level

Spring does not create proxies for all beans by default. Plain beans without cross-cutting behavior remain standard, unproxied Java instances stored directly in the DefaultListableBeanFactory cache. Spring creates a dynamic runtime proxy (surrogate wrapper) only when an explicit functional or architectural trigger is detected.

The 5 Primary Proxy Triggers

  1. AOP and Declarative Infrastructure Annotations:
    • @Transactional: Injects transaction boundary management (commit / rollback).
    • @Async: Offloads execution to a background TaskExecutor thread pool.
    • @Cacheable, @CachePut, @CacheEvict: Coordinates cache lookups and invalidations.
    • @PreAuthorize, @Secured: Enforces method-level Spring Security authorization checks.
    • @Validated: Intercepts method arguments to run Jakarta Bean Validation constraints on service-layer beans.
  2. Configuration Classes (@Configuration):
    • Classes annotated with @Configuration(proxyBeanMethods = true) are enhanced to intercept inter-bean method calls and enforce singleton semantics.
  3. Scoped Proxies (@RequestScope, @SessionScope):
    • Wraps short-lived web-scoped beans so they can be safely injected into long-lived singletons without state corruption.
  4. Spring Data Repository Interfaces (JpaRepository, CrudRepository):
    • Automatically provides runtime implementations for repository interfaces that have no Java class definitions.
  5. Lazy Dependency Injection (@Lazy):
    • Delays the actual instantiation of a heavy bean until a caller first invokes one of its methods.
package com.example.service;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    // Trigger 1: Transaction interceptor required
    @Transactional
    public void updateUser(Long id, String email) { }

    // Trigger 2: Caching interceptor required
    @Cacheable("users")
    public User findById(Long id) { return null; }

    // Trigger 3: Asynchronous task executor required
    @Async
    public void sendEmailNotification(String email) { }
}

[!NOTE] Even if only one method in a class contains an AOP annotation, Spring wraps the entire bean instance in a dynamic proxy. All external calls to any method on that bean will pass through the proxy wrapper.


🟡 Middle Level

The 5 Architectural Proxy Scenarios

                             ┌───────────────────────────────────────────────┐
                             │       When Does Spring Generate a Proxy?      │
                             └───────────────────────┬───────────────────────┘
                                                     │
         ┌───────────────────┬───────────────────────┼───────────────────────┬───────────────────┐
         ▼                   ▼                       ▼                       ▼                   ▼
 ┌───────────────┐   ┌───────────────┐       ┌───────────────┐       ┌───────────────┐   ┌───────────────┐
 │   Spring AOP  │   │ @Configuration│       │ Scoped Proxy  │       │  Spring Data  │   │     @Lazy     │
 │  Interceptors │   │   Classes     │       │ (Web Scopes)  │       │  Repositories │   │  Dependencies │
 ├───────────────┤   ├───────────────┤       ├───────────────┤       ├───────────────┤   ├───────────────┤
 │@Transactional │   │ proxyBean-    │       │ @RequestScope │       │ JpaRepository │   │ Defers real   │
 │@Async, @Cache │   │ Methods=true  │       │ @SessionScope │       │ CrudRepository│   │ instantiation │
 └───────────────┘   └───────────────┘       └───────────────┘       └───────────────┘   └───────────────┘

1. Spring AOP & Custom Aspects

Spring scans all beans against registered Advisor instances. An Advisor combines an Advice (what code to execute) and a Pointcut (where to execute it). If any method of a bean matches a Pointcut:

  • Declarative infrastructure annotations (@Transactional, @Async, @Cacheable, @Retryable).
  • Custom AspectJ aspects defined via @Aspect (e.g., @Around("@annotation(LogExecutionTime)")).

Spring wraps the target bean in a CGLIB or JDK Dynamic Proxy during bean post-processing.

2. @Configuration(proxyBeanMethods = true) (Full Mode)

By default in Spring Framework and Spring Boot, configuration classes run in Full Mode:

@Configuration(proxyBeanMethods = true) // true by default
public class AppConfig {

    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        // Calling dataSource() here is intercepted by CGLIB!
        // Instead of calling the Java method and creating a 2nd DataSource,
        // CGLIB retrieves the already-created singleton from the BeanFactory.
        return new JdbcTemplate(dataSource());
    }
}

If proxyBeanMethods = false (Lite Mode), no proxy is generated, and calling dataSource() directly creates a separate, unmanaged instance.

3. Scoped Web Proxies

Injecting an HTTP-request-scoped bean directly into a singleton service creates a scope mismatch: the singleton is created once at startup when no HTTP request exists.

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserRequestContext {
    private String jwtToken;
    // Getters and setters
}

Spring injects a ScopedProxyFactoryBean (a CGLIB proxy). When a method is called on UserRequestContext, the proxy dynamically looks up the active RequestContextHolder for the calling thread and delegates to the request-specific instance.

4. Dynamic Spring Data Repositories

When you declare:

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

There is no underlying .java class implementing UserRepository. At application startup, Spring Data’s RepositoryFactorySupport creates a JDK Dynamic Proxy implementing the interface. Method calls are routed to QueryExecutorMethodInterceptor, which parses the method name or queries @Query annotations, builds JPQL/SQL, and executes them via EntityManager.

5. Lazy Injections (@Lazy)

Placing @Lazy on an @Autowired injection point instructs Spring not to create the target bean during application startup:

@Service
public class OrderService {

    @Autowired
    @Lazy
    private HeavyAuditReportService auditService; // Injected with a lightweight synthetic proxy
}

Spring injects a synthetic proxy backed by LazyInitTargetSource. The actual HeavyAuditReportService bean is instantiated only upon the first method call made to auditService.


🔴 Senior Level

Under the Hood: AnnotationAwareAspectJAutoProxyCreator

Dynamic proxy creation for AOP beans is coordinated by a specialized BeanPostProcessor named AnnotationAwareAspectJAutoProxyCreator (inheriting from AbstractAutoProxyCreator).

Bean Lifecycle Pipeline:
Instantiate Raw Bean ──> Populate Properties ──> Initialize Bean (@PostConstruct / afterPropertiesSet)
                                                               │
                                                               ▼
                             [AbstractAutoProxyCreator.postProcessAfterInitialization()]
                                                               │
                    ┌──────────────────────────────────────────┴──────────────────────────────────────────┐
                    │ 1. getAdvicesAndAdvisorsForBean(beanClass, beanName)                                │
                    │    • Evaluates Pointcuts across all registered Advisors                             │
                    │ 2. If advisors found:                                                               │
                    │    • createProxy(beanClass, beanName, specificInterceptors, targetSource)           │
                    │    • ProxyFactory configures JDK Proxy or CGLIB Enhancer                            │
                    │    • Returns Proxy instance                                                         │
                    │ 3. If no advisors found:                                                            │
                    │    • Returns original, unproxied bean instance                                      │
                    └─────────────────────────────────────────────────────────────────────────────────────┘
                                                               │
                                                               ▼
                                             Singleton Cached in Level 1 Cache

The Early Proxy Creation Exception (Circular Dependencies)

Standard AOP proxies are created in Phase 4 (Post-Initialization). However, if a bean participates in a circular dependency (e.g., ServiceA ↔ ServiceB):

  1. ServiceA is instantiated as a raw instance.
  2. ServiceA publishes an ObjectFactory lambda to the Level 3 Cache (singletonFactories).
  3. ServiceA attempts to inject ServiceB.
  4. ServiceB is instantiated and requires ServiceA.
  5. Spring invokes singletonFactories.get("serviceA"), which executes:
    SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(rawBean, beanName);
    
  6. AbstractAutoProxyCreator detects the circular reference and creates the AOP Proxy prematurely before ServiceA has even completed property population or @PostConstruct!
  7. The early proxy is cached in the Level 2 Cache (earlySingletonObjects) and injected into ServiceB.
  8. When ServiceA finally reaches postProcessAfterInitialization, AbstractAutoProxyCreator checks its earlyProxyReferences map, detects that the proxy was already created, and returns the raw instance without wrapping it a second time.

AOP Proxies vs @Configuration Proxies: Fundamental Divergence

Developers frequently confuse AOP proxies with @Configuration class proxies. They are created at entirely different container lifecycle phases by different components:

Criterion AOP Proxy (@Transactional, etc.) Configuration Proxy (@Configuration)
Creator Component BeanPostProcessor (AnnotationAwareAspectJAutoProxyCreator) BeanFactoryPostProcessor (ConfigurationClassPostProcessor)
Creation Phase Bean Initialization Phase (late, during singleton instantiation) BeanDefinition Registry Phase (early, before singletons exist)
Proxy Technology CGLIB or JDK Dynamic Proxy Exclusively CGLIB (configuration classes do not implement interfaces)
Interception Purpose Applies MethodInterceptors for cross-cutting logic Intercepts @Bean methods to return the cached singleton from BeanFactory
Target Representation Delegates calls to an internal target bean instance Delegates to BeanFactory.getBean(beanName) via BeanMethodInterceptor
// Conceptual interceptor inside ConfigurationClassEnhancer:
class BeanMethodInterceptor implements MethodInterceptor {
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        if (isCurrentlyInvokedFactoryMethod(method)) {
            // Container is instantiating this bean: execute the method body
            return proxy.invokeSuper(obj, args);
        }
        // Inter-bean call from another @Bean method: route directly to BeanFactory!
        return beanFactory.getBean(beanName);
    }
}

Performance, Metaspace, and GraalVM AOT Compilation

  1. Cold Start & Metaspace Overhead: Every dynamically generated CGLIB subclass is loaded into the JVM’s Metaspace. In applications with thousands of services and aspects, generating and verifying bytecode at startup increases startup time by several seconds and expands Metaspace consumption.
  2. GraalVM Native Image (Ahead-Of-Time): GraalVM Native Images do not permit dynamic bytecode generation or class loading at runtime. Under Spring Boot 3.x with GraalVM AOT, spring-aot scans all proxy requirements at build time and generates static proxy classes prior to native compilation.

4 Tricky Questions

1. In what exact scenario does Spring create an AOP proxy before the postProcessAfterInitialization phase?

Answer: Spring creates an AOP proxy prematurely during circular dependency resolution.

When BeanA depends on BeanB and BeanB depends on BeanA, BeanA exposes an ObjectFactory in the third-level cache (singletonFactories). When BeanB requests BeanA, Spring executes SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(). Inside this method, AbstractAutoProxyCreator wraps BeanA in an AOP proxy before BeanA has undergone property population or initialization, placing the resulting proxy into the second-level cache (earlySingletonObjects).

2. How does Spring create an implementation for a JpaRepository interface when no Java class implements it?

Answer: Spring Data does not generate Java source files or bytecode subclasses on disk. Instead, during container startup, RepositoryFactoryBeanSupport and RepositoryFactorySupport utilize JDK Dynamic Proxies (java.lang.reflect.Proxy.newProxyInstance()) targeting the repository interface.

The invocation handler registered with the proxy is a QueryExecutorMethodInterceptor. When a repository method like findByEmail(String email) is invoked, this interceptor intercepts the call, delegates to a QueryLookupStrategy to parse the method name into a JPQL/SQL query (or retrieve the @Query annotation), binds parameters, and executes the query through the JPA EntityManager.

3. What is the fundamental difference between the proxy created for a @Configuration class and the proxy created for a @Transactional bean?

Answer: The two proxies differ in timing, engine, and architectural purpose:

  • @Configuration Proxy: Created during the BeanFactoryPostProcessor phase by ConfigurationClassPostProcessor using CGLIB. It intercepts @Bean factory methods to ensure calls like new JdbcTemplate(dataSource()) return the existing singleton bean from the BeanFactory rather than instantiating a new object.
  • @Transactional Proxy: Created during the BeanPostProcessor phase by AnnotationAwareAspectJAutoProxyCreator during individual bean initialization. It wraps the target bean to build an interceptor chain (TransactionInterceptor) that manages database transactions around method execution.

4. If a class has 50 methods and only one private method is annotated with @Transactional, does Spring create a proxy? What if one public method is annotated?

Answer:

  • Only one private method annotated: Spring will not create a proxy. AnnotationAwareAspectJAutoProxyCreator evaluates advisors against beans. Spring’s TransactionAttributeSourcePointcut explicitly ignores non-public methods. Since no pointcut matches any public method on the bean, zero advisors apply, and Spring returns the raw unproxied instance.
  • One public method annotated: Spring will create a dynamic proxy for the entire bean class. Dynamic proxies wrap whole instances, not individual methods. External callers will invoke all 50 methods through the proxy, though the TransactionInterceptor will only start a transaction for the one method matching the pointcut.

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

Spring does not proxy every bean—proxies are created strictly on demand. There are 5 main triggers:

  1. Spring AOP: Presence of @Transactional, @Async, @Cacheable, @Validated, @PreAuthorize, or custom @Aspects (created by AnnotationAwareAspectJAutoProxyCreator in postProcessAfterInitialization).
  2. Configuration Full Mode: @Configuration(proxyBeanMethods = true) enhanced by CGLIB via ConfigurationClassPostProcessor to enforce singleton semantics.
  3. Scoped Proxies: @RequestScope / @SessionScope beans wrapped to permit injection into singletons.
  4. Spring Data Repositories: Interface-only repositories (JpaRepository) generated dynamically via JDK Dynamic Proxy.
  5. Lazy Injection: @Lazy fields injected with synthetic proxies to defer bean instantiation.

Core Proxy Creation Triggers Summary

Trigger Component Responsible Underlying Technology Proxy Scope
AOP Annotations (@Transactional, etc.) AbstractAutoProxyCreator (BeanPostProcessor) CGLIB (default in Boot) / JDK Proxy Entire bean instance
@Configuration ConfigurationClassPostProcessor (BFPP) Exclusively CGLIB Configuration class
Scoped Proxies (@RequestScope) ScopedProxyFactoryBean CGLIB / JDK Proxy Web request delegation
Spring Data Repositories RepositoryFactorySupport JDK Dynamic Proxy Repository interface
@Lazy Injection LazyInitTargetSource CGLIB / JDK Proxy Injected field

Red Flags (DO NOT Say)

  • ❌ “Spring wraps every single bean in a dynamic proxy just in case.” (Proxies introduce overhead; Spring only creates them when pointcuts, scopes, or configuration triggers match).
  • ❌ “Proxies are applied method-by-method.” (Proxies wrap entire bean instances; method matching happens inside the interceptor chain).
  • ❌ “The @Configuration proxy is created by a BeanPostProcessor just like @Transactional.” (It is created by a BeanFactoryPostProcessor before singleton beans are instantiated).
  • ❌ “Spring Data JPA generates a Java class file on your hard drive.” (It generates an in-memory JDK Dynamic Proxy at startup).