Dependency Injection Basics
Learn what Dependency Injection is and see a simple, complete example of one class being injected into another by Spring.
Introduction
You have seen Dependency Injection (DI) sprinkled throughout earlier lessons — now let's slow down and focus on it directly. This lesson defines DI precisely, walks through a complete working example, and introduces the different ways Spring can perform injection, setting up Lesson 9's deeper comparison of constructor and setter injection.
What is Dependency Injection?
Dependency Injection is a pattern where an object receives ("is injected with") the other objects it depends on from an external source, rather than creating those objects itself.
A "dependency" is simply any object that another object needs in order to do its job. If class A uses class B, then B is a dependency of A. Without DI, A would create its own instance of B. With DI, something else — in our case, the Spring container — creates B and hands it to A.
A Simple DI Example
Let's build a small example: a NotificationService that depends on an EmailSender to actually deliver messages.
package com.programinds.demo;
import org.springframework.stereotype.Component;
@Componentpublic class EmailSender {
public void send(String message) { System.out.println("Sending email: " + message); }}package com.programinds.demo;
import org.springframework.stereotype.Service;
@Servicepublic class NotificationService {
private final EmailSender emailSender;
// Spring injects an EmailSender instance here automatically public NotificationService(EmailSender emailSender) { this.emailSender = emailSender; }
public void notifyUser(String userMessage) { emailSender.send(userMessage); }}package com.programinds.demo;
import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;
@Configuration@ComponentScan(basePackages = "com.programinds.demo")public class AppConfig {}package com.programinds.demo;
import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainApp { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
NotificationService service = context.getBean(NotificationService.class); service.notifyUser("Your order has shipped!"); }}Click Run to see what this code prints.
How Spring Resolves the Dependency
When Spring builds NotificationService, it inspects its constructor and sees it requires an EmailSender. It looks in its registry of bean definitions for a bean of type EmailSender, finds the one created from our @Component class, creates it (if not already created), and passes it into the NotificationService constructor. All of this happens automatically — we never called "new EmailSender()" anywhere.
Types of Dependency Injection
| Type | How It Works | When Used |
|---|---|---|
| Constructor Injection | Dependencies are passed as constructor arguments | Recommended default for required dependencies |
| Setter Injection | Dependencies are set via a public setter method | Useful for optional dependencies |
| Field Injection | Dependencies are injected directly into fields via @Autowired | Convenient but discouraged for testability reasons |
Field Injection (and Why to Avoid It)
You will frequently see field injection in older tutorials because it looks compact.
@Servicepublic class NotificationService {
@Autowired private EmailSender emailSender;
public void notifyUser(String userMessage) { emailSender.send(userMessage); }}Field injection hides dependencies (you cannot tell what a class needs without reading every field), makes the class impossible to instantiate without Spring or reflection in tests, and allows the object to exist in a half-initialized state before injection completes. Lesson 9 covers constructor injection, the preferred alternative, in depth.
Common Mistakes
- Defaulting to field injection with @Autowired out of habit instead of using constructor injection.
- Forgetting that the dependency class must also be a Spring bean (annotated with @Component or similar) for injection to work.
- Creating circular dependencies, where two beans each require the other in their constructors, which Spring cannot resolve by default.
Best Practices
- Prefer constructor injection for all required dependencies — it makes dependencies explicit and enables easy unit testing.
- Mark injected fields as "final" when using constructor injection so the compiler enforces they are always set.
- Keep the number of dependencies a single class needs small; too many constructor parameters is a sign the class is doing too much.
Frequently Asked Questions
No, not if the class has only one constructor — Spring 4.3+ automatically uses it for injection without needing @Autowired.
It throws a NoSuchBeanDefinitionException at startup, telling you exactly which dependency it could not satisfy.
Yes, a constructor can accept as many dependencies as needed, and Spring will inject each one from the container.
Key Takeaways
- Dependency Injection means a class receives its dependencies from an external source instead of creating them.
- Spring inspects constructors (or setters/fields) to determine what a bean needs and supplies matching beans automatically.
- Constructor injection is generally preferred; field injection is common but discouraged.
- Every dependency involved must itself be a Spring-managed bean.
Summary
Dependency Injection is what makes Inversion of Control practical: Spring automatically supplies each bean with the collaborators it declares, using the constructor in our NotificationService example. Next, we will compare constructor and setter injection directly and see exactly why constructor injection has become the default recommendation.