Spring Annotations Interview Questions – Events and Advanced Configuration

  • Last Updated: April 29, 2026
  • By: javahandson
  • Series
img

Spring Annotations Interview Questions – Events and Advanced Configuration

This article covers 15 carefully crafted Spring annotations interview questions — ranging from beginner to advanced — focusing on @Value, SpEL, the Environment abstraction, stereotype annotations, Spring Events, @Async, @Scheduled, @Conditional, CGLIB proxying in @Configuration classes, and more. Every answer includes the deep technical detail that interviewers at product companies expect.

Spring is an annotation-driven framework at its heart. Almost every feature you use day-to-day — dependency injection, event publishing, async execution, scheduled tasks, conditional bean registration, and configuration management — is powered by a specific annotation or a combination of annotations. For senior Java developers, a deep understanding of these annotations is not optional. It is the difference between writing correct, production-grade Spring code and writing code that works in demos but breaks in production.

📌 How to use this article:  Read each question as if the interviewer just asked it. Then read the answer. The Interview Insight notes at the end of each answer highlight the specific detail that separates average candidates from strong ones.

Beginner Level — Q1 to Q5

Q1. What are @Value — SpEL expressions, default values, and type conversion?

Answer – @Value is a Spring annotation used to inject values into fields, method parameters, or constructor parameters. It supports three distinct modes of operation: plain literal injection, property placeholder resolution, and Spring Expression Language (SpEL) evaluation.

In its simplest form, @Value accepts a string literal. More commonly, it is used with the ${…} syntax to inject values from Spring’s Environment, which includes application.properties, application.yml, system properties, and environment variables. It also supports the #{…} syntax for full SpEL expressions.

@Component
public class AppConfig {

    // 1. Plain literal
    @Value("javahandson")
    private String appName;

    // 2. Property placeholder — reads from application.properties
    @Value("${server.port}")
    private int serverPort;

    // 3. Default value — if property missing, 8080 is used
    @Value("${server.port:8080}")
    private int portWithDefault;

    // 4. SpEL expression — evaluates 'systemProperties' map
    @Value("#{systemProperties['user.home']}")
    private String userHome;

    // 5. SpEL referencing another bean
    @Value("#{orderService.defaultTimeout * 1000}")
    private long timeoutMs;

    // 6. List injection from comma-separated property value
    @Value("${app.allowed-origins}")
    private List<String> allowedOrigins;
}

Spring performs automatic type conversion from the string value to the declared field type. Primitives like int, long, and boolean, and their boxed equivalents, all work out of the box. Arrays and Lists are supported for comma-separated values. For complex types, a registered ConversionService or PropertyEditor handles the conversion.

The default value syntax is colon-separated: ${property.key:defaultValue}. If the property is not found in the Environment, the value after the colon is used as the fallback. This keeps your beans functional even in environments where certain properties are not defined.

🎯 Interview Insight:  A common interview follow-up: ‘What happens if a @Value property is missing and no default is set?’ Spring throws an IllegalArgumentException: Could not resolve placeholder at context startup — not lazily at runtime. This is because @Value is processed by AutowiredAnnotationBeanPostProcessor during bean creation, before the application is ready to serve traffic.

Q2. What is Spring Expression Language (SpEL) — what can you do with it?

Answer – Spring Expression Language, or SpEL, is a powerful expression language built into the Spring Framework. It supports querying and manipulating object graphs at runtime, and can be used in @Value annotations, @Conditional expressions, Spring Security access control expressions, Spring Data queries, Spring Integration routing, and more.

SpEL is evaluated at runtime by the ExpressionParser and its implementations. In annotation-based usage, it is delimited by #{ }. The expression inside can reference Spring beans by name (using the @beanName syntax), access system properties, call methods, perform arithmetic, use conditionals (ternary operator), work with collections, and call static methods.

@Component
public class SpelExamples {

    // Reference another Spring bean and call its method
    @Value("#{configBean.maxConnections}")
    private int maxConns;

    // Arithmetic
    @Value("#{10 * 60 * 1000}")
    private long tenMinutesMs;

    // Ternary / Elvis operator
    @Value("#{systemProperties['debug'] != null ? 'debug' : 'info'}")
    private String logLevel;

    // System environment variable
    @Value("#{environment['HOME']}")
    private String homeDir;

    // Static method call
    @Value("#{T(java.lang.Math).PI}")
    private double pi;

    // Collection filtering — get all active users
    @Value("#{userService.allUsers.?[active == true]}")
    private List<User> activeUsers;
}

SpEL’s collection projection (.![expression]) extracts a field from each element of a list, producing a new list. The selection operator (.?[condition]) filters a collection. These are particularly useful in Spring Integration routing and Spring Batch configurations.

For programmatic use, ExpressionParser and StandardEvaluationContext allow you to evaluate SpEL expressions in plain Java code without annotation wiring. This is how Spring Security evaluates @PreAuthorize(‘hasRole(“ADMIN”)’) expressions at method invocation time.

🎯 Interview Insight:  Interviewers sometimes ask: ‘Can you call a static method in SpEL?’ Yes — use the T() operator: T(java.lang.Math).sqrt(16). The T() operator returns the Class type for the specified class, allowing access to static fields and methods. This is different from instance method calls, which require a bean reference.

Q3. .   What is @Value vs the Environment abstraction — when to use which?

Answer – Both @Value and Environment let you pull external configuration into a Spring bean, but they serve different purposes and operate at different scales. @Value is declarative — one annotation resolves one property. Environment is a plain Java object you inject once and use like any other collaborator, which is what makes it the right tool once configuration stops being ‘one or two values’ and starts being ‘a related group.’

Feature@Value  vs  Environment
Binding styleOne field at a time  |  Whole group, resolved together in your own code
SpEL supportYes  |  No — plain string lookup only
Type-safe bindingLimited — one type per field  |  Full — you choose the types when you build the object
Nested propertiesAwkward — one annotation per leaf value  |  Natural — you assemble the nesting yourself
ValidationNo  |  Yes — manual, but enforced at startup in your @Bean method
Relaxed bindingNo  |  No — Environment resolves keys literally, exact match only
Best forSingle values, dynamic/SpEL expressions  |  Grouped config, nested or validated types

@Value — One Field at a Time@Value is the simplest way to inject a property into a bean. You declare it on a field, a constructor parameter, or a setter, give it a ${…} placeholder, and Spring resolves and injects the value before the bean is handed to anyone else.

// config.properties
app.name=JavaHandsOn
app.timeout=5000
app.debug=true
@Configuration
@PropertySource("classpath:config.properties")
public class AppConfig { }

@Component
public class AppService {

    @Value("${app.name}")
    private String name;

    @Value("${app.timeout:3000}")
    private int timeout;

    @Value("${app.debug:false}")
    private boolean debug;
}

Notice the @PropertySource on AppConfig — plain Spring Framework does not scan for or auto-load a properties file on its own; you name the file explicitly, and Spring adds it to the Environment for you. This works fine for a few scattered values, but it gets messy at scale: each additional property is another field and another annotation, with no way to treat “all the app.* properties” as a single unit.

Environment — Whole Group at Once  Environment is the object Spring itself uses internally to resolve every ${…} placeholder you write. Instead of one annotation per property, you inject Environment once — as a constructor parameter, exactly like any other dependency — and read all related properties together in a single @Bean method, then hand the rest of the application a finished object.

public class AppProperties {
    private final String name;
    private final int timeout;
    private final boolean debug;
    private final int maxConnections;

    public AppProperties(String name, int timeout, boolean debug, int maxConnections) {
        this.name = name;
        this.timeout = timeout;
        this.debug = debug;
        this.maxConnections = maxConnections;
    }
    // getters only — keep it immutable
}
@Configuration
@PropertySource("classpath:config.properties")
public class AppConfig {

    @Bean
    public AppProperties appProperties(Environment env) {
        return new AppProperties(
            env.getProperty("app.name"),
            env.getProperty("app.timeout", Integer.class, 3000),
            env.getProperty("app.debug", Boolean.class, false),
            env.getProperty("app.max-connections", Integer.class, 5)
        );
    }
}
@Service
public class AppService {

    private final AppProperties props;

    public AppService(AppProperties props) {
        this.props = props;
    }

    public void printConfig() {
        System.out.println(props.getName());
        System.out.println(props.getMaxConnections());
    }
}

Clean, centralized, and easy to maintain — once you’ve written the handful of lines that do the grouping. It’s more typing up front than a single annotation would be, but every other bean in the application injects AppProperties normally, and the string-based reading happens in exactly one place.

Nested Properties — Where Environment Still Works. The same idea extends naturally to nested configuration. Spring Framework has no mechanism to automatically turn dotted property paths into a nested object graph — you build the graph by hand once, and everyone downstream gets a clean, typed object back.

app.datasource.url=jdbc:mysql://localhost/mydb
app.datasource.username=root
app.datasource.pool.max-size=20
app.datasource.pool.min-idle=5
public class Pool {
    private final int maxSize;
    private final int minIdle;
    public Pool(int maxSize, int minIdle) {
        this.maxSize = maxSize;
        this.minIdle = minIdle;
    }
    // getters
}

public class Datasource {
    private final String url;
    private final String username;
    private final Pool pool;
    public Datasource(String url, String username, Pool pool) {
        this.url = url;
        this.username = username;
        this.pool = pool;
    }
    // getters
}
@Bean
public Datasource datasource(Environment env) {
    Pool pool = new Pool(
        env.getProperty("app.datasource.pool.max-size", Integer.class, 10),
        env.getProperty("app.datasource.pool.min-idle", Integer.class, 2)
    );
    return new Datasource(
        env.getProperty("app.datasource.url"),
        env.getProperty("app.datasource.username"),
        pool
    );
}

Try doing this with @Value alone, and you would need a separate annotated field for every leaf property, with no natural place to group url, username, and pool together as one concept:

@Value("${app.datasource.pool.max-size}")
private int maxSize;  // repeated for every nested field, with no grouping

Validation with Environment  Without a binder running behind the scenes, there is no @Validated plus Bean Validation pipeline firing automatically. You get the same fail-fast guarantee by validating manually, right at the point where the object is constructed.

@Bean
public AppProperties appProperties(Environment env) {
    String name = env.getProperty("app.name");
    if (name == null || name.isBlank()) {
        throw new IllegalStateException("app.name must not be blank");
    }
    int timeout = env.getProperty("app.timeout", Integer.class, 3000);
    if (timeout < 1000 || timeout > 30000) {
        throw new IllegalStateException("app.timeout must be between 1000 and 30000");
    }
    return new AppProperties(name, timeout, false, 5);
}

Because this runs inside a @Bean method, a thrown exception fails the entire container startup immediately — fail fast, no surprises at runtime. @Value has no equivalent safety net at all; a bad value simply flows into the field as-is.

Relaxed Binding — Not Available in Plain Spring  Relaxed binding — where app.max-connections, app.maxConnections, and APP_MAX_CONNECTIONS all resolve to the same value automatically — is not something Environment.getProperty() does for you. It performs an exact, literal key lookup: ask for the wrong casing or separator, and you get null.

app.max-connections=10

env.getProperty("app.max-connections");  // works
env.getProperty("app.maxConnections");    // null — different key, no aliasing

If you need to support more than one naming convention for the same setting, you check each candidate key yourself and take the first non-null result — there is no framework-level aliasing to lean on. In practice, the simpler fix is to just pick one property-naming convention for your application and use it consistently.

Q4. What are stereotype annotations — @Component, @Service, @Repository, @Controller — are they functionally different?

Answer – Stereotype annotations are a family of annotations that mark a class as a Spring-managed component and indicate its role in the application architecture. They all trigger classpath scanning and bean registration when @ComponentScan is active, but they convey different semantics, and some provide additional framework-level integration.

@Component is the base stereotype. All other stereotype annotations are themselves meta-annotated with @Component, so Spring recognizes them during classpath scanning. The component scan picks up any class annotated with @Component or an annotation that is meta-annotated with @Component.

// @Service is literally defined as:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component          // <-- meta-annotated with @Component
public @interface Service { ... }

// Same pattern for @Repository and @Controller

@Repository adds exception translation. Spring installs a PersistenceExceptionTranslationPostProcessor, which detects @Repository beans and wraps any persistence-framework-specific exceptions (SQLExceptions, HibernateExceptions, etc.) into Spring’s unified DataAccessException hierarchy. This is the one concrete functional difference.

@Service carries no additional framework behavior beyond @Component. It is a pure semantic marker indicating that the class holds business logic. By convention, service classes should not directly access databases — that responsibility belongs to @Repository classes.

@Controller marks a class as a Spring MVC controller. It enables request mapping dispatch when combined with @RequestMapping. It is recognized by the DispatcherServlet’s handler mapping strategy. @RestController is @Controller + @ResponseBody, which removes the need to annotate each handler method with @ResponseBody.

AnnotationAdditional Behavior Beyond @Component
@ComponentBase stereotype — no additional behavior
@ServiceSemantic only — no additional framework behavior
@RepositoryException translation via PersistenceExceptionTranslationPostProcessor
@ControllerRecognized by DispatcherServlet for handler mapping
@RestController@Controller + @ResponseBody — JSON/XML response by default
🎯 Interview Insight:  Interviewers often ask: ‘If @Service and @Component do the same thing, why use @Service?’ The answer has two parts. First, semantics matter for code readability and team conventions — @Service clearly communicates intent. Second, Spring AOP advice and other framework features can target specific stereotypes using type-level pointcuts. A pointcut targeting @Repository beans applies only to repository classes, not service or controller classes.

Q5. What is @PostConstruct, and when does it run relative to dependency injection?

Answer – @PostConstruct is a JSR-250 annotation that marks a method to be called by the Spring container after the bean has been fully constructed and all dependencies have been injected, but before the bean is put into service. It is the recommended way to perform initialization logic in Spring beans.

The exact execution point in the bean lifecycle is inside BeanPostProcessor.postProcessBeforeInitialization(). Specifically, CommonAnnotationBeanPostProcessor processes @PostConstruct during that phase. This means it runs after the constructor and after all @Autowired field/setter injections are complete, but before InitializingBean.afterPropertiesSet() and any custom init-method.

@Service
public class CacheWarmingService {

    @Autowired
    private ProductRepository productRepository;  // injected first

    private Map<Long, Product> productCache;

    @PostConstruct
    public void warmCache() {
        // Safe to use productRepository here — injection is complete
        productCache = productRepository.findAll()
            .stream()
            .collect(Collectors.toMap(Product::getId, p -> p));
        System.out.println("Cache warmed with " + productCache.size() + " products");
    }
}

One important constraint: @PostConstruct methods must have no parameters and must return void. They can declare checked exceptions — Spring wraps any exception thrown in a BeanCreationException. They can be private, protected, package-private, or public — the access modifier does not matter because CommonAnnotationBeanPostProcessor invokes the method via reflection.

If you attempt to use an injected dependency in the constructor instead of @PostConstruct, you risk NullPointerExceptions because field injection has not happened yet. Constructor injection is the exception — dependencies passed via the constructor are available immediately inside the constructor body. @PostConstruct is mainly necessary when you use field or setter injection and need to run initialization logic after all injections are complete.

🎯 Interview Insight:  A nuanced but common interview follow-up: ‘Can @PostConstruct access a @Transactional boundary?’ The answer is: it depends. If the @PostConstruct method itself is @Transactional, the proxy has not been fully initialized yet at postProcessBeforeInitialization() time (the AOP proxy is created in postProcessAfterInitialization(), which runs after @PostConstruct). The solution is to use ApplicationRunner or CommandLineRunner for initialization that requires a live transaction context.

Intermediate Level — Q6 to Q11

Q6. What are Spring Application Events — how do you publish and listen to them?

Answer – Spring’s Application Event system is a built-in publish-subscribe (observer pattern) mechanism that allows beans to communicate with each other without being directly coupled. A publisher fires an event by calling ApplicationEventPublisher.publishEvent(). Any number of listeners can react to that event without the publisher knowing anything about them.

There are two layers to Spring Events: the older ApplicationEvent-based approach (pre-Spring 4.2) where events must extend ApplicationEvent, and the modern annotation-based approach (Spring 4.2+) where any object can be published as an event without extending any class.

// 1. Define an event — any POJO works in Spring 4.2+
public class OrderPlacedEvent {
    private final Long orderId;
    private final String customerEmail;

    public OrderPlacedEvent(Long orderId, String customerEmail) {
        this.orderId = orderId;
        this.customerEmail = customerEmail;
    }
    // getters
}

// 2. Publish the event
@Service
public class OrderService {

    @Autowired
    private ApplicationEventPublisher publisher;

    public void placeOrder(Order order) {
        // business logic
        publisher.publishEvent(new OrderPlacedEvent(order.getId(), order.getEmail()));
    }
}

// 3. Listen to the event
@Component
public class EmailNotificationListener {

    @EventListener
    public void handleOrderPlaced(OrderPlacedEvent event) {
        System.out.println("Sending email to: " + event.getCustomerEmail());
    }
}

Spring also ships with several built-in application events that you can listen to for framework lifecycle hooks. ContextRefreshedEvent fires when the ApplicationContext is fully initialized or refreshed. ContextClosedEvent fires when the context is being closed. ContextStartedEvent and ContextStoppedEvent fire in response to Lifecycle.start() and Lifecycle.stop().

Events are synchronous by default in Spring. The publishEvent() call blocks until all registered listeners for that event have completed execution. If you need non-blocking event dispatch, combine @EventListener with @Async.

🎯 Interview Insight:  Interviewers sometimes ask: ‘Are Spring Events transactional?’ By default, no. If you publish an event inside a @Transactional method and a listener commits side effects (like sending an email), those side effects happen immediately — even if the transaction is later rolled back. The fix is @TransactionalEventListener, which binds listener execution to a specific transaction phase (AFTER_COMMIT by default). This ensures that the listener runs only if the originating transaction commits successfully.

Q7. What is @EventListener — how does it work and what are its filtering options?

Answer – @EventListener is the annotation-based way to register a Spring event listener. Any Spring-managed bean method annotated with @EventListener automatically becomes a listener for the event type declared in its method parameter. There is no need to implement any interface.

Spring’s EventListenerMethodProcessor detects @EventListener-annotated methods during context startup and registers them as ApplicationListener instances backed by ApplicationListenerMethodAdapter. The method parameter type determines which event the listener handles.

@Component
public class AuditEventListener {

    // Listens for OrderPlacedEvent
    @EventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        System.out.println("Audit: order placed " + event.getOrderId());
    }

    // Condition filter — only fires if order total > 1000
    @EventListener(condition = "#event.total > 1000")
    public void onHighValueOrder(OrderPlacedEvent event) {
        System.out.println("VIP order: " + event.getOrderId());
    }

    // Listen to multiple event types with classes attribute
    @EventListener(classes = {OrderPlacedEvent.class, OrderCancelledEvent.class})
    public void onOrderLifecycleEvent(ApplicationEvent event) {
        System.out.println("Order lifecycle: " + event.getClass().getSimpleName());
    }

    // Return a new event — fires an additional event from listener output
    @EventListener
    public InventoryUpdateEvent onOrderPlacedForInventory(OrderPlacedEvent event) {
        return new InventoryUpdateEvent(event.getOrderId());
    }
}

The condition attribute accepts a SpEL expression. The root object for evaluation is the event object, accessible via #event or #root.event. This allows fine-grained filtering without putting conditional logic inside the listener body. The classes attribute allows a single listener method to handle multiple event types when you declare an abstract or supertype as the parameter.

When an @EventListener method has a non-void return type, the returned value is automatically published as a new event. This creates an event-chaining mechanism without requiring the listener to have direct access to ApplicationEventPublisher.

🎯 Interview Insight:  A subtle but important point: @EventListener methods are called synchronously in the order events are dispatched. If multiple listeners handle the same event, use @Order to control the execution sequence. @Order(1) runs before @Order(2). Listeners without @Order get the default lowest precedence. This is critical when listener order matters for correctness — for example, an audit listener that must run before a cleanup listener.

Q8. How do you make event listeners asynchronous with @Async?

Answer – By default, Spring event listeners execute synchronously in the same thread as the publisher. The publishEvent() call blocks until all listeners complete. For listeners that perform slow operations like sending emails, writing logs, or calling external APIs, this synchronous behavior can significantly slow down the publishing thread.

To make an @EventListener execute asynchronously, add @Async to the listener method. This requires @EnableAsync on a @Configuration class to activate Spring’s async execution infrastructure.

// Enable async execution
@Configuration
@EnableAsync
public class MyApplication { }

// Async event listener
@Component
public class AsyncEmailListener {

    @Async
    @EventListener
    public void sendOrderConfirmationEmail(OrderPlacedEvent event) {
        // Runs on a separate thread from the publisher
        // publishEvent() returns immediately without waiting for this
        emailService.sendConfirmation(event.getCustomerEmail());
        System.out.println("Email sent on thread: " + Thread.currentThread().getName());
    }
}

// Optionally configure a named executor for the async listener
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("event-async-");
        executor.initialize();
        return executor;
    }
}

When @Async is applied, Spring’s AOP proxy intercepts the listener method call and submits it to a thread pool executor. The publisher thread returns from publishEvent() immediately without waiting for the listener to complete. Exceptions thrown inside async listeners cannot propagate back to the publisher — they must be handled internally or via AsyncUncaughtExceptionHandler.

Note: @Async on an @EventListener that returns a new event to be published (chained events) does not work — return values from async listeners are ignored because the method executes in a different thread after the publisher has already moved on.

🎯 Interview Insight:  A common production mistake: combining @TransactionalEventListener with @Async. When you do this, the listener executes in a new thread that has no transaction context — the original transaction’s connection has already been released. If your listener needs database access, it must open its own transaction using @Transactional(propagation = REQUIRES_NEW) on the listener method. This is a frequent source of LazyInitializationException bugs in production Spring applications.

Q9. What is ApplicationContextAware, and when would you implement it?

Answer – ApplicationContextAware is a Spring-aware interface that allows a bean to obtain a reference to the ApplicationContext that manages it. When a bean implements this interface, Spring calls setApplicationContext(ApplicationContext ctx) during the bean initialization phase — specifically in the Aware interface callbacks step, which occurs after dependency injection but before @PostConstruct.

@Component
public class SpringContextHolder implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextHolder.context = applicationContext;
    }

    // Static accessor — lets non-Spring classes retrieve beans
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }

    public static Object getBean(String beanName) {
        return context.getBean(beanName);
    }
}

The most common production use case is implementing a static SpringContextHolder utility class that allows legacy code, utility classes, or third-party integrations — which are not Spring-managed — to retrieve beans from the context by name or type. This is a recognized pattern for bridging between Spring-managed and non-Spring code.

Other legitimate use cases include: dynamically looking up beans by type at runtime when you do not know the type at wiring time; iterating over all beans of a certain type for registration or discovery purposes; programmatically refreshing or closing the context in tests; and accessing context-level resources or events that are not available through direct injection.

The alternative to ApplicationContextAware is injecting ApplicationContext directly via @Autowired. In modern Spring code, direct injection is always preferred over implementing Aware interfaces. ApplicationContextAware is most valuable for the static holder pattern, where injection is not possible.

🎯 Interview Insight:  Interviewers sometimes ask: ‘What is the difference between BeanFactoryAware and ApplicationContextAware?’ ApplicationContext extends BeanFactory and adds event publishing, internationalization, AOP support, and resource loading on top of the core bean factory. BeanFactoryAware provides access only to the lower-level ConfigurableBeanFactory. In practice, ApplicationContextAware is almost always the right choice, since it provides the full set of capabilities.

Q10. What is @EnableAsync — how does Spring implement async execution internally?

Answer – @EnableAsync is a @Configuration-level annotation that activates Spring’s annotation-driven asynchronous method execution. Without it, @Async annotations on methods are simply ignored — the methods execute synchronously as if @Async were not there. @EnableAsync is typically placed on a dedicated @Configuration class in your application context.

Internally, @EnableAsync works through AOP. When you add @EnableAsync, Spring imports AsyncConfigurationSelector, which registers a ProxyAsyncConfiguration bean. This bean creates an AsyncAnnotationBeanPostProcessor, which is the actual BeanPostProcessor responsible for detecting @Async methods and wrapping the declaring beans in AOP proxies.

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) ->
            log.error("Async method {} failed: {}", method.getName(), ex.getMessage());
    }
}

@Service
public class ReportService {

    @Async
    public CompletableFuture<Report> generateReport(Long reportId) {
        Report report = buildReport(reportId);  // long-running
        return CompletableFuture.completedFuture(report);
    }
}

The execution flow when an @Async method is called: the caller invokes the method on the proxy; the proxy intercepts the call via AsyncExecutionInterceptor, submits a Callable that wraps the real method to the configured Executor, and returns immediately. If the method returns a CompletableFuture or a Future, the caller can track its completion. If it returns void, there is no completion handle — exceptions must be handled via AsyncUncaughtExceptionHandler.

If you do not configure a custom Executor via AsyncConfigurer, Spring falls back to a SimpleAsyncTaskExecutor, which creates a new thread for every @Async invocation — no pooling, no bounded queue. This is safe for development but dangerous in production under load. Always configure a proper ThreadPoolTaskExecutor for production.

🎯 Interview Insight:  The most critical self-invocation limitation: if a method inside a bean calls another @Async method in the same bean, the async proxy is bypassed entirely. The call goes directly to the raw object, not through the proxy, so the method executes synchronously. The same limitation applies to @Transactional. The fix: inject the bean into itself via @Autowired (or ApplicationContext.getBean()) so calls go through the proxy.

Q11. What is @Scheduled — how does it work and what are its cron, fixedRate, and fixedDelay options?

Answer – @Scheduled marks a method to be executed on a recurring schedule managed by Spring’s task-scheduling infrastructure. It requires @EnableScheduling on a @Configuration class to activate the scheduling infrastructure. The annotated method must have no parameters and a void return type.

Spring registers all @Scheduled methods with a TaskScheduler (backed by a ScheduledThreadPoolExecutor with a single thread by default) at context startup via ScheduledAnnotationBeanPostProcessor. The scheduling is set up after the bean is fully initialized.

@Component
public class ScheduledTasks {

    // fixedRate: runs every 5 seconds, measured from start of previous execution
    // If execution takes 6s, next run starts immediately (overlap possible in multi-thread)
    @Scheduled(fixedRate = 5000)
    public void syncDataFixedRate() {
        System.out.println("Fixed rate sync at " + LocalTime.now());
    }

    // fixedDelay: waits 5 seconds AFTER previous execution completes
    // No overlap risk — guarantees sequential execution with gap
    @Scheduled(fixedDelay = 5000)
    public void cleanupFixedDelay() {
        System.out.println("Fixed delay cleanup at " + LocalTime.now());
    }

    // initialDelay: wait 10 seconds before the FIRST execution
    @Scheduled(fixedRate = 5000, initialDelay = 10000)
    public void delayedStart() { }

    // cron: standard Unix cron expression
    // 'seconds minutes hours dayOfMonth month dayOfWeek'
    @Scheduled(cron = "0 0 2 * * MON-FRI")
    public void runAtMidnight2AMWeekdays() { }

    // cron from property — allows changing schedule without recompilation
    @Scheduled(cron = "${report.cron:0 0 8 * * MON}")
    public void generateWeeklyReport() { }
}
AttributeBehavior
fixedRateInterval from the end of the previous execution. Always sequential, no overlap.
fixedDelayInterval from the end of the previous execution. Always sequential, no overlap.
initialDelayDelay before the first execution only. Works with fixedRate and fixedDelay.
cronUnix cron expression with seconds field: s m h dom M dow. Most precise control.
zoneInterval from the start of the previous execution. Can overlap if execution is slow.

Spring’s default scheduler uses a single thread. If one @Scheduled task is slow, it can delay other tasks. For production workloads with multiple scheduled tasks, configure a custom TaskScheduler with a thread pool:

@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar registrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);
        scheduler.setThreadNamePrefix("scheduled-");
        scheduler.initialize();
        registrar.setTaskScheduler(scheduler);
    }
}
🎯 Interview Insight:  A common mistake: @Scheduled on a @Transactional bean creates two proxies — a scheduling proxy and a transactional proxy. The @Scheduled infrastructure invokes the method via ScheduledAnnotationBeanPostProcessor, which invokes the TaskScheduler, which then calls the method directly on the bean — bypassing the transactional proxy. To ensure transaction management works with scheduled tasks, the @Transactional annotation should be on a service method called by the scheduled method, not on the scheduled method itself.

Advanced Level — Q12 to Q15

Q12. What is ContextRefreshedEvent vs SmartInitializingSingleton — how do you run startup logic in plain Spring?

Answer – Spring Framework has no single built-in hook explicitly named for ‘run this after startup’ the way a one-line CLI runner would. The underlying problem — ‘run this code once every bean exists, and the container is fully ready’ — is solved with two tools built directly into the container itself: ContextRefreshedEvent, and the SmartInitializingSingleton callback interface.

ContextRefreshedEvent — Listening for a Fully Refreshed Context  ContextRefreshedEvent is published by the ApplicationContext every time refresh() completes — including the very first startup. You can listen to it either by implementing ApplicationListener<ContextRefreshedEvent> directly, or with the simpler @EventListener annotation on any Spring-managed bean method.

@Component
public class StartupRunner implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private UserRepository userRepository;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (userRepository.count() == 0) {
            userRepository.save(new User("admin", "admin@example.com"));
        }
    }
}

// Equivalent using @EventListener — no interface required
@Component
public class AnnotatedStartupRunner {

    @EventListener
    public void onStartup(ContextRefreshedEvent event) {
        System.out.println("Context refreshed: " + event.getApplicationContext().getId());
    }
}

There is one well-known gotcha: in an application with a parent-child context hierarchy — a root context plus a DispatcherServlet context, for example — ContextRefreshedEvent fires once per context. A listener registered that both contexts can see it will run twice. Guard against this by checking event.getApplicationContext().getParent() == null, or by comparing the event’s context against the specific ApplicationContext your bean was wired into.

@Component
public class CacheWarmer implements SmartInitializingSingleton {

    @Autowired
    private ProductRepository productRepository;

    @Override
    public void afterSingletonsInstantiated() {
        productRepository.findAll().forEach(this::warmCache);
    }

    private void warmCache(Product product) {
        // populate an in-memory cache
    }
}

When to Use Which

  • Your startup logic genuinely needs the ApplicationContext itself —  inside the callback (event.getApplicationContext()).
  • You’re already using @EventListener elsewhere —  and want one consistent listening style across the codebase.
  • The logic should respond every time the context refreshes — not just once (this is rare but real in some modular or test setups).
  • You want startup logic to run exactly once —  with no parent/child duplication to guard against.
  • The logic only needs other beans —  not the ApplicationContext itself.
  • You’re doing cache warming, one-time validation, or wiring non-Spring listeners onto already-initialized Spring beans.

Bonus  Both mechanisms respect ordering: implement Ordered, or annotate the class with @Order, and Spring runs multiple startup components in ascending order — the same rule @Order follows everywhere else in the container.

Bottom line:  In plain Spring Framework, ContextRefreshedEvent and SmartInitializingSingleton are the two building blocks for ‘run this after the container is ready.’ Reach for ContextRefreshedEvent when you need the ApplicationContext itself; reach for SmartInitializingSingleton for a clean, one-time startup hook with no context-hierarchy surprises.

SmartInitializingSingleton — A Cleaner, One-Time Hook  For most ‘run once, after every singleton is ready’ use cases, SmartInitializingSingleton is a better fit than ContextRefreshedEvent. Its single callback method, afterSingletonsInstantiated(), fires exactly once — right after all non-lazy singleton beans have been created — and it is scoped to a single BeanFactory, so it does not have the parent/child double-firing problem.

AspectBest Choice
Need the ApplicationContext inside the callbackContextRefreshedEvent
Must run exactly once, no parent/child duplicationSmartInitializingSingleton
Cache warming or one-time validationSmartInitializingSingleton
Should react every time the context is refreshedContextRefreshedEvent
Ordering multiple startup tasksEither implement Ordered or use @Order
🎯 Interview Insight:  A common trap: registering a ContextRefreshedEvent listener in a classic Spring MVC setup with a root context and a DispatcherServlet child context causes startup logic to fire twice — once per context refresh. Always guard with a parent-context check, or simply prefer SmartInitializingSingleton, which is scoped to a single BeanFactory and cannot double-fire in this way.

Q13. How does the @Configuration class handle internal @Bean method calls — explain CGLIB proxying?

Answer – @Configuration classes exhibit a subtle but critical behavior: when one @Bean method within a @Configuration class calls another @Bean method in the same class, it does not re-execute the method body. Instead, it retrieves the already-created bean from the Spring container. This is called the inter-bean method, or call interception, and it is implemented via CGLIB proxying.

When Spring processes a @Configuration class, it does not use the raw class you wrote. Instead, it creates a CGLIB subclass of your @Configuration class. This subclass overrides all @Bean methods. When an overridden method is called, the CGLIB subclass intercepts the call, checks whether a bean with that name is already present in the singleton registry, and returns the cached instance if it exists — only calling the real method body if the bean has not been created yet.

@Configuration
public class AppConfig {

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

    @Bean
    public HikariConfig hikariConfig() {
        // In plain Java: every call to hikariConfig() creates a NEW HikariConfig
        // In @Configuration + CGLIB: every call returns the SAME singleton bean
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost/mydb");
        return config;
    }

    @Bean
    public TransactionManager transactionManager() {
        // dataSource() here returns the SAME DataSource bean, not a new instance
        return new DataSourceTransactionManager(dataSource());
    }
}

Without this CGLIB interception, calling dataSource() inside transactionManager() would re-execute the dataSource() method body, creating a second DataSource instance. Your transaction manager would then be using a different connection pool than the rest of your application — a serious production bug.

@Configuration(proxyBeanMethods = false) — introduced in Spring 5.2 — disables this CGLIB subclassing entirely. With this setting, the @Configuration class is used directly without proxying. Inter-bean method calls create new instances as in plain Java. This is appropriate only when @Bean methods are leaf nodes with no inter-dependencies, and it gives a small startup performance improvement by skipping CGLIB proxy generation.

// Lite @Configuration — no CGLIB proxy, no inter-bean call interception
@Configuration(proxyBeanMethods = false)
public class LiteConfig {

    @Bean
    public OrderService orderService(PaymentService paymentService) {
        // Dependencies injected via method parameters — SAFE
        // This works correctly because Spring passes the bean as parameter
        return new OrderService(paymentService);
    }

    @Bean
    public PaymentService paymentService() {
        return new PaymentService();
    }
    // orderService() above does NOT call paymentService() directly — it receives it as param
}
🎯 Interview Insight:  You can verify CGLIB proxying by printing the class of a @Configuration bean: context.getBean(AppConfig.class).getClass() returns AppConfig$$EnhancerBySpringCGLIB$$abc123, not AppConfig. This confirms the CGLIB subclass is active. @Configuration classes with no inter-bean @Bean calls are good candidates for proxyBeanMethods = false, since skipping CGLIB subclassing shaves a small amount off startup time with no behavior change — dependencies are simply passed in as method parameters instead.

Q14. What is @Conditional — how do you write a custom condition?

Answer – @Conditional is a Spring annotation that allows a @Bean, @Component, or @Configuration to be registered with the container only if a specified condition is met at startup time. It is the same mechanism Spring itself uses to implement @Profile internally — @Profile is meta-annotated with @Conditional(ProfileCondition.class).

@Conditional takes a Condition implementation class as its value. Spring evaluates the condition before deciding whether to register the bean. The Condition interface defines a single method: boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata).

// Step 1: Implement the Condition interface
public class OnLinuxCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String os = context.getEnvironment().getProperty("os.name", "").toLowerCase();
        return os.contains("linux");
    }
}

// Step 2: Create a meta-annotation (optional but recommended)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnLinuxCondition.class)
public @interface ConditionalOnLinux { }

// Step 3: Use it
@Bean
@ConditionalOnLinux
public FileSystemMonitor linuxFileMonitor() {
    return new InotifyFileSystemMonitor();
}

@Bean
@Conditional(OnLinuxCondition.class)  // or use directly
public HealthCheck linuxHealthCheck() {
    return new LinuxHealthCheck();
}

The ConditionContext provides access to the BeanDefinitionRegistry (to check what beans have already been registered), the ConfigurableListableBeanFactory (to inspect existing bean definitions), the Environment (to read properties), the ResourceLoader (to check for classpath resources), and the ClassLoader (to check for class presence). These allow you to build arbitrarily sophisticated conditions.

@Profile is the built-in example every Spring Framework developer already uses without thinking about it: @Profile(“prod”) is meta-annotated with @Conditional(ProfileCondition.class), and ProfileCondition’s matches() method simply checks context.getEnvironment().acceptsProfiles(…). Once you see that, writing your own condition — like the OnLinuxCondition example above — follows exactly the same pattern: read something off ConditionContext, and return true or false.

// More complex condition — uses multiple ConditionContext capabilities
public class OnProductionProfileCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // Check active profiles
        Environment env = context.getEnvironment();
        String[] activeProfiles = env.getActiveProfiles();
        for (String profile : activeProfiles) {
            if ("prod".equals(profile)) return true;
        }
        // Also check a property
        return "true".equals(env.getProperty("app.production-mode"));
    }
}
🎯 Interview Insight:  A key ordering detail: conditions on @Configuration classes are evaluated before any @Bean methods inside them are processed. If the class-level condition fails, none of its @Bean methods are registered — regardless of method-level @Conditional annotations. A similar pitfall applies to any custom Condition that checks whether a bean has already been registered via BeanDefinitionRegistry: bean registration order during container startup is not guaranteed unless you enforce it explicitly — with @DependsOn, or by controlling @Configuration class ordering — so a Condition relying on registration order can produce unreliable results across different startup paths.

Q15. What is @Order and the Ordered interface — where does it apply, and what does it NOT control?

Answer – @Order and its programmatic equivalent, the Ordered interface, control the relative ordering of components when Spring collects multiple beans of the same type into a List or applies them in a sequence. Lower values have higher priority and are processed first. The constant Ordered.HIGHEST_PRECEDENCE (Integer.MIN_VALUE) means first, and Ordered.LOWEST_PRECEDENCE (Integer.MAX_VALUE) means last.

@Order applies in several specific Spring contexts. For BeanPostProcessors and BeanFactoryPostProcessors, @Order controls the sequence in which processors run. For Filter beans registered in a Servlet-based web application, @Order controls the filter chain ordering. For @EventListener methods that handle the same event, @Order controls the execution sequence. For multiple Aspect classes or advice, @Order on @Aspect controls which aspect’s advice runs first around the same join point. For classes that implement SmartInitializingSingleton or listen for ContextRefreshedEvent, @Order (or the Ordered interface) controls the startup execution sequence when multiple exist.

// BeanPostProcessor ordering
@Component
@Order(1)
public class SecurityBeanPostProcessor implements BeanPostProcessor { }

@Component
@Order(2)
public class AuditBeanPostProcessor implements BeanPostProcessor { }

// Event listener ordering
@Component
public class OrderEventListeners {

    @EventListener
    @Order(1)
    public void validateFirst(OrderPlacedEvent event) {
        // runs first
    }

    @EventListener
    @Order(2)
    public void auditSecond(OrderPlacedEvent event) {
        // runs second
    }
}

// Collecting ordered beans into a list
@Service
public class OrderProcessingPipeline {

    @Autowired
    private List<OrderValidator> validators;  // ordered by @Order

    public void process(Order order) {
        validators.forEach(v -> v.validate(order));
    }
}

When Spring injects a List<T>, it collects all beans of type T and sorts them by their @Order value before returning the list. This is a powerful pattern for building ordered processing pipelines — validators, filters, enrichers, or handlers — where the sequence matters and new stages can be added without modifying existing code.

What @Order does NOT control is equally important. @Order does not control the order in which beans are created (instantiated) by the Spring container. Bean creation order is determined by dependency relationships — if bean A depends on bean B, B is created first, regardless of their @Order values. @Order also does not affect which bean is injected when a single bean of a type is expected — @Primary and @Qualifier handle that. And @Order does not control the execution order of @Transactional transaction interceptors at the same proxy level.

@Order controls@Order does NOT control
Order in injected List<T> collectionsBean instantiation/creation order
BeanPostProcessor execution sequenceWhich bean is chosen for single @Autowired injection
Servlet Filter chain ordering@Transactional advice ordering (same proxy level)
@EventListener execution sequenceDependency resolution — @Primary/@Qualifier handle this
ApplicationRunner / CommandLineRunner sequenceDatabase query execution order
🎯 Interview Insight:  A common interview trap: ‘Does @Order guarantee the creation order of beans?’ No. The only thing that guarantees bean A is created before bean B is an actual dependency relationship — either through injection (@Autowired, constructor dependency), or through @DependsOn. @Order has no effect on creation order. It only affects ordering in collection injection and in sequenced processing lists.

Conclusion

Spring’s annotation system is not just syntactic sugar — each annotation you have seen in this article is backed by a specific BeanPostProcessor, AOP proxy, or framework mechanism that runs at a precise point in the container lifecycle. Understanding these mechanisms allows you to confidently debug production issues and answer advanced Spring interview questions with the depth that top companies expect.

Key takeaways from this article:

  • @Value and @ConfigurationProperties both inject external configuration, but @ConfigurationProperties is preferred for groups of related properties because it supports relaxed binding, validation, and IDE auto-completion.
  • Stereotype annotations (@Service, @Repository, @Controller) are all @Component at their core. @Repository is the only one with additional framework behavior: exception translation.
  • Spring Events decouple publishers from listeners. Combine @EventListener with @TransactionalEventListener to ensure listeners only run after a successful transaction commit.
  • @Async requires @EnableAsync and always uses a custom ThreadPoolTaskExecutor in production. The default SimpleAsyncTaskExecutor is thread-per-task with no pooling.
  • @Configuration classes use CGLIB proxying to ensure @Bean inter-method calls return the container-managed singleton, not a new Java object.
  • @Conditional lets you register beans conditionally based on runtime state — the exact mechanism Spring itself uses to implement @Profile. You can build any custom condition by implementing the Condition interface and using ConditionContext.
  • @Order controls execution sequence in collections and processor chains. It does NOT control the order of bean creation.

Leave a Comment