Spring Basics
Dependency Injection
In the world of Java enterprise applications, dependency injection (DI) is the cornerstone technique that transforms your code from tightly coupled monoliths into flexible, maintainable, and testable systems. Think of it as a design pattern where objects automatically receive their dependencies (like services, repositories, or configuration) instead of manually creating or managing them. This approach is the heart of Spring’s Inversion of Control (IoC) principle, and it’s what makes Spring the dominant framework for building scalable Java applications.
Why does this matter? Without DI, your code becomes brittle. Imagine a UserManager class that hardcodes a UserRepository instance:
<code class="language-java">public class UserManager {
<p> private final UserRepository userRepository = new UserRepository();</p>
<p> </p>
<p> // Hardcoded dependency = tight coupling!</p>
<p> public void processUser() {</p>
<p> userRepository.save(new User());</p>
<p> }</p>
<p>}</code>
This pattern violates open/closed principle and makes testing impossible (you can’t mock UserRepository without refactoring). Spring solves this by injecting dependencies at runtime—a concept we’ll explore in detail below.
What is Dependency Injection?
At its core, DI is the automatic provision of dependencies to a class. Instead of creating dependencies internally, your class declares them, and Spring’s container injects the required implementations. This shifts responsibility from your code to the framework, enabling:
- Testability: Mock dependencies during unit tests
- Loose coupling: Change implementations without modifying class code
- Simplicity: Less boilerplate than manual dependency management
- Scalability: Easily integrate new components without refactoring
How Spring Implements Dependency Injection
Spring uses three primary DI mechanisms, each with distinct use cases:
- Constructor Injection
Best for: Immutable objects, production code (no runtime changes)
- Setter Injection
Best for: Mutable objects, legacy code, or when you need runtime configuration
- Field Injection
Best for: Simple prototypes (avoid in production due to testability risks)
Let’s walk through each with concrete examples.
Constructor Injection Example
<code class="language-java">// UserService with constructor injection
<p>public class UserService {</p>
<p> private final UserRepository userRepository;</p>
<p> // Constructor defines dependencies</p>
<p> public UserService(UserRepository userRepository) {</p>
<p> this.userRepository = userRepository;</p>
<p> }</p>
<p> public void createUser(User user) {</p>
<p> userRepository.save(user);</p>
<p> }</p>
<p>}</code>
Why this works: Spring detects the UserRepository dependency in the constructor, creates an instance of UserRepository, and passes it to UserService. This ensures thread safety and immutability—critical for enterprise applications.
Setter Injection Example
<code class="language-java">// UserService with setter injection
<p>public class UserService {</p>
<p> private UserRepository userRepository;</p>
<p> // Setter method for dependency</p>
<p> public void setUserRepository(UserRepository userRepository) {</p>
<p> this.userRepository = userRepository;</p>
<p> }</p>
<p> public void createUser(User user) {</p>
<p> userRepository.save(user);</p>
<p> }</p>
<p>}</code>
When to use: Ideal for objects that need to change dependencies after construction (e.g., configuration-based services). Note: Spring requires @Autowired or @Resource annotations to recognize the setter.
Field Injection Example
<code class="language-java">// UserService with field injection (minimal code)
<p>public class UserService {</p>
<p> @Autowired</p>
<p> private UserRepository userRepository;</p>
<p> public void createUser(User user) {</p>
<p> userRepository.save(user);</p>
<p> }</p>
<p>}</code>
Caution: While concise, field injection is discouraged in production code. It reduces testability (harder to mock) and violates SOLID principles. Spring’s documentation recommends constructor injection for production systems.
Practical Spring DI Setup
To see DI in action, let’s build a minimal Spring application:
- Create a
UserRepositoryinterface - Implement it with a
JdbcUserRepository - Configure Spring to inject dependencies
<code class="language-java">// Step 1: Interface
<p>public interface UserRepository {</p>
<p> void save(User user);</p>
<p>}</p>
<p>// Step 2: Implementation</p>
<p>public class JdbcUserRepository implements UserRepository {</p>
<p> @Override</p>
<p> public void save(User user) {</p>
<p> System.out.println("Saving user via JDBC: " + user.getName());</p>
<p> }</p>
<p>}</p>
<p>// Step 3: UserService with constructor injection</p>
<p>public class UserService {</p>
<p> private final UserRepository userRepository;</p>
<p> public UserService(UserRepository userRepository) {</p>
<p> this.userRepository = userRepository;</p>
<p> }</p>
<p> public void processUser(User user) {</p>
<p> userRepository.save(user);</p>
<p> }</p>
<p>}</p>
<p>// Step 4: Spring configuration (application-context.xml)</p>
<p><?xml version="1.0" encoding="UTF-8"?></p>
<p><beans xmlns="http://www.springframework.org/schema/beans"</p>
<p> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"</p>
<p> xsi:schemaLocation="http://www.springframework.org/schema/beans</p>
<p> http://www.springframework.org/schema/beans/spring-beans.xsd"></p>
<p> </p>
<p> <!-- Define the repository implementation --></p>
<p> <bean id="userRepository" class="JdbcUserRepository"/></p>
<p> </p>
<p> <!-- Inject repository into service --></p>
<p> <bean id="userService" class="UserService"></p>
<p> <constructor-arg ref="userRepository"/></p>
<p> </bean></p>
<p></beans></code>
How it runs:
- Spring scans
application-context.xmlfordefinitions - Creates
JdbcUserRepositoryinstance - Uses constructor argument to inject
userRepositoryintoUserService - When
userService.processUser()is called, it automatically uses the injectedUserRepositoryimplementation
This setup demonstrates zero manual dependency creation—Spring handles the complexity.
Why Dependency Injection Matters in Enterprise Java
| Scenario | Without DI | With Spring DI |
|---|---|---|
| Testing | Hard to mock dependencies | Mock dependencies in tests (e.g., @MockBean) |
| Scalability | Requires code changes for new impls | Swap impls via configuration (no code changes) |
| Maintenance | Tight coupling → slow refactoring | Loose coupling → faster iterations |
| Error handling | Hard to isolate failures | Clear dependency boundaries |
In enterprise contexts, DI enables resilient systems. For example, if a UserRepository fails (e.g., database outage), Spring can fail fast without crashing the entire application—something impossible with hardcoded dependencies.
Pro Tips for Production
- Always prefer constructor injection for production code (immutability + testability)
- Use
@Autowiredsparingly—prefer constructor injection for clean interfaces - For complex hierarchies, combine DI with Aspect-Oriented Programming (AOP) for cross-cutting concerns
- Never inject dependencies directly into service layers—use repositories as intermediaries
💡 Remember: DI isn’t just a Spring feature—it’s a design philosophy. When you inject dependencies, you’re not writing code; you’re building modular systems that adapt to change.
Summary
Dependency injection is Spring’s foundation for building maintainable enterprise applications. By automatically injecting dependencies (via constructor, setter, or field), Spring eliminates hardcoded references, enabling testability, scalability, and resilience. In production, constructor injection is the gold standard—it ensures immutability and simplifies testing. Start small: inject one dependency per class, and watch your code evolve from brittle monoliths into robust, enterprise-grade systems. 🌟