Spring Core: IoC, DI, Bean Lifecycle, ApplicationContext

Master Spring Framework core concepts: Inversion of Control, Dependency Injection patterns, Bean lifecycle phases, and ApplicationContext variants.

published: reading time: 30 min read author: GeekWorkBench
Quick Summary

Master Spring Framework core concepts: Inversion of Control, Dependency Injection patterns, Bean lifecycle phases, and ApplicationContext variants. The guide uses practical examples to explain introduction to spring framework core, inversion of control (ioc) and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.

Spring Core: IoC, DI, Bean Lifecycle, ApplicationContext

Spring Framework sits at the heart of modern Java enterprise development. Whether you are building a microservices architecture or a monolith application, Spring handles the heavy lifting of object creation, dependency management, and lifecycle orchestration. This guide covers the four foundational concepts that make it all work: Inversion of Control, Dependency Injection, Bean Lifecycle, and ApplicationContext.

By the end of this post, you will understand how Spring manages your objects, why the framework is structured the way it is, and how to avoid the traps that catch most developers learning Spring for the first time.


Introduction to Spring Framework Core

The Spring Framework is built around a single powerful idea: let the framework manage the objects your application needs rather than having your code create and wire them together manually. This shift in responsibility is not just a convenience; it fundamentally changes how you design applications.

When you annotate a class with @Component, mark a constructor with @Autowired, or declare a @Bean method in a configuration class, you are participating in Spring’s dependency injection container. Understanding how this container operates helps you debug issues, optimize startup time, and make architectural decisions with confidence.

The core Spring Framework consists of several modules, but the container itself lives in the spring-core and spring-beans packages. Everything else — web, data, security — builds on top of this foundation.

Before Spring, wiring dependencies manually created brittle code:

// Manual wiring — OrderService owns its dependencies' lifecycles
public class OrderService {
    private final PaymentProcessor processor = new PayPalProcessor();
    private final InventoryRepository repo = new JdbcInventoryRepository();

    public Order processOrder(String orderId) {
        if (!repo.exists(orderId)) throw new IllegalArgumentException("Not found");
        return new Order(orderId, processor.charge(orderId));
    }
}

With Spring, the container handles construction and wiring — your code just declares what it needs:

// Spring injects these at construction time
@Service
public class OrderService {
    private final PaymentProcessor processor;
    private final InventoryRepository repo;

    public OrderService(PaymentProcessor processor, InventoryRepository repo) {
        this.processor = processor;
        this.repo = repo;
    }

    public Order processOrder(String orderId) {
        if (!repo.exists(orderId)) throw new IllegalArgumentException("Not found");
        return new Order(orderId, processor.charge(orderId));
    }
}

The second version has no idea how PaymentProcessor or InventoryRepository are created — Spring supplies them, and you can swap implementations by changing configuration, not code.


Inversion of Control (IoC)

Inversion of Control is a design principle where the control over program flow shifts from your code to an external framework. In traditional programming, your main method creates objects, calls methods, and manages the call stack. With IoC, the framework handles instantiation and method invocation, and your code provides callback hooks.

Picture a theme park. In a traditional approach, you plan the route, when to eat, and where to go next. With IoC, you buy a ticket and let the park’s schedule determine your experience — you just show up for each ride.

Hollywood Principle

IoC is frequently described with the phrase “Don’t call us, we’ll call you.” This comes from Hollywood studios who told aspiring actors not to call the studio for work; the studio would contact them when needed. In Spring, this means your code defines behavior through callbacks (methods annotated with @PostConstruct, for example), and Spring invokes them at the appropriate time.

What IoC Gives You

  • Decoupling — Your business logic does not depend on concrete implementations of collaborator classes
  • Testability — You can inject mock dependencies without involving the full Spring container
  • Flexibility — You can swap implementations by changing configuration, not code
  • Lifecycle management — The framework handles setup and teardown consistently

The IoC Container

In Spring, the IoC container is the ApplicationContext. It holds your bean definitions, resolves dependencies, and manages the complete lifecycle of every managed object. When the application starts, Spring reads configuration metadata (XML, annotations, or Java configuration) and builds a graph of objects and their dependencies.

// Traditional approach: your code creates dependencies
public class OrderService {
    private final PaymentProcessor processor = new PayPalProcessor();
    private final InventoryRepository repo = new JdbcInventoryRepository();
}

// IoC approach: dependencies are injected by the container
public class OrderService {
    private final PaymentProcessor processor;
    private final InventoryRepository repo;

    public OrderService(PaymentProcessor processor, InventoryRepository repo) {
        this.processor = processor;
        this.repo = repo;
    }
}

The second version has no knowledge of how PaymentProcessor or InventoryRepository are created. The container supplies them. This is the essence of Inversion of Control.


Dependency Injection (DI)

Dependency Injection is the mechanism through which IoC is implemented in Spring. Rather than your code instantiating its own dependencies, the container provides them. Spring supports three primary injection patterns.

Constructor Injection

Dependencies are supplied through a class constructor. Spring calls the constructor with the required beans.

@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;

    @Autowired
    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
}

Constructor injection is the Spring-recommended approach because it makes dependencies explicit, enables immutability (final fields), and works well with testing frameworks. When all dependencies are constructor-injected, the object is fully constructed by the time you receive it.

Since Spring 4.3, the @Autowired annotation is optional on constructors with a single constructor, but you should include it for clarity.

Setter Injection

Dependencies are provided through setter methods that the container calls after constructing the bean.

@Service
public class NotificationService {
    private MessageSender messageSender;

    @Autowired
    public void setMessageSender(MessageSender messageSender) {
        this.messageSender = messageSender;
    }
}

Setter injection is useful when a dependency is optional or when you need to reconfigure the bean at runtime. It creates mutable objects, which can make reasoning about state more difficult. Use it when constructor injection is not practical.

Field Injection

Dependencies are injected directly into fields, even private ones, using reflection.

@Service
public class InventoryService {
    @Autowired
    private WarehouseClient warehouseClient;

    @Autowired
    private CacheService cacheService;
}

Field injection is concise but has serious drawbacks. It makes dependencies invisible in the class’s public API, prevents immutability, and makes unit testing difficult without a Spring context. The Spring team explicitly recommends avoiding field injection in favor of constructor or setter injection.

// Field injection is difficult to test without Spring
// This will fail with NullPointerException in a unit test
@Test
void testInventoryService() {
    InventoryService service = new InventoryService();
    service.processReorder(); // warehouseClient is null
}

Which Injection Pattern to Use

Pattern Immutable Testable Optional Dependencies Readability
Constructor Yes Excellent Explicit via overloaded constructors High
Setter No Good Yes, via required=false Medium
Field No Poor Via @Autowired(required=false) Low

Prefer constructor injection. Reserve setter injection for genuinely optional dependencies. Never use field injection in production code.

Resolving Multiple Implementations

When you have multiple beans of the same type, you must tell Spring which one to inject.

// Use @Primary to designate a default
@Bean
@Primary
public PaymentProcessor defaultProcessor() {
    return new StripeProcessor();
}

// Or use @Qualifier to specify by name
@Service
public class CheckoutService {
    private final PaymentProcessor paypalProcessor;

    public CheckoutService(@Qualifier("paypalProcessor") PaymentProcessor paypalProcessor) {
        this.paypalProcessor = paypalProcessor;
    }
}

// Or use @Resource by name
@Resource(name = "braintreeProcessor")
private PaymentProcessor braintreeProcessor;

Bean Lifecycle

Every Spring bean goes through a well-defined lifecycle from instantiation to destruction. Understanding these phases helps you hook into the container at the correct points and avoid the initialization errors that catch developers who skip proper setup hooks.

Lifecycle Phases

graph TB
    A[Instantiation] --> B[Populate Properties]
    B --> C[setBeanName Callback]
    C --> D[setBeanFactory Callback]
    D --> E[postProcessBeforeInitialization<br/>BeanPostProcessor]
    E --> F[Initialization Callback<br/>@PostConstruct<br/>InitializingBean.afterPropertiesSet<br/>@Bean initMethod]
    F --> G[postProcessAfterInitialization<br/>BeanPostProcessor]
    G --> H[Bean Ready for Use]
    H --> I[Destruction Callback<br/>@PreDestroy<br/>DisposableBean.destroy<br/>@Bean destroyMethod]
    I --> J[Bean Destroyed]

Detailed Phase Breakdown

Phase 1: Instantiation

Spring creates a new bean instance using the class’s no-argument constructor. If your class does not have a no-arg constructor and you use constructor injection, Spring uses that constructor instead. This is one reason constructor injection tends to be cleaner.

Phase 2: Populate Properties

Spring injects values into bean properties marked with @Value, @Autowired, or @Resource, including autowiring other beans by type or name.

Phase 3: Initialization Callbacks

During initialization, your bean can run setup code in three ways:

// Option 1: @PostConstruct annotation
@Component
public class DataInitializer {
    @PostConstruct
    public void init() {
        // Runs after property population
        cache.warmUp();
    }
}

// Option 2: InitializingBean interface
@Component
public class ServiceInitializer implements InitializingBean {
    @Override
    public void afterPropertiesSet() {
        // Runs after all bean properties are set
        establishConnections();
    }
}

// Option 3: Custom init method in @Bean
@Configuration
public class AppConfig {
    @Bean(initMethod = "startup")
    public TaskScheduler scheduler() {
        return new ThreadPoolTaskScheduler();
    }
}

Use @PostConstruct. It comes from jakarta.annotation (formerly javax.annotation), it makes initialization intent explicit in the code, and it works across different DI containers.

Phase 4: Destruction Callbacks

When the application context closes or the bean is removed, cleanup code runs.

@Component
public class ConnectionManager {
    private Connection connection;

    @PreDestroy
    public void cleanup() {
        connection.close();
    }
}

// Or via DisposableBean
@Component
public class FileHandler implements DisposableBean {
    @Override
    public void destroy() {
        releaseResources();
    }
}

BeanPostProcessor

BeanPostProcessor is a special interface that allows you to intercept the bean lifecycle and modify the bean instance before or after initialization.

@Component
public class ValidationPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        if (bean instanceof Validatable) {
            ((Validatable) bean).validate();
        }
        return bean;
    }
}

Spring itself ships several BeanPostProcessor implementations for features like @Autowired, @PostConstruct, and @PreDestroy annotation processing.


ApplicationContext vs BeanFactory

Spring provides two fundamental container interfaces: BeanFactory and ApplicationContext. Both share a common ancestor, but they serve different use cases.

BeanFactory

BeanFactory is the root interface for Spring’s IoC container. It provides basic DI capabilities with minimal overhead.

BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
ProductService service = factory.getBean("productService", ProductService.class);

BeanFactory is lazy by default — beans are instantiated only when getBean() is called. This makes it suitable for resource-constrained environments where startup time and memory usage must be minimized.

ApplicationContext

ApplicationContext extends BeanFactory and adds enterprise-grade features. This is the interface you work with in most Spring applications.

// Annotation-based application context
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

// ClassPathXmlApplicationContext
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

// FileSystemXmlApplicationContext
ApplicationContext context = new FileSystemXmlApplicationContext("src/main/resources/context.xml");

Comparison Table

Feature BeanFactory ApplicationContext
Bean instantiation Lazy Eager
Internationalization Not supported Built-in via MessageSource
AOP integration Manual Automatic proxy creation
Web applications Not designed for WebApplicationContext support
Event publication Not supported Built-in event system
Resource loading Manual Built-in Resource abstraction
Enterprise services None Transaction management, etc.
Startup time Faster Slightly slower
Memory footprint Lower Higher

When to Use Each

Use BeanFactory only in very specific scenarios: mobile applications, lightweight SDKs, or situations where every kilobyte matters. In virtually all server-side Java applications, ApplicationContext is the correct choice.

In a typical Spring Boot application, you never instantiate either directly — SpringApplication.run() creates a ConfigurableApplicationContext (a hybrid interface) behind the scenes.

Common ApplicationContext Implementations

  • AnnotationConfigApplicationContext — Reads configuration from @Configuration classes
  • ClassPathXmlApplicationContext — Loads XML configuration from the classpath
  • FileSystemXmlApplicationContext — Loads XML configuration from the filesystem
  • WebApplicationContext — Designed for web applications, tied to the ServletContext

When to Use / When NOT to Use

When to Use Spring Core

  • Build enterprise Java applications that require loose coupling and testability
  • Work with a team where dependency management through configuration prevents integration conflicts
  • Need to manage complex object graphs with many dependencies and lifecycles
  • Require the rich ecosystem that builds on Spring Core (Spring Data, Spring Security, Spring Cloud)

When NOT to Use Spring Core

  • Building a simple command-line utility where the overhead of a DI container is unjustified
  • Working in a constrained environment with severe memory limits (some embedded systems)
  • Creating a library that should not impose a dependency on Spring
  • Building a high-frequency trading system where every microsecond of startup time matters (prefer manual DI or a lighter container)

Common Pitfalls

Circular dependencies occur when two or more beans depend on each other through constructors.

// This causes a circular dependency error
@Service
public class ServiceA {
    private final ServiceB serviceB;
    public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; }
}

@Service
public class ServiceB {
    private final ServiceA serviceA;
    public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; }
}

Fix circular dependencies by using setter injection instead of constructor injection for at least one of the circular partners, or extract the shared dependency into a third service.

Bean not found errors happen when the container cannot resolve a dependency. The error message includes the bean name that could not be found and lists the available beans of the expected type.

No qualifying bean of type 'com.example.PaymentProcessor' available

Common causes: the bean is not annotated with a stereotype (@Component, @Service, etc.), the class is not scanned (check @ComponentScan), or the bean is in a configuration class not being processed.

Scoped beans accessed from singleton is a design issue where a bean with a shorter scope (like prototype-scoped) is injected into a singleton. The prototype bean gets created only once, when the singleton is constructed, which defeats its purpose.

@Bean
@Scope("prototype")
public MyHelper myHelper() {
    return new MyHelper();
}

@Service
public class SingletonService {
    // This helper is created once and reused — not the intended behavior
    private final MyHelper myHelper;

    public SingletonService(MyHelper myHelper) {
        this.myHelper = myHelper;
    }
}

Use ObjectFactory<MyHelper> or Provider<MyHelper> to delay prototype bean creation, or use ApplicationContext directly to request a new instance each time.

Conflicting bean definitions occur when multiple @Bean methods or stereotype annotations produce beans with the same name. Spring resolves this based on Primary attributes and BeanDefinitionOverride settings. In Spring Boot 2.1+, bean definition overriding is disabled by default.


Security Notes

Spring Framework Core does not handle authentication or authorization directly — that is Spring Security’s job. But there are still security-relevant considerations at the container level worth knowing.

Avoid storing sensitive data in bean properties. If you must store passwords, API keys, or tokens in Spring configuration, use environment variables or a dedicated secrets manager rather than plain text in configuration files.

// Hardcoded credentials — do not do this
@Value("password123") private String password;

// From environment variable — better
@Value("${DB_PASSWORD}") private String password;

// From Spring Cloud Vault or AWS Secrets Manager — production standard
// @Value("${DB_PASSWORD}") resolves to the secret from Vault/ASM at runtime

Be cautious with @ComponentScan packages. Scanning too broad a package range can inadvertently expose utility classes as Spring beans that should not be managed by the container.

Lazy initialization security implications. Lazy initialization reduces attack surface at startup, but it can cause errors during request processing when the first user action triggers expensive bean creation. Factor this into your threat model before relying on lazy initialization as a security strategy.


Implementation Snippets

Full Spring Configuration Class

@Configuration
@ComponentScan(basePackages = "com.example")
public class ApplicationConfig {

    @Bean
    public DataSource dataSource(
            @Value("${db.url}") String url,
            @Value("${db.username}") String username,
            @Value("${db.password}") String password) {
        return DataSourceBuilder.create()
                .url(url)
                .username(username)
                .password(password)
                .build();
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

Multi-Bean Configuration with Qualifier

@Configuration
public class PaymentConfig {

    @Bean
    @Primary
    public PaymentProcessor stripeProcessor() {
        return new StripeProcessor();
    }

    @Bean
    public PaymentProcessor paypalProcessor() {
        return new PayPalProcessor();
    }

    @Bean
    public PaymentProcessor braintreeProcessor() {
        return new BraintreeProcessor();
    }
}

Lazy Initialization

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

// Or on a specific bean
@Bean
@Lazy
public ExpensiveService expensiveService() {
    return new ExpensiveService();
}

Observability Checklist

Ensuring your Spring beans behave correctly requires visibility into the container and the beans themselves.

  • Verify ApplicationContext starts successfully without missing bean errors
  • Confirm BeanPostProcessor implementations execute in the expected phase
  • Monitor initialization order when beans depend on each other
  • Check that @PostConstruct and @PreDestroy methods execute without throwing exceptions
  • Validate that prototype-scoped beans are created on each request, not only once
  • Ensure lazy beans do not cause unexpected delays on first access
  • Confirm circular dependency detection throws descriptive errors
  • Verify property placeholders resolve correctly from environment variables

Trade-off Table

Design Decision Benefit Cost
Constructor injection Immutable, testable, explicit dependencies Verbose with many constructor parameters
Setter injection Flexibility for optional dependencies Mutable state, harder to reason about
Field injection Concise syntax Hidden dependencies, untestable, no immutability
Eager initialization Fail-fast at startup Slower startup, higher memory at launch
Lazy initialization Faster startup Fail-late, potential latency spikes on first use
@ComponentScan Auto-discovery of components Implicit bean creation, possible naming conflicts
@Bean in @Configuration Fine-grained control More verbose, manual instance management
Prototype scope Fresh instance each time Memory pressure if overused, manual cleanup

Failure Scenarios

ApplicationContext fails to start. This usually means a configuration problem was caught early. Missing beans, unresolvable property placeholders, and circular dependencies all prevent startup. The error message tells you which bean is the problem and why. Fix the configuration before moving on.

Bean works in development but fails in production. This usually comes from different environment variables, missing profiles, or different component scanning paths between environments. Check your @ActiveProfiles annotation and make sure your test profiles cover all the beans your application needs in production.

Memory grows unbounded with prototype-scoped beans. Prototype-scoped beans are not managed by Spring after creation. If you keep requesting them without releasing references, memory grows. Release references when they are no longer needed, or consider a pooling strategy instead.

Initialization runs twice. This happens when a bean is both scanned via @ComponentScan and registered as a @Bean manually. Spring catches this conflict and throws an error unless bean definition overriding is enabled. Pick one registration mechanism and stick with it.

Singleton bean sees stale state in a prototype bean. If you inject a prototype bean into a singleton via constructor injection, you get one prototype instance created when the singleton was constructed. Use ObjectFactory or Provider to get a fresh instance on each call.


Quick Recap Checklist

  • Spring Core implements IoC through its dependency injection container
  • Constructor injection is preferred — immutable, testable, explicit
  • Setter injection is for optional dependencies only
  • Field injection should never appear in production code
  • Bean lifecycle: Instantiate -> Populate -> PostProcessBeforeInitialization -> Initialize -> PostProcessAfterInitialization -> Ready -> Destroy
  • Use @PostConstruct for initialization, @PreDestroy for cleanup
  • ApplicationContext extends BeanFactory with enterprise features
  • BeanFactory is lazy; ApplicationContext is eager by default
  • Circular dependencies through constructors break at startup
  • Prototype beans injected into singletons get created only once
  • Use @Qualifier or @Primary when multiple beans of the same type exist
  • Spring Boot auto-configuration builds on top of Spring Core

Bean Scopes Deep Dive

Spring provides several bean scopes beyond the default singleton. Each scope controls the lifecycle of the bean instance and when it is created.

Available Scopes

Scope Description Use Case
singleton One instance per Spring container (default) Stateless services, repositories
prototype New instance on each request Stateful beans, heavy-weight objects
request One instance per HTTP request Web request-scoped data
session One instance per HTTP session User-specific data
application One instance per ServletContext Shared application-level state
websocket One instance per WebSocket session WebSocket-scoped beans

Scope Inheritance

Scopes can be combined with custom annotations through ScopeMetadataResolver:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope("prototype")
public @interface HeavyWeight {
}

Choosing the Right Scope

Use singleton for stateless services where shared state is not required. Use prototype when you need a fresh instance per use and are willing to manage cleanup manually. Request and session scopes are web-specific and automatically managed by Spring MVC or Spring WebFlux. Avoid application and websocket scopes unless you have a specific need for extended bean lifecycles.


Interview Questions

1. What is the difference between Inversion of Control and Dependency Injection?

Inversion of Control is the broader design principle — the framework controls program flow rather than your code calling framework libraries directly. Dependency Injection is one specific implementation of IoC where the framework injects dependencies into your objects rather than your code constructing them.

Think of IoC as the concept and DI as the mechanism. Other IoC patterns include the Strategy pattern and the Template Method pattern. Spring, Google Guice, and Jakarta CDI all implement IoC through DI.

2. Explain the Spring Bean lifecycle from instantiation to destruction.

Spring beans move through distinct phases. The container instantiates the bean using its constructor, then populates properties by injecting autowired dependencies and values. Before initialization runs, BeanPostProcessor.postProcessBeforeInitialization wraps the bean. Initialization callbacks then execute — @PostConstruct methods, InitializingBean.afterPropertiesSet(), or custom initMethod declarations. After initialization, BeanPostProcessor.postProcessAfterInitialization wraps the bean again. The bean is now ready for use. When the container shuts down, destruction callbacks run via @PreDestroy, DisposableBean.destroy(), or destroyMethod, after which the bean is garbage collected.

3. Why is constructor injection preferred over field injection in Spring?

Constructor injection produces immutable objects because dependencies are stored in final fields. This eliminates an entire category of bugs where a bean field is accessed before it is set. Constructor injection also makes dependencies explicit in the public API — any caller must provide all required dependencies. This makes the class self-documenting and prevents subtle bugs where a developer creates the class without realizing a dependency is null. Testing is simpler because you can instantiate the class directly with mock dependencies. Field injection hides dependencies behind private fields, making both the API and the test strategy unclear.

4. What happens when you inject a prototype-scoped bean into a singleton?
The prototype bean is created exactly once — when the singleton is constructed. The same instance is then held by the singleton for its entire lifetime. This defeats the purpose of prototype scope.
@Service
public class SingletonService {
    // Prototype created once at singleton construction — not what you want
    private final MyHelper myHelper;

    public SingletonService(MyHelper myHelper) {
        this.myHelper = myHelper;
    }
}

// Fix: inject ObjectFactory<T> for lazy, fresh instances
@Service
public class SingletonServiceFixed {
    private final ObjectFactory<MyHelper> myHelperFactory;

    public SingletonServiceFixed(ObjectFactory<MyHelper> myHelperFactory) {
        this.myHelperFactory = myHelperFactory;
    }

    public void process() {
        MyHelper helper = myHelperFactory.getObject(); // new instance each call
    }
}

Inject ObjectFactory<T> or Provider<T> to delay the lookup. Alternatively, inject ApplicationContext directly and call getBean() — though this couples your code to Spring.

5. What is the difference between BeanFactory and ApplicationContext in Spring?

BeanFactory is the minimal IoC container interface — it loads beans lazily and provides basic DI capabilities. ApplicationContext extends BeanFactory and adds enterprise features: internationalization (MessageSource), event publication, automatic AOP proxy creation, and web application support via WebApplicationContext.

In practice, use ApplicationContext for all standard applications. Use BeanFactory only in memory-constrained environments where lazy loading provides meaningful benefit. Spring Boot's SpringApplication.run() creates a ConfigurableApplicationContext, the hybrid interface that supports both programmatic configuration and closing.

6. How does @ComponentScan work and what are its common pitfalls?
@ComponentScan tells Spring where to look for classes annotated with @Component, @Service, @Repository, @Controller, and other stereotypes. By default it scans the package of the annotated configuration class and all sub-packages.

Common pitfalls — and how to fix them:

// ❌ Pitfall: scans entire com.example and all sub-packages
// This picks up utility classes, DTOs, and third-party libs as beans
@ComponentScan(basePackages = "com.example")

// ✅ Fix: scan only the packages you need
@ComponentScan(basePackages = {"com.example.service", "com.example.repository"})

// ✅ Fix: use basePackageClasses to anchor scan to a known type
@ComponentScan(basePackageClasses = {UserService.class, OrderRepository.class})

// ❌ Pitfall: class not in scanned package — silently skipped
package com.xyz.util;          // not scanned
@Component
public class MetricsHelper { } // never registered as a bean

// ✅ Fix: verify with ApplicationContext.getBeanDefinitionNames()
String[] beans = context.getBeanDefinitionNames();
Arrays.stream(beans).filter(b -> b.contains("Helper")).forEach(System.out::println);

Use basePackages or basePackageClasses to narrow the scan precisely and avoid inadvertently promoting utility classes to Spring beans.

7. What is the difference between @Bean and @Component for defining Spring beans?

@Component and its stereotypes mark a class as a candidate for auto-discovery — Spring scans and creates instances of these classes automatically. @Bean lives inside a @Configuration class and defines a bean programmatically, giving you full control over instantiation logic.

Use @Component when the class is yours to annotate and auto-discovery is sufficient. Use @Bean when you need to create beans from third-party classes, apply complex instantiation logic, or define beans that should not be scanned. Mixing both on the same class can cause duplicate bean registration errors.

8. What is the purpose of @Primary and when would you use it over @Qualifier?

@Primary designates a default bean when multiple candidates of the same type exist. It is applied at the bean definition level and is used automatically when no specific bean is requested. @Qualifier selects a specific bean by name at the injection point and overrides @Primary.

Use @Primary when one implementation should be the sensible default across the entire application. Use @Qualifier when you need context-specific bean selection that varies by injection point. Combining both is valid — @Qualifier takes precedence at the specific injection site.

9. Explain how Spring resolves bean dependencies when there are multiple implementations.

Spring uses a multi-step resolution process. First it identifies all beans matching the required type. If exactly one exists, it is used. If multiple exist, Spring looks for a @Primary bean and uses it. If no @Primary exists, it looks for a @Qualifier matching the variable or parameter name. If ambiguity remains, Spring throws NoUniqueBeanDefinitionException.

You can resolve conflicts by adding @Primary to one bean, using @Qualifier at each injection point, or restructuring to eliminate the need for multiple implementations through interfaces.

10. What happens during component scanning and when might a class be missed?

Component scanning uses a ComponentScanMarker pattern where the scanner looks for classes with stereotype annotations within the configured package hierarchy. Classes can be missed when they are outside the scanned packages, when they lack stereotype annotations entirely, when they are in a package that is excluded via filters, or when the configuration class itself is not picked up by the scanner.

Additionally, if a class is instantiated directly via new rather than through Spring, it will not be a Spring-managed bean regardless of its annotations.

11. What is the lifecycle of a prototype-scoped bean compared to a singleton?

A singleton bean is created once when the container starts (or when first accessed in lazy mode) and lives for the entire application context lifetime. A prototype bean is created fresh each time it is injected or requested via getBean(), and the container does not manage its destruction — you are responsible for cleanup.

This makes prototype beans more expensive per injection but useful for stateful objects where you want a clean slate each time. The cost trade-off means prototypes should be used sparingly and only when truly needed.

12. How does @Value injection work and what are its limitations?

@Value injects property values from property sources, environment variables, or SpEL expressions into bean fields. It supports defaults with ${property:default} syntax and SpEL expressions like #{systemProperties['key']}.

Limitations include: it is string-based so no compile-time validation; it mixes configuration with bean logic; it makes testing harder because you must mock property sources; and it creates implicit dependencies that are not visible in constructors. Prefer external configuration classes and constructor injection for required values.

13. What is the difference between BeanPostProcessor and InitializingBean?

BeanPostProcessor is a container-level hook that runs before and after any bean is initialized — it can wrap or modify any bean in the container. InitializingBean is a callback interface implemented by a specific bean to run custom logic after all properties are set.

Use BeanPostProcessor when you need to intercept and process multiple beans generically, such as for annotation-driven validation or proxy creation. Use InitializingBean (or @PostConstruct) for bean-specific initialization logic.

14. Why does Spring recommend using @PostConstruct over InitializingBean?

@PostConstruct is a standard annotation from jakarta.annotation (formerly javax.annotation) that makes initialization intent declarative and method name flexible. InitializingBean is a Spring-specific interface that couples your class to the Spring API and requires implementing a specific method name afterPropertiesSet().

@PostConstruct keeps your code portable across different DI containers and makes it clear what the method does at a glance. It is the recommended approach in modern Spring applications.

15. How does Spring handle circular dependencies and what are the workarounds?
Spring detects circular dependencies through constructors and throws BeanCurrentlyInCreationException at startup.
// This causes a circular dependency error
@Service
public class ServiceA {
    private final ServiceB serviceB;
    public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; }
}

@Service
public class ServiceB {
    private final ServiceA serviceA;
    public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; }
}

Workarounds: use setter injection instead of constructor injection for at least one of the circular partners; extract the shared dependency into a third bean that both original beans depend on; or redesign the architecture to eliminate the circular relationship. Spring cannot resolve true constructor-level circular dependencies.

16. What is the difference between ApplicationContext and ConfigurableApplicationContext?

ApplicationContext is a read-only interface for the Spring container — you can refresh it but not modify bean definitions. ConfigurableApplicationContext adds methods for programmatic control: refresh(), registerShutdownHook(), adding bean factory post-processors, and modifying the environment.

SpringApplication.run() returns a ConfigurableApplicationContext, which is why you can call close() on it in tests or standalone applications. GenericApplicationContext is a full implementation that supports programmatic bean registration.

17. What are the advantages of lazy initialization in Spring?

Lazy initialization defers bean creation until first access instead of at container startup. This reduces startup time and memory pressure when many beans exist but few are used during typical runs. It is useful in large applications with expensive-to-construct beans that may not be needed in every execution path.

The trade-off is fail-late rather than fail-fast — an error that would have appeared at startup now appears on first access, potentially in a user-facing request. This can delay diagnosis and surface errors in unexpected locations.

18. How does Spring Boot auto-configuration build on Spring Core?

Spring Boot auto-configuration uses Spring Core's @Conditional annotations and BeanFactoryPostProcessor / BeanPostProcessor infrastructure to conditionally register beans based on classpath presence, property values, and environment settings. It automatically configures a DataSource if Spring Data JPA is on the classpath, for example.

Auto-configuration is additive to — not a replacement for — explicit Spring Core configuration. When you define a bean explicitly, it overrides any auto-configured bean of the same type. Understanding Spring Core is essential for debugging when auto-configuration behaves unexpectedly.

19. What is the role of BeanFactoryPostProcessor in the Spring container lifecycle?

BeanFactoryPostProcessor operates on the bean definition registry before any beans are instantiated. It can modify bean definition metadata — changing property values, adding or removing bean definitions, or inspecting the factory configuration. This is how Spring Boot's auto-configuration conditionally registers beans based on environment state.

Common implementations include PropertySourcesPlaceholderConfigurer for resolving @Value placeholders and ConfigurationClassPostProcessor for processing @Configuration classes. Unlike BeanPostProcessor, which works on bean instances, BeanFactoryPostProcessor works on bean definitions.

20. What are the implications of using field injection for testing?

Field injection makes unit testing difficult because you cannot construct the class with its dependencies directly — you need a Spring context or reflection-based injection to populate private fields. This adds test complexity and slows test execution compared to constructor injection where you simply call new MyService(mockDep1, mockDep2).

The inability to instantiate and test a bean without Spring is considered a serious design flaw. Constructor injection makes testing straightforward and is one of the strongest arguments for preferring it over field injection in all production code.


Further Reading

Conclusion

Spring Core provides the foundation that every Spring Boot application builds upon. Inversion of Control shifts object creation and wiring responsibility to the framework, letting you focus on business logic rather than plumbing. Dependency Injection, the primary mechanism for implementing IoC in Spring, comes in three forms: constructor injection for required dependencies, setter injection for optional ones, and field injection which should be avoided in production code due to its testability and immutability drawbacks.

The Bean lifecycle defines exactly how Spring manages your objects from instantiation through destruction. Understanding the callback phases — and knowing when to use @PostConstruct and @PreDestroy rather than the older InitializingBean and DisposableBean interfaces — lets you hook into the container at precisely the right moments. BeanPostProcessor extends this by letting you intercept and modify any bean in the container, which is the basis for much of Spring’s annotation-driven behavior.

ApplicationContext is the full-featured IoC container that most applications use. It extends BeanFactory with internationalization, event publication, automatic AOP proxy creation, and web application support. For virtually all server-side Java applications, ApplicationContext is the correct choice — reserve BeanFactory for resource-constrained environments where lazy loading provides measurable benefit.

Spring’s relaxed binding connects external configuration to typed Java objects without requiring you to match naming conventions exactly. Combined with @ConfigurationProperties, this gives you type-safe, validated, IDE-autocompleteable configuration that adapts cleanly across environments from development laptops to production Kubernetes clusters.

Category

Related Posts

Spring Boot Build Tools: Maven & Gradle

Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.

#spring-boot #spring-boot-roadmap #learning-path

Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow

Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.

#spring-boot #spring-boot-roadmap #learning-path

JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests

Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.

#spring-boot #spring-boot-roadmap #learning-path