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

If you are preparing for a Spring or Spring Boot interview, mastering Spring annotations interview questions is non-negotiable. 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 separates candidates who can use Spring from candidates who understand how Spring works internally.

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 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 @ConfigurationProperties vs @Value — when to use which?

Answer – Both @ConfigurationProperties and @Value inject external configuration into Spring beans, but they serve different purposes and have important behavioral differences. Choosing between them depends on the complexity and structure of the configuration you are binding.

@Value is field-level injection. It injects a single value per annotation from the Spring Environment using either a property placeholder or a SpEL expression. It is fine for injecting one or two individual properties into a bean. However, it does not support relaxed binding, type-safe validation, or documentation via @ConfigurationPropertiesMetadata.

@ConfigurationProperties binds an entire group of related properties to a POJO in one shot. It supports relaxed binding — meaning app.maxConnections, app.max-connections, APP_MAX_CONNECTIONS, and app.max_connections all map to the same field. It integrates with Spring’s JSR-303 validation via @Validated. It is the recommended approach for any structured configuration with multiple properties.

// application.properties
app.datasource.url=jdbc:mysql://localhost/mydb
app.datasource.username=root
app.datasource.max-connections=20
app.datasource.timeout-ms=5000

// @ConfigurationProperties approach — binds all at once
@ConfigurationProperties(prefix = "app.datasource")
@Component
@Validated
public class DataSourceProperties {

    @NotBlank
    private String url;
    private String username;
    private int maxConnections;
    private long timeoutMs;

    // getters and setters required
}

// @Value approach — one annotation per property
@Component
public class DataSourceConfig {
    @Value("${app.datasource.url}")
    private String url;
    @Value("${app.datasource.max-connections}")
    private int maxConnections;
    // requires one @Value per field
}
AspectRecommendation
Single property injectionUse @Value
Group of related propertiesUse @ConfigurationProperties
Type-safe validation neededUse @ConfigurationProperties + @Validated
SpEL expression neededUse @Value with #{…}
IDE auto-complete in application.propertiesUse @ConfigurationProperties
Quick prototype or simple beanUse @Value
🎯 Interview Insight:  @ConfigurationProperties requires @EnableConfigurationProperties or @ConfigurationPropertiesScan in Spring Boot to be picked up, unless the class is also annotated with @Component. In Spring Boot 2.2+, @ConfigurationProperties classes can be registered as beans via @ConfigurationPropertiesScan without @Component. This is the recommended pattern as it keeps configuration classes free of Spring stereotypes.

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 communicates intent clearly. 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.

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(). ApplicationStartedEvent and ApplicationReadyEvent are Spring Boot-specific events for the application startup sequence.

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 the listener only runs 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 the listener needing 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 such as sending emails, writing logs, or calling external APIs, this synchronous behavior can significantly slow 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
@SpringBootApplication
@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 Spring Boot 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 your main @SpringBootApplication class or a dedicated @Configuration class.

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; it submits a Callable wrapping 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 end of 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.

Q12. What is ApplicationRunner vs CommandLineRunner — when to use which?

Answer – Both ApplicationRunner and CommandLineRunner are interfaces in Spring Boot that allow you to execute code after the Spring ApplicationContext has been fully initialized and the application is ready to accept traffic. They are the recommended way to perform startup logic that requires all beans to be available — such as database seeding, cache warming, or running migration checks.

The key difference is in how they receive command-line arguments. CommandLineRunner provides raw String[] args — the same array you get in a standard Java main method. ApplicationRunner provides an ApplicationArguments object, which offers named argument parsing, option arguments (prefixed with –), and non-option arguments as separate collections.

// CommandLineRunner — raw string array
@Component
@Order(1)
public class DatabaseSeeder implements CommandLineRunner {

    @Autowired
    private UserRepository userRepository;

    @Override
    public void run(String... args) throws Exception {
        if (userRepository.count() == 0) {
            userRepository.save(new User("admin", "admin@example.com"));
        }
    }
}

// ApplicationRunner — parsed ApplicationArguments
@Component
@Order(2)
public class AppStartupRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // java -jar app.jar --env=prod --debug report.csv
        boolean debug = args.containsOption("debug");
        String env = args.getOptionValues("env").get(0);  // "prod"
        List<String> files = args.getNonOptionArgs();       // ["report.csv"]
        System.out.println("Starting in " + env + " mode, debug=" + debug);
    }
}

Both interfaces run after ApplicationReadyEvent is fired, meaning the full context is up, and the embedded server is ready. This makes them safe for any initialization logic that needs live beans, transactions, or active database connections. If you have multiple runners, use @Order to control execution sequence — lower order value runs first.

If a runner throws an exception, the Spring Boot application fails to start. Spring Boot catches the exception, logs it, and exits. This is intentional — a startup failure in a runner should not result in a partially initialized application serving traffic.

Use CaseRecommended Interface
Simple startup logic, no CLI args neededEither — CommandLineRunner is slightly simpler
Parsing named/option CLI arguments (–key=value)ApplicationRunner
Database seeding or migration checksCommandLineRunner or ApplicationRunner
Cache warming after all beans are readyApplicationRunner
Integration tests needing startup logicCommandLineRunner (easier to mock)
🎯 Interview Insight:  @PostConstruct runs during bean initialization — before the context is fully started and before the embedded server is ready. ApplicationRunner and CommandLineRunner run after everything is up and ApplicationReadyEvent has fired. For any initialization that requires a running transaction, an active data source connection, or interaction with other fully initialized beans, always prefer ApplicationRunner or CommandLineRunner over @PostConstruct.

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 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. Spring Boot’s auto-configuration classes use proxyBeanMethods = false extensively for performance since they never call other @Bean methods directly — dependencies are always passed as method parameters.

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 foundation of Spring Boot’s entire auto-configuration system — every @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty, and @ConditionalOnWebApplication annotation is built on top of @Conditional.

@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.

Spring Boot’s built-in conditions follow a naming convention that reflects their behavior. @ConditionalOnClass checks whether a class is on the classpath. @ConditionalOnMissingBean checks whether no bean of a type is registered yet. @ConditionalOnProperty checks whether a specific property has a given value. @ConditionalOnWebApplication checks whether the application context is a web application context.

// 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. Also, @ConditionalOnMissingBean conditions should only be used in auto-configuration classes (in spring.factories or META-INF/spring/…) and not in user @Configuration classes. In user code, the order of bean registration during context startup is not guaranteed, so @ConditionalOnMissingBean in user configuration can produce unreliable results.

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 in a Spring Boot web application, @Order controls the order of the filter chain. For @EventListener methods that handle the same event, @Order controls the execution order. For multiple Aspect classes or advice, @Order on @Aspect controls which aspect’s advice runs first around the same join point. For ApplicationRunner and CommandLineRunner, @Order controls the startup execution sequence.

// 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 is the foundation of Spring Boot auto-configuration. 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