25:00
Focus
Lesson 6
~16 min125 XP

Spring Framework and Dependency Injection

Introduction

The Spring Framework revolutionized how Java developers build enterprise applications by solving one of software engineering's most fundamental challenges: how to manage complex webs of object dependencies without creating brittle, tightly coupled code. In this lesson, you'll discover how Dependency Injection (DI) works at a deep level, how Spring's Inversion of Control (IoC) container orchestrates your application's object lifecycle, and how to leverage these concepts to write clean, testable, and maintainable Java applications. By the end, you'll understand not just the "how" but the "why" behind Spring's design philosophy.

The Problem: Tight Coupling and Why It Hurts

Before diving into Spring, let's understand the problem it solves. Consider a typical Java application where classes create their own dependencies:

public class OrderService {
    private final InventoryRepository repository = new MySqlInventoryRepository();
    private final EmailNotifier notifier = new SmtpEmailNotifier();

    public void placeOrder(Order order) {
        repository.reserve(order.getItems());
        notifier.sendConfirmation(order.getCustomerEmail());
    }
}

This code works, but it has serious problems. OrderService is tightly coupled to MySqlInventoryRepository and SmtpEmailNotifier. If you want to switch to a PostgreSQL database or use a different email provider, you must modify OrderService itself. Want to write a unit test with a fake repository? You can't — the concrete class is hardcoded.

This violates the Dependency Inversion Principle (the "D" in SOLID): high-level modules should not depend on low-level modules; both should depend on abstractions. When class A directly instantiates class B, A "knows too much" about B. It knows B's concrete type, its constructor arguments, and its lifecycle. This knowledge creates rigid, fragile code.

The core insight behind Inversion of Control (IoC) is simple but powerful: instead of objects creating their own dependencies, an external mechanism provides those dependencies. The "control" over object creation is "inverted" — moved from the object itself to an outside orchestrator. Think of it like a restaurant: instead of cooking your own meal (creating your own dependencies), you tell the waiter what you want (declare your dependencies), and the kitchen (IoC container) prepares and delivers it to you.

Exercise 1Multiple Choice
Why is it problematic for a class to directly instantiate its own dependencies using the new keyword?

Dependency Injection: The Three Flavors

Dependency Injection is a specific implementation of IoC where an object's dependencies are "injected" from the outside rather than created internally. In Spring, there are three primary ways to inject dependencies:

1. Constructor Injection (Recommended)

Dependencies are provided through the class constructor. This is Spring's recommended approach because it enforces that the object is always in a valid state — you can't create an OrderService without its required dependencies.

@Service
public class OrderService {
    private final InventoryRepository repository;
    private final EmailNotifier notifier;

    @Autowired  // Optional in Spring 4.3+ if only one constructor exists
    public OrderService(InventoryRepository repository, EmailNotifier notifier) {
        this.repository = repository;
        this.notifier = notifier;
    }
}

Notice the fields are final — they can't be changed after construction, which gives you immutability. This is a huge win for thread safety and reasoning about your code.

2. Setter Injection

Dependencies are provided through setter methods. This is useful for optional dependencies that have reasonable defaults.

@Service
public class ReportService {
    private ReportFormatter formatter;

    @Autowired
    public void setFormatter(ReportFormatter formatter) {
        this.formatter = formatter;
    }
}

3. Field Injection

Dependencies are injected directly into the field using @Autowired. While concise, this is generally discouraged because it hides dependencies, makes testing harder (you need reflection or Spring's test context), and prevents the use of final fields.

@Service
public class OrderService {
    @Autowired
    private InventoryRepository repository;  // Not recommended
}

Important: Constructor injection should be your default choice. It makes dependencies explicit, supports immutability, and ensures your objects are never in a partially initialized state. Field injection is convenient but is considered an anti-pattern in modern Spring development.

Exercise 2Multiple Choice
Which form of dependency injection does Spring recommend as the default, and why?

The Spring IoC Container: ApplicationContext Deep Dive

The heart of the Spring Framework is its IoC container, most commonly represented by the ApplicationContext interface. Think of it as a sophisticated factory that knows how to create, configure, wire together, and manage the lifecycle of all the objects (called beans) in your application.

When your Spring application starts, the container performs several steps:

  1. Configuration Loading — The container reads your configuration, which can come from XML files (legacy), Java @Configuration classes, or component scanning of annotated classes.
  2. Bean Definition Registration — It creates an internal registry of bean definitions — blueprints that describe how to create each bean, including its class, scope, dependencies, and initialization callbacks.
  3. Dependency Resolution — The container builds a dependency graph and determines the order in which beans must be created. If OrderService depends on InventoryRepository, the repository is created first.
  4. Bean Instantiation and Wiring — Beans are created and their dependencies are injected.
  5. Lifecycle Callbacks — Methods annotated with @PostConstruct are called, InitializingBean.afterPropertiesSet() is invoked, and other initialization hooks fire.

Bean Scopes

By default, Spring beans are singletons — the container creates exactly one instance and shares it across the entire application. This is different from the GoF Singleton pattern; Spring simply manages a single instance per container.

Other scopes include:

  • @Scope("prototype") — A new instance is created every time the bean is requested
  • @Scope("request") — One instance per HTTP request (web applications)
  • @Scope("session") — One instance per HTTP session

Common Pitfall: Injecting a prototype-scoped bean into a singleton-scoped bean doesn't work as you might expect. The prototype is injected once at singleton creation time and the same instance is reused forever. To get a new prototype each time, use ObjectProvider<T> or @Lookup method injection.

Exercise 3True or False
By default, Spring beans are prototype-scoped, meaning a new instance is created each time the bean is requested.

Annotations and Component Scanning

Modern Spring applications rely heavily on annotations and component scanning to eliminate boilerplate configuration. When you annotate a class with one of Spring's stereotype annotations, the container automatically detects and registers it as a bean.

Stereotype Annotations

  • @Component — The generic annotation marking a class as a Spring-managed bean
  • @Service — Specialization of @Component for service-layer classes (business logic)
  • @Repository — Specialization for data access layer; also enables automatic exception translation from database-specific exceptions to Spring's DataAccessException hierarchy
  • @Controller / @RestController — Specialization for web layer classes

Functionally, @Service, @Repository, and @Controller all behave the same as @Component at the IoC level — they mark the class for component scanning. However, using the correct stereotype communicates intent and enables layer-specific features (like @Repository's exception translation).

How Component Scanning Works

When you annotate a configuration class with @ComponentScan (or use @SpringBootApplication, which includes it), Spring scans the specified base package and all its sub-packages for classes with stereotype annotations:

@SpringBootApplication  // Includes @ComponentScan for this package and below
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

@Bean vs. @Component

Sometimes you need to create beans from third-party classes that you can't annotate. That's where @Bean methods in @Configuration classes come in:

@Configuration
public class AppConfig {

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate template = new RestTemplate();
        template.setConnectTimeout(Duration.ofSeconds(5));
        return template;
    }

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}

Use @Component (and its specializations) for your own classes. Use @Bean for third-party classes or when you need programmatic control over bean creation.

Resolving Ambiguity with @Qualifier

When multiple beans implement the same interface, Spring doesn't know which one to inject. You resolve this with @Qualifier:

@Service
public class NotificationService {
    private final MessageSender sender;

    public NotificationService(@Qualifier("smsSender") MessageSender sender) {
        this.sender = sender;
    }
}

Alternatively, you can use @Primary on one implementation to make it the default choice when no qualifier is specified.

Exercise 4Fill in the Blank
When you need to register a third-party class as a Spring bean (a class you cannot annotate directly), you use a method annotated with @___ inside a @Configuration class.

Real-World Pattern: Building a Testable Architecture

Let's bring everything together with a realistic example that demonstrates why DI matters so much in practice. Consider building an order processing system:

This example illustrates the ultimate payoff of DI: your business logic is completely decoupled from infrastructure. The OrderService doesn't know or care whether orders are stored in PostgreSQL, MongoDB, or an in-memory list. It doesn't know if payments go through Stripe or PayPal. It doesn't know if events are published to Kafka or a simple in-memory queue. This makes your code:

  • Testable — Mock any dependency in isolation
  • Flexible — Swap implementations without changing business logic
  • Maintainable — Changes in infrastructure don't ripple through your domain code
  • Team-friendly — One developer can build OrderService while another implements StripePaymentProcessor

Spring Boot: Convention Over Configuration

While the Spring Framework provides the foundational IoC container, Spring Boot dramatically simplifies the developer experience through auto-configuration. Instead of manually configuring hundreds of beans, Spring Boot examines your classpath and automatically configures sensible defaults.

When you add spring-boot-starter-data-jpa to your dependencies, Spring Boot automatically:

  • Creates a DataSource bean based on your application.properties settings
  • Configures a JpaTransactionManager
  • Sets up an EntityManagerFactory
  • Enables JPA repository scanning

You can always override any auto-configured bean by defining your own. Spring Boot follows the principle that explicit configuration wins over auto-configuration.

The @SpringBootApplication Annotation

This single annotation is a composite of three important annotations:

@SpringBootConfiguration   // Marks this as a configuration source
@EnableAutoConfiguration    // Enables Spring Boot's auto-configuration
@ComponentScan              // Scans for components starting from this package
public class MyApplication { }

Externalized Configuration

Spring Boot's Environment abstraction allows you to externalize configuration across multiple sources with a well-defined precedence order:

  1. Command-line arguments (highest priority)
  2. Environment variables
  3. application-{profile}.properties
  4. application.properties (lowest among external sources)

You inject configuration values using @Value or, preferably, type-safe @ConfigurationProperties:

@ConfigurationProperties(prefix = "app.payment")
public record PaymentProperties(
    String apiKey,
    Duration timeout,
    int maxRetries
) {}
# application.properties
app.payment.api-key=sk_live_abc123
app.payment.timeout=5s
app.payment.max-retries=3

This approach gives you type safety, validation, and IDE auto-completion — a significant upgrade over scattered @Value annotations.

Key Takeaways

  • Dependency Injection decouples object creation from object usage, making code testable, flexible, and maintainable by programming to interfaces rather than concrete implementations.
  • Constructor injection is the preferred DI mechanism in Spring — it enforces required dependencies, supports final fields for immutability, and prevents partially initialized objects.
  • The Spring IoC container (ApplicationContext) manages the complete lifecycle of beans: instantiation, dependency wiring, initialization, and destruction — using bean definitions derived from annotations, @Configuration classes, or XML.
  • Stereotype annotations (@Component, @Service, @Repository, @Controller) combined with component scanning eliminate manual bean registration, while @Bean methods handle third-party classes and complex construction logic.
  • Spring Boot builds on the core framework with auto-configuration, externalized properties, and starter dependencies — reducing boilerplate so you can focus on business logic rather than infrastructure plumbing.
Check Your Understanding

In the Spring Framework, the IoC container can manage beans with different scopes, and developers must choose between approaches like constructor injection and setter injection when wiring dependencies. Consider a scenario where you are building an e-commerce application with an `OrderService` that depends on a `PaymentGateway` interface and a `ShippingCalculator` interface. Explain why constructor injection is generally preferred over setter injection for these dependencies, describe how the Inversion of Control principle changes the relationship between `OrderService` and its dependencies compared to having `OrderService` instantiate them directly, and discuss how this design makes it easier to write unit tests for `OrderService`. In your explanation, be sure to address what "tight coupling" means in this context and how depending on abstractions rather than concrete implementations supports the Dependency Inversion Principle.

🔒Upgrade to submit written responses and get AI feedback
Go deeper
  • How does Spring handle circular dependencies between beans?🔒
  • What's the difference between @Autowired and constructor injection?🔒
  • When should I use @Component versus @Bean annotation?🔒
  • How does Spring's IoC container manage bean scopes?🔒
  • Can dependency injection hurt performance in large applications?🔒