Dependency Injection: Passing Dependencies In
What dependency injection is and why it makes code testable: constructor injection in plain Java, a clock fake that makes a greeting test deterministic, and what frameworks add on top.
Dependency injection means an object receives the things it depends on, such as a clock or a repository, from outside instead of creating them itself. The detail interviewers expect: constructor injection is preferred because required dependencies are visible and fields can be final, and tests can pass fakes, while frameworks like Spring only automate the wiring.
Dependency injection means an object receives the things it depends on instead of creating them. With constructor injection, the same Service produced "Good morning" for a fake clock at 9 and "Good afternoon" at 15, which a hard-wired real clock could not do in a test. Frameworks automate the wiring but the idea needs none.
- Dependency injection supplies an object's dependencies from outside rather than having the object create them or look them up from a global.
- With constructor injection, the same Service returned "Good morning" for a fake clock at hour 9 and "Good afternoon" at hour 15, making the test deterministic.
- Constructor injection makes dependencies required and visible in the signature and lets the field be final; setter injection suits optional dependencies.
- A composition root is the single place near program start, such as main or a configuration class, where the object graph is wired together.
- Dependency inversion, the D in SOLID, is the principle of depending on abstractions; dependency injection is one technique for following it.
- A constructor with eight or more parameters under dependency injection is a design smell suggesting the class has too many responsibilities.
01The idea in one sentence
A class has dependencies: other objects it needs to do its work, such as a database connection, a clock, an HTTP client. It can obtain them by creating them itself (new RealClock()), by looking them up from a global (a singleton), or by having them handed in from outside. Dependency injection is the third option.
The benefit is control. The code that creates the object decides which implementation it gets. Production code passes real ones. Tests pass fakes. The class itself works with an interface and does not know or care which.
02Without injection: hard to test
Consider a greeting that depends on the time of day. If the class reads the system clock directly, a test of "Good morning" passes only when run before noon. You would need to run tests at certain times or change the machine's time. That is what hidden dependencies cost: behaviour you cannot control from a test.
03Constructor injection
interface Clock { long now(); } // the hour of day, for this example
static class Service {
final Clock clock;
Service(Clock c) { clock = c; }
String greeting() {
return clock.now() < 12 ? "Good morning" : "Good afternoon";
}
}
new Service(() -> 9).greeting(); // Good morning
new Service(() -> 15).greeting(); // Good afternoonThe test run printed Good morning / Good afternoon. The two services differ only in the lambda handed to the constructor, and the behaviour is now fully predictable. In production you would pass an implementation that reads the real time. Constructor injection is the preferred form: the dependency is required, visible in the signature, and the field can be final, so the object is never half-configured.
Other forms: setter injection (optional dependencies, but the object can exist unconfigured) and field injection by a framework (short to write, but hides dependencies and makes plain construction in tests awkward).
04Wiring at the edge
Someone must still create the objects. In a small program, do it by hand in main or a configuration class: create the repository, pass it to the service, pass the service to the controller. This is the composition root, the single place that knows which concrete classes are used. The rest of the program only sees interfaces.
Manual wiring is perfectly good for small and medium programs and keeps everything explicit. It starts to feel tedious when there are dozens of classes with deep chains of dependencies.
05What containers add
A dependency injection container scans your classes, works out what each constructor needs, and builds the objects in the right order. It also manages lifetimes: one shared instance (singleton scope, the default in Spring, a safe use of the singleton idea), a new instance per request, or per session. You declare what to inject with annotations like @Autowired or @Inject, or with configuration.
The container is a convenience, not the concept. A class designed for constructor injection works the same with or without it, and can be tested without starting any framework. Be wary of designs that cannot be instantiated without the container.
06Testing benefits
With injection, a test supplies a controlled stand-in: a fake repository holding data in a map, a stub clock returning a fixed time, a mock that records calls so the test can verify them. Tests become fast, because they avoid real databases and networks, and deterministic, because time, randomness and failures are under the test's control. You can simulate a failing payment gateway to see how your code reacts, which is hard against the real thing.
A warning: heavy use of mocks can make tests mirror the implementation instead of the behaviour. Prefer simple hand-written fakes where they are practical.
07Related ideas and pitfalls
Dependency inversion (the D in SOLID) is the principle: depend on abstractions. Injection is one way to follow it. A service locator, where an object asks a global registry for its dependencies, hides them just as a singleton does. A constructor with eight or ten parameters is a warning that the class has too many responsibilities, and that is a design smell the explicit signature makes visible. Do not inject things that never vary or have no reason to be replaced: values such as constants and simple immutable data can just be created.
08Common interview questions
Q: What is dependency injection?
A: Supplying an object's dependencies from outside rather than having it create or look them up itself.
Q: Constructor versus setter injection?
A: Constructor injection makes required dependencies explicit and allows immutable fields. Setter injection suits optional dependencies.
Q: How does it help testing?
A: Tests can pass fakes or mocks, making behaviour deterministic and avoiding real external systems.
Q: What is a composition root?
A: The one place near program start where the object graph is wired together.
Q: Dependency injection versus dependency inversion?
A: Inversion is the principle (depend on abstractions). Injection is a technique to provide them.
Q: Do you need a framework?
A: No. Manual constructor injection works well, and frameworks only automate the wiring.