Spring Core Explained
Inversion of Control (IoC)
Definition: IoC is a design principle where the control of object creation and lifecycle management is transferred from the application code to a container/framework. Instead of objects creating or looking up their dependencies, the framework “injects” them.
Traditional (tight coupling):
public class OrderService {
private PaymentGateway gateway = new RazorpayGateway(); // hard-coded
}
IoC-managed:
public class OrderService {
private final PaymentGateway gateway;
public OrderService(PaymentGateway gateway) { // injected by container
this.gateway = gateway;
}
}
Why it matters architecturally:
- Decouples interface from implementation → swap implementations without touching consumer code
- Enables testability (mock injection)
- Centralizes object graph construction → consistent lifecycle management, AOP weaving, proxying
- Foundation for Hollywood Principle: “Don’t call us, we’ll call you”
IoC implementations in Spring: Dependency Injection (most common), Factory pattern, Template method, Service Locator (anti-pattern, avoided in modern Spring).
Dependency Injection (DI)
DI is the mechanism by which IoC is realized in Spring. The container resolves and supplies dependencies.
Types of Injection
| Type | Mechanism | When to use |
|---|---|---|
| Constructor | Dependencies passed via constructor | Default/recommended — enforces immutability, mandatory deps, fails fast at startup |
| Setter | @Autowired on setter method |
Optional dependencies, reconfigurable beans |
| Field | @Autowired directly on field |
Avoid in production code — hides dependencies, breaks immutability, hard to unit test without reflection |
@Service
public class OrderService {
private final PaymentGateway gateway;
private final InventoryClient inventory;
// Constructor injection — no @Autowired needed since Spring 4.3+ with single constructor
public OrderService(PaymentGateway gateway, InventoryClient inventory) {
this.gateway = gateway;
this.inventory = inventory;
}
}
Resolving Ambiguity
@Autowired
@Qualifier("razorpayGateway")
private PaymentGateway gateway;
Or use @Primary on the preferred bean when multiple implementations of an interface exist.
Circular Dependency
Constructor injection fails fast (BeanCurrentlyInCreationException) — this is intentional, it surfaces a design smell. Setter/field injection can mask circular dependencies by allowing partial bean construction, but it’s a red flag for refactoring (often via introducing an interface, @Lazy, or ApplicationEventPublisher to decouple).
Architect talking point: prefer constructor injection universally — it makes the dependency graph explicit, supports final fields, and surfaces design issues (too many constructor args = SRP violation) early.
Bean Lifecycle
The full lifecycle Spring manages per bean:
- Instantiation — constructor invoked
- Populate properties — dependencies injected (setter/field)
BeanNameAware/BeanFactoryAware/ApplicationContextAwarecallbacks (if implemented)BeanPostProcessor.postProcessBeforeInitialization()— runs for every bean (e.g., used internally for@Autowired, AOP proxy creation hooks)@PostConstructannotated methodInitializingBean.afterPropertiesSet()(interface-based alternative to@PostConstruct)- Custom
init-method(if specified via@Bean(initMethod=...)) BeanPostProcessor.postProcessAfterInitialization()— this is where AOP proxies (e.g.,@Transactional,@Async) actually get wrapped around the bean- Bean is ready and in use
@PreDestroyannotated methodDisposableBean.destroy()- Custom
destroy-method
@Component
public class CacheManager implements InitializingBean, DisposableBean {
@PostConstruct
public void warmCache() { /* preferred modern approach */ }
@Override
public void afterPropertiesSet() { /* legacy interface approach */ }
@PreDestroy
public void flushCache() { }
@Override
public void destroy() { }
}
Architect-level nuance: BeanPostProcessor is the extension point that powers most “magic” in Spring — @Async, @Transactional, @Cacheable are all implemented as proxies woven in at postProcessAfterInitialization. Understanding this is key to debugging “why isn’t my @Transactional working” (self-invocation, proxy not applied) issues — a very common interview trap question.
Bean Scopes
| Scope | Lifetime | Use case |
|---|---|---|
singleton (default) |
One instance per Spring container | Stateless services, repositories |
prototype |
New instance every time requested | Stateful, non-thread-safe objects |
request |
One instance per HTTP request | Web-tier request-scoped data (needs web context) |
session |
One instance per HTTP session | User session state |
application |
One instance per ServletContext |
Effectively singleton at servlet-context level |
websocket |
One instance per WebSocket session | Stateful WS handlers |
@Bean
@Scope("prototype")
public ReportBuilder reportBuilder() { return new ReportBuilder(); }
Critical pitfall (classic interview question): Injecting a prototype-scoped bean into a singleton-scoped bean via constructor/field injection only resolves it once, at singleton creation time — the “prototype” then behaves like a singleton inside that context. Fix with a scoped proxy:
@Bean
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public ReportBuilder reportBuilder() { return new ReportBuilder(); }
The proxy ensures a new prototype instance is fetched on every method call into it.
@Component, @Bean, @Configuration
| Annotation | Applied to | Mechanism |
|---|---|---|
@Component (and specializations @Service, @Repository, @Controller) |
Class | Declarative — class-level marker, discovered via classpath scanning (@ComponentScan). Spring instantiates it directly. |
@Bean |
Method (inside a @Configuration class, or any @Component) |
Imperative — you control object creation explicitly inside the method body. Used for third-party classes you can’t annotate, or conditional/complex construction logic. |
@Configuration |
Class | Marks a class as a source of @Bean definitions. Spring CGLIB-proxies the configuration class so that inter-bean method calls go through the container (ensuring singleton semantics are honored). |
@Configuration
public class AppConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return mapper;
}
@Bean
public RestTemplate restTemplate(ObjectMapper objectMapper) { // direct param injection
return new RestTemplateBuilder().build();
}
}
Architect-level nuance — @Configuration(proxyBeanMethods = true) (default):
@Bean
public ServiceA serviceA() { return new ServiceA(serviceB()); }
@Bean
public ServiceB serviceB() { return new ServiceB(); }
Calling serviceB() inside serviceA() goes through the CGLIB proxy and returns the same singleton instance managed by the container — not a fresh new ServiceB(). This is “full” mode. Setting proxyBeanMethods = false (“lite” mode) skips CGLIB enhancement for faster startup (relevant in Spring Native/GraalVM contexts) but loses this inter-bean-reference guarantee — direct method calls then bypass the container.
Rule of thumb for architecture reviews: use @Component for your own application classes; use @Bean inside @Configuration for third-party library objects, conditional wiring (@ConditionalOnProperty), or anything requiring builder-style construction logic.
ApplicationContext vs BeanFactory
| Aspect | BeanFactory |
ApplicationContext |
|---|---|---|
| Loading | Lazy — beans created on first getBean() call |
Eager (default) — singletons instantiated at startup |
| Internationalization | Not supported | MessageSource support built-in |
| Event publishing | Not supported | ApplicationEventPublisher — supports ApplicationEvent/@EventListener |
| AOP | Manual setup required | Auto-detects BeanPostProcessor/BeanFactoryPostProcessor |
| Environment abstraction | No | Environment (profiles, property sources) |
Annotation support (@Autowired, @Component scanning) |
No, needs manual registration | Yes, full out-of-the-box |
| Typical usage | Rare; used internally / extremely memory-constrained environments | Standard in virtually all real applications (AnnotationConfigApplicationContext, ClassPathXmlApplicationContext, or implicitly via Spring Boot’s ApplicationContext) |
Relationship: ApplicationContext extends BeanFactory (specifically ListableBeanFactory and HierarchicalBeanFactory), adding enterprise-level features on top. In practice, you will essentially never instantiate a raw BeanFactory directly in application code — Spring Boot’s auto-configuration always gives you a fully-featured ApplicationContext (e.g., AnnotationConfigServletWebServerApplicationContext).
Interview trap: “Why does Spring eagerly instantiate singletons in ApplicationContext but not BeanFactory?” — Answer: fail-fast principle. Eager instantiation surfaces configuration errors (missing beans, circular dependencies) at startup rather than at first use in production, which is a deliberate reliability trade-off favoring early detection over lazy resource usage.