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
- AOP and Declarative Infrastructure Annotations:
@Transactional: Injects transaction boundary management (commit/rollback).@Async: Offloads execution to a backgroundTaskExecutorthread 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.
- Configuration Classes (
@Configuration):- Classes annotated with
@Configuration(proxyBeanMethods = true)are enhanced to intercept inter-bean method calls and enforce singleton semantics.
- Classes annotated with
- Scoped Proxies (
@RequestScope,@SessionScope):- Wraps short-lived web-scoped beans so they can be safely injected into long-lived singletons without state corruption.
- Spring Data Repository Interfaces (
JpaRepository,CrudRepository):- Automatically provides runtime implementations for repository interfaces that have no Java class definitions.
- 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):
ServiceAis instantiated as a raw instance.ServiceApublishes anObjectFactorylambda to the Level 3 Cache (singletonFactories).ServiceAattempts to injectServiceB.ServiceBis instantiated and requiresServiceA.- Spring invokes
singletonFactories.get("serviceA"), which executes:SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(rawBean, beanName); AbstractAutoProxyCreatordetects the circular reference and creates the AOP Proxy prematurely beforeServiceAhas even completed property population or@PostConstruct!- The early proxy is cached in the Level 2 Cache (
earlySingletonObjects) and injected intoServiceB. - When
ServiceAfinally reachespostProcessAfterInitialization,AbstractAutoProxyCreatorchecks itsearlyProxyReferencesmap, 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
- 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.
- 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-aotscans 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:
@ConfigurationProxy: Created during theBeanFactoryPostProcessorphase byConfigurationClassPostProcessorusing CGLIB. It intercepts@Beanfactory methods to ensure calls likenew JdbcTemplate(dataSource())return the existing singleton bean from theBeanFactoryrather than instantiating a new object.@TransactionalProxy: Created during theBeanPostProcessorphase byAnnotationAwareAspectJAutoProxyCreatorduring 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
privatemethod annotated: Spring will not create a proxy.AnnotationAwareAspectJAutoProxyCreatorevaluates advisors against beans. Spring’sTransactionAttributeSourcePointcutexplicitly 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
publicmethod 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 theTransactionInterceptorwill 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:
- Spring AOP: Presence of
@Transactional,@Async,@Cacheable,@Validated,@PreAuthorize, or custom@Aspects (created byAnnotationAwareAspectJAutoProxyCreatorinpostProcessAfterInitialization).- Configuration Full Mode:
@Configuration(proxyBeanMethods = true)enhanced by CGLIB viaConfigurationClassPostProcessorto enforce singleton semantics.- Scoped Proxies:
@RequestScope/@SessionScopebeans wrapped to permit injection into singletons.- Spring Data Repositories: Interface-only repositories (
JpaRepository) generated dynamically via JDK Dynamic Proxy.- Lazy Injection:
@Lazyfields 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
@Configurationproxy is created by aBeanPostProcessorjust like@Transactional.” (It is created by aBeanFactoryPostProcessorbefore 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).