Spring Core & Dependency Injection

Difficulty

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

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

Inversion of Control flips this: the object declares what it needs, and 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 (most commonly accessed via ApplicationContext) is the component that actually 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, resolving 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 (StripeGateway), only the PaymentGateway abstraction — the container decides which implementation to plug in, which is what makes it trivial 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, but 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 substantial set of enterprise-oriented features on top:

  • Eager singleton initialization by default — singleton beans are created at container startup, not on first use, surfacing configuration errors immediately rather than 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: virtually every real Spring (and Spring Boot) application interacts with an ApplicationContext (specifically, Spring Boot uses an auto-configured AnnotationConfigServletWebServerApplicationContext or similar) — BeanFactory is mostly relevant as the conceptual base interface and for very 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, unlike field injection where 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, rather than 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)), with 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, which is part of why constructor injection has become the idiomatic default in modern Spring code.

All four are stereotype annotations, and @Service, @Repository, and @Controller are each themselves annotated with @Component — meaning 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, which automatically translates low-level, technology-specific persistence exceptions (e.g., a JPA PersistenceException, a JDBC SQLException) into Spring's unified, unchecked DataAccessException hierarchy — letting service-layer code catch a 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) is placed directly on a class, and 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 is placed 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 requires 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 (e.g., 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.