Spring Core Explained

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:

  1. Instantiation — constructor invoked
  2. Populate properties — dependencies injected (setter/field)
  3. BeanNameAware / BeanFactoryAware / ApplicationContextAware callbacks (if implemented)
  4. BeanPostProcessor.postProcessBeforeInitialization() — runs for every bean (e.g., used internally for @Autowired, AOP proxy creation hooks)
  5. @PostConstruct annotated method
  6. InitializingBean.afterPropertiesSet() (interface-based alternative to @PostConstruct)
  7. Custom init-method (if specified via @Bean(initMethod=...))
  8. BeanPostProcessor.postProcessAfterInitialization() — this is where AOP proxies (e.g., @Transactional, @Async) actually get wrapped around the bean
  9. Bean is ready and in use
  10. @PreDestroy annotated method
  11. DisposableBean.destroy()
  12. 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.