Spring IoC and Dependency Injection
Every Spring Boot application is built on one idea: the framework creates and wires your objects, not you. This article explains how that works and how to use it effectively.
Inversion of Control
In traditional Java, you create objects yourself:
// You control the dependencies
public class OrderService {
private final OrderRepository repository = new JpaOrderRepository();
private final EmailService email = new SmtpEmailService("smtp.gmail.com");
}
Problems:
OrderServiceis tightly coupled to specific implementations- You can’t swap
JpaOrderRepositoryfor a mock in tests without editingOrderService - If
SmtpEmailServiceneeds its own dependencies, you must construct those too
Inversion of Control (IoC) flips this: instead of creating your dependencies, you declare what you need and let a container provide them.
// Container provides the dependencies
public class OrderService {
private final OrderRepository repository;
private final EmailService email;
public OrderService(OrderRepository repository, EmailService email) {
this.repository = repository;
this.email = email;
}
}
The Spring IoC container (ApplicationContext) creates OrderService, finds a OrderRepository implementation and an EmailService implementation, and passes them in.
The ApplicationContext
The ApplicationContext is the Spring IoC container. It:
- Creates beans (objects it manages)
- Resolves and injects dependencies
- Manages bean lifecycles (init, destroy)
- Provides access to configuration
SpringApplication.run() creates the ApplicationContext:
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(OrderServiceApplication.class, args);
// You can retrieve beans directly (rarely needed)
OrderService service = ctx.getBean(OrderService.class);
}
}
In practice you never call ctx.getBean() in application code — Spring injects everything automatically.
Declaring Beans
Stereotype Annotations (most common)
Mark your class with a stereotype and Spring will register it as a bean:
@Component // generic bean
@Service // business logic layer (semantically meaningful, same behavior)
@Repository // data access layer (adds exception translation)
@Controller // web layer (Spring MVC)
@RestController // web layer (= @Controller + @ResponseBody)
@Service
public class OrderService {
// Spring creates one instance of this and manages it
}
@Repository
public class JpaOrderRepository implements OrderRepository {
// Spring creates one instance of this
}
@ComponentScan (included in @SpringBootApplication) finds all these classes in your package and sub-packages.
@Bean Methods (for third-party classes)
When you don’t own the class (it’s from a library) or need fine-grained construction control, declare a bean method in a @Configuration class:
@Configuration
public class AppConfig {
@Bean
public RestClient orderApiClient() {
return RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Accept", "application/json")
.build();
}
@Bean
public ObjectMapper objectMapper() {
return JsonMapper.builder()
.findAndAddModules()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.build();
}
}
The method name is the bean name. The return type is the bean type.
Three Ways to Inject Dependencies
1. Constructor Injection (recommended)
@Service
public class OrderService {
private final OrderRepository repository;
private final EmailService emailService;
private final OrderProperties props;
// Spring 4.3+: @Autowired is optional on single-constructor classes
public OrderService(OrderRepository repository,
EmailService emailService,
OrderProperties props) {
this.repository = repository;
this.emailService = emailService;
this.props = props;
}
}
Why constructor injection is best:
- Dependencies are
final— immutable after construction - Class is testable without Spring (just pass mocks to the constructor)
- Missing dependencies cause a startup failure — not a
NullPointerExceptionat runtime - Makes circular dependencies visible at compile time
With Lombok, the boilerplate disappears:
@Service
@RequiredArgsConstructor // generates constructor for all final fields
public class OrderService {
private final OrderRepository repository;
private final EmailService emailService;
private final OrderProperties props;
}
2. Field Injection (avoid)
@Service
public class OrderService {
@Autowired
private OrderRepository repository; // NOT recommended
@Autowired
private EmailService emailService;
}
Problems:
- Can’t be
final— the field is set after construction via reflection - Can’t test without a Spring context (or messy reflection tricks)
- Hides dependencies — you can’t see what the class needs from its constructor
- Masks circular dependencies until runtime
3. Setter Injection (for optional dependencies)
@Service
public class OrderService {
private final OrderRepository repository;
private MetricsCollector metrics; // optional
public OrderService(OrderRepository repository) {
this.repository = repository;
}
@Autowired(required = false)
public void setMetrics(MetricsCollector metrics) {
this.metrics = metrics;
}
}
Use setter injection only for optional dependencies (where the bean may not exist). Required dependencies should always use constructor injection.
Resolving Ambiguity: Multiple Implementations
When Spring finds multiple beans of the same type, it needs help choosing.
@Primary
Mark one implementation as the default:
@Service
@Primary
public class JpaOrderRepository implements OrderRepository { ... }
@Service
public class InMemoryOrderRepository implements OrderRepository { ... }
OrderService gets JpaOrderRepository unless told otherwise.
@Qualifier
Be explicit about which bean you want:
@Service
public class OrderService {
private final OrderRepository repository;
public OrderService(@Qualifier("jpaOrderRepository") OrderRepository repository) {
this.repository = repository;
}
}
The qualifier value defaults to the bean name (class name with lowercase first letter).
Inject All Implementations
Sometimes you want all of them:
@Service
public class NotificationService {
private final List<NotificationChannel> channels;
public NotificationService(List<NotificationChannel> channels) {
this.channels = channels;
}
public void notifyAll(Order order) {
channels.forEach(ch -> ch.send(order));
}
}
Spring injects a list of all NotificationChannel beans — email, SMS, push, whatever you register.
@Autowired vs No Annotation
In modern Spring Boot you rarely write @Autowired. Spring automatically injects:
- Constructor parameters (always, on any
@Componentbean) @Beanmethod parameters in@Configurationclasses@Valuefields (still needs the annotation)
@Configuration
public class AppConfig {
@Bean
public OrderService orderService(OrderRepository repository, EmailService email) {
// repository and email are injected automatically — no @Autowired needed
return new OrderService(repository, email);
}
}
The Complete Wiring Picture
Here’s how the order-service component graph looks:
ApplicationContext
│
├── OrderController (@RestController)
│ └── needs: OrderService
│
├── OrderService (@Service)
│ ├── needs: OrderRepository
│ ├── needs: EmailService
│ └── needs: OrderProperties
│
├── JpaOrderRepository (@Repository)
│ └── needs: EntityManager (auto-configured)
│
├── SesEmailService (@Service, @Profile("prod"))
│ └── needs: SesClient (declared as @Bean)
│
└── OrderProperties (@ConfigurationProperties)
└── bound from application.yml
Spring resolves this graph at startup. If anything is missing or ambiguous, it fails immediately with a clear error.
@Value: Injecting Individual Properties
For a single property value, @Value is simpler than a full @ConfigurationProperties:
@Service
public class CurrencyService {
@Value("${app.order.currency:USD}") // default is USD if property missing
private String defaultCurrency;
@Value("${server.port}")
private int port;
@Value("#{systemProperties['java.version']}") // SpEL expression
private String javaVersion;
}
Use @Value sparingly. For groups of related properties, @ConfigurationProperties is better (typed, validated, IDE support).
Practical: Wiring the order-service
Let’s look at the complete service layer with proper DI:
// OrderRepository interface
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(UUID id);
List<Order> findByCustomerId(UUID customerId);
}
// JPA implementation
@Repository
public class JpaOrderRepository implements OrderRepository {
private final EntityManager em;
public JpaOrderRepository(EntityManager em) {
this.em = em;
}
@Override
public Order save(Order order) {
return em.merge(order);
}
@Override
public Optional<Order> findById(UUID id) {
return Optional.ofNullable(em.find(Order.class, id));
}
@Override
public List<Order> findByCustomerId(UUID customerId) {
return em.createQuery(
"SELECT o FROM Order o WHERE o.customerId = :cid", Order.class)
.setParameter("cid", customerId)
.getResultList();
}
}
// Service
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository repository;
private final EmailService emailService;
private final OrderProperties props;
public Order createOrder(CreateOrderRequest request) {
if (request.items().size() > props.getMaxItemsPerOrder()) {
throw new OrderTooLargeException("Too many items");
}
Order order = Order.from(request);
Order saved = repository.save(order);
emailService.send(confirmationEmail(saved));
return saved;
}
}
// Controller
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<Order> create(@RequestBody CreateOrderRequest request) {
Order order = orderService.createOrder(request);
return ResponseEntity.created(
URI.create("/api/orders/" + order.id())
).body(order);
}
}
Each layer depends on the abstraction below it, not the implementation. Spring wires the concrete classes at startup based on what’s available on the classpath and which profile is active.
Testing with DI
Constructor injection makes unit testing trivial — no Spring context needed:
class OrderServiceTest {
OrderRepository repository = mock(OrderRepository.class);
EmailService emailService = mock(EmailService.class);
OrderProperties props = new OrderProperties();
OrderService service = new OrderService(repository, emailService, props);
@Test
void shouldRejectOversizedOrders() {
props.setMaxItemsPerOrder(2);
var request = CreateOrderRequest.with(3, items());
assertThrows(OrderTooLargeException.class,
() -> service.createOrder(request));
verifyNoInteractions(repository, emailService);
}
}
No @SpringBootTest, no application context startup, no database. Fast and focused.
What You’ve Learned
- IoC means the container creates and wires your objects — you declare what you need
- The
ApplicationContextis the Spring IoC container @Component,@Service,@Repository,@Controllerregister beans for component scanning@Beanmethods in@Configurationclasses register beans for third-party types- Constructor injection is the right choice for required dependencies — use it by default
@Primaryand@Qualifierresolve ambiguity when multiple beans match a type- Constructor injection makes classes fully testable without Spring
Next: Article 7 — Bean Scopes and Lifecycle — controlling when beans are created, how many there are, and what happens when they’re destroyed.