Spring Core & Dependency Injection

Difficulty

In traditional procedural code, an object creates its own collaborators directly:

class OrderService {
    private PaymentGateway gateway = new StripeGateway(); // OrderService controls its own dependency
}

Inversion of Control flips this around. The object just declares what it needs. Something external supplies it. Control over creating and wiring dependencies moves out of the object and into a container:

class OrderService {
    private final PaymentGateway gateway;
    OrderService(PaymentGateway gateway) { this.gateway = gateway; } // supplied from outside
}

Spring's IoC container (usually accessed via ApplicationContext) is what performs this inversion at runtime:

  1. It scans for bean definitions — via component scanning (@Component, @Service, ...), @Bean methods in @Configuration classes, or (historically) XML.
  2. It instantiates the beans. It resolves each one's declared dependencies (constructor parameters, @Autowired fields/setters) by looking them up among other managed beans.
  3. It wires everything together and manages each bean's full lifecycle (initialization callbacks, scope, destruction).
@Service
class OrderService {
    private final PaymentGateway gateway;
    @Autowired // Spring resolves and injects this automatically
    OrderService(PaymentGateway gateway) { this.gateway = gateway; }
}

The practical benefit: OrderService never mentions a concrete class like StripeGateway, only the PaymentGateway abstraction. The container decides which implementation to plug in. That's what makes it easy to swap implementations — for tests, different environments, or different providers — without touching OrderService itself.

Related Resources

BeanFactory is the most basic Spring IoC container interface. It can look up beans, resolve their dependencies, and manage bean scopes — that's essentially it. Its most distinguishing default behavior: it lazily instantiates singleton beans only when they're first requested via getBean().

ApplicationContext extends BeanFactory and adds a set of enterprise-oriented features on top:

  • Eager singleton initialization by default — singleton beans are created at container startup, not on first use. This surfaces configuration errors immediately instead of at some arbitrary later point.
  • Event publicationApplicationEventPublisher/@EventListener support for in-process, decoupled communication between beans.
  • MessageSource — internationalization/localized message resolution.
  • Environment abstraction — unified access to properties from multiple sources (application.properties, environment variables, JVM system properties, profiles).
  • Easier AOP integration and automatic BeanPostProcessor/BeanFactoryPostProcessor registration.
ApplicationContext ctx = SpringApplication.run(MyApp.class, args);
MyService service = ctx.getBean(MyService.class);

In practice: almost every real Spring (and Spring Boot) application interacts with an ApplicationContext — specifically, Spring Boot uses an auto-configured AnnotationConfigServletWebServerApplicationContext or similar. BeanFactory mostly matters as the conceptual base interface, and in memory-constrained scenarios where its lighter weight and lazy-by-default behavior matter.

Spring supports three injection styles:

1. Constructor injection (recommended default):

@Service
class OrderService {
    private final PaymentGateway gateway;
    private final OrderRepository repository;

    OrderService(PaymentGateway gateway, OrderRepository repository) { // @Autowired optional
        this.gateway = gateway;
        this.repository = repository;
    }
}

2. Setter injection:

@Service
class OrderService {
    private PaymentGateway gateway;
    @Autowired
    void setGateway(PaymentGateway gateway) { this.gateway = gateway; }
}

3. Field injection:

@Service
class OrderService {
    @Autowired
    private PaymentGateway gateway; // injected via reflection, bypassing the constructor entirely
}

Why constructor injection wins:

  • Immutability: dependencies can be declared final, guaranteeing they're set once and never reassigned.
  • Impossible to construct in an invalid state: you cannot create an OrderService without its required dependencies. The compiler enforces it. Field injection is different — a plain new OrderService() compiles fine but produces an object with null dependencies at runtime.
  • Fail-fast at startup: a missing or misconfigured dependency causes a clear ApplicationContext startup failure, instead of a NullPointerException deep in some unrelated code path later.
  • Trivial to unit test: you can construct the object directly with mocks in a plain JUnit test (new OrderService(mockGateway, mockRepo)). No Spring container and no reflection-based injection needed. Field injection requires either a running context or reflection tricks (ReflectionTestUtils) to set the field for a test.
  • Surfaces excessive dependencies: a constructor with 8 parameters is an obvious, visible code smell suggesting the class does too much. Field injection hides that same problem.

Since Spring 4.3, @Autowired is even optional on a constructor if the class has exactly one constructor — Spring infers it automatically. That's part of why constructor injection has become the idiomatic default in modern Spring code.

All four are stereotype annotations. @Service, @Repository, and @Controller are each themselves annotated with @Component — so component scanning treats all four identically for the basic purpose of "register this class as a Spring bean":

@Component // generic — "this is a Spring-managed bean"
class DataFormatter { }

@Service // business/service-layer logic
class OrderService { }

@Repository // data-access layer
class OrderRepository { }

@Controller // web layer, returns view names (or @RestController for JSON/text bodies directly)
class OrderController { }

Why bother with more specific ones, if they're functionally interchangeable for bean registration?

  1. Readability/intent — seeing @Repository on a class immediately communicates its architectural role, which plain @Component doesn't.
  2. @Repository has a genuine behavioral difference: Spring registers a PersistenceExceptionTranslationPostProcessor for beans annotated @Repository. It automatically translates low-level, technology-specific persistence exceptions (a JPA PersistenceException, a JDBC SQLException) into Spring's unified, unchecked DataAccessException hierarchy. Service-layer code can then catch one consistent, technology-agnostic exception type, regardless of which persistence technology the repository actually uses underneath.
  3. @Controller/@RestController additionally participate in Spring MVC's request-handling machinery, being detected specifically as web-layer components that can hold @RequestMapping-annotated handler methods.

Rule of thumb: always prefer the most specific stereotype available for the layer a class belongs to. It costs nothing, documents architecture more clearly, and (for @Repository) unlocks a genuinely useful behavior.

Both register a Spring-managed bean, but via different mechanisms suited to different situations:

@Component (and its stereotype specializations) goes directly on a class, and gets picked up automatically by component scanning:

@Component
class EmailNotifier implements Notifier {
    // Spring instantiates this via component scanning, no explicit wiring code needed
}

This only works for classes you own and can annotate. You can't add @Component to a class from a third-party JAR.

@Bean goes on a method inside an @Configuration class. The method's return value becomes the managed bean, and you write the actual construction logic yourself:

@Configuration
class AppConfig {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.setConnectTimeout(Duration.ofSeconds(5)).build(); // full control over construction
    }
}

When @Bean is necessary (not just a style choice):

  • Wiring up a third-party class you can't put @Component on (RestTemplate, an external SDK client).
  • When bean creation needs custom logic or conditional configuration beyond what a constructor/field injection can express.
  • When you need multiple beans of the same type with different configurations — for example, two differently-configured RestTemplate beans, disambiguated by method name or an explicit bean name.

Rule of thumb: use @Component (or a stereotype) for your own application classes. Use @Bean inside @Configuration classes for anything you don't own, or that needs custom construction logic.