The idea
What it is
Once you've decided to inject a dependency instead of building it with new inside a class, one question remains: how do you hand it in? There are exactly three ways. You can pass it through the constructor, you can set it through a setter (or property) after the object exists, or you can let a framework set a private field directly. All three end with the dependency living inside the object — but they are not interchangeable, and the choice quietly affects how safe, immutable, and testable your class is.
Think about building a chair. Constructor injection is bolting all four legs on at the factory before the chair ships — you can never receive a legless chair, because it isn't a chair until it has legs. Setter injection is shipping the chair and screwing the legs on later — handy if the legs are optional, but the chair can sit in your hallway wobbling in between. Field injection is a magic hand that reaches in and glues legs on somewhere you can't see — the least effort, but you can't watch it happen, can't make the legs permanent, and can't easily inspect the chair to check.
The one sentence to remember
Use the constructor for required dependencies (default), a setter only for genuinely optional or reconfigurable ones, and avoid field injection — it hides the dependency, blocks immutability, and forces you to bring a framework just to test the class.
These are all 'dependency injection'
All three styles are forms of dependency injection — the dependency comes from outside the class rather than being new-ed inside it. So this isn't a fight between DI and not-DI. It's a fight within DI about which way of passing the dependency gives you the strongest guarantees. The winner, almost always, is the constructor.
Mechanics
How it works
Constructor injection — the dependency is a constructor parameter
The dependency is declared as a parameter of the constructor, and the class stores it in a final/readonly field. Because the object cannot be built without supplying it, a required dependency is guaranteed present the moment the object exists.
class OrderService {
constructor(private readonly sender: MessageSender) {} // required, immutable
placeOrder(c: string) { this.sender.send(c, "Confirmed"); }
}
new OrderService(new EmailSender()); // can't build it without a senderThis buys you four things at once. (1) A required dependency is guaranteed — there is no way to construct a half-built object. (2) The field can be final/readonly, so the dependency is immutable and can't be swapped out from under you. (3) The dependency is visible in the public API — anyone reading the constructor signature sees exactly what this class needs. (4) You can build it in a plain unit test with new OrderService(fakeSender) — no framework, no reflection, no container.
Setter (property) injection — a setX() called after construction
Here the object is constructed first, then the dependency is handed in through a setter or writable property afterward.
class OrderService {
private sender?: MessageSender;
setSender(s: MessageSender) { this.sender = s; } // set AFTER construction
placeOrder(c: string) { this.sender?.send(c, "Confirmed"); }
}
const svc = new OrderService(); // ⚠️ valid object, but sender is undefined here
svc.setSender(new EmailSender()); // ...until you remember to call thisThe strength of setter injection is the same as its weakness: the dependency is optional and reconfigurable. That's perfect when a dependency genuinely is optional — a cache you may or may not attach, a logger that defaults to no-op. But for a required dependency it's a trap: between new and setSender(...) the object exists in a half-built state where calling placeOrder() blows up or silently does nothing. The dependency is no longer guaranteed, the field can't be final, and it's only partly visible in the API — you have to know to look for the setter.
Field injection — a framework sets a private field directly
The dependency is declared as a private field with a framework annotation, and the framework uses reflection to set it directly — no constructor parameter, no setter. In Java this is the @Autowired field.
class OrderService {
@Autowired // framework reaches in via reflection
private MessageSender sender; // can't be final; hidden from the API
void placeOrder(String c) { sender.send(c, "Confirmed"); }
}
// new OrderService() leaves sender = null unless the framework wires it.Field injection looks the cleanest — no boilerplate at all. But it's the one most teams discourage, for concrete reasons. The dependency is hidden — nothing in the public API tells you the class needs a MessageSender. The field can't be `final`, so it's mutable and the object can briefly exist with a null dependency. And worst for daily work: you can't test it with a plain `new` — new OrderService() leaves the field null, so you must spin up the framework or use reflection to inject a fake. A required dependency hidden behind reflection is exactly the thing constructor injection makes safe.
The scorecard
Line the three up against the properties that actually matter and the pattern is obvious. Constructor injection is the only style that wins every row; setter injection is half-and-half (its 'looseness' is a feature only for optional deps); field injection loses the rows you care about most.
new; its API visibility is partial (~) because you must know to look for the setter. Field loses the rows that hurt most: the dependency is hidden, mutable, and untestable without the framework.Constructor injection and 'too many parameters'
The usual objection — but my constructor has eight parameters! — is a feature, not a bug. A bloated constructor is constructor injection shouting that the class has too many responsibilities. Field injection doesn't fix that; it just hides the smell so the class can keep growing. Listen to the constructor and split the class.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
Three tabs for one OrderService that needs a MessageSender: Constructor, Setter, and Field. Each tab shows that style's small code form and lights up a fixed four-row scorecard — required dep guaranteed at construction?, can be `final`/immutable?, testable with a plain `new` + fake, no framework?, visible in the public API? — as ✓ / ~ / ✗. A running green-checks tally sits beside each style; Constructor scores highest. The one explain panel is replaced on every tab switch (never appended) to narrate that style's trade-off and when it's the right pick.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Start on Constructor and read the scorecard
Open the prototype on the Constructor tab. Read the small code form — the MessageSender is a constructor parameter stored in a final/readonly field. Now read the four-row scorecard: every row is a green ✓, and the tally shows the highest score. Note why each is a ✓: you can't build the object without the sender (guaranteed), the field is immutable, you can new it with a fake in a test, and the dependency is right there in the signature.
Switch to Setter and watch rows flip
Click the Setter tab. The one explain panel is replaced (not appended) to narrate the trade-off. Watch required guaranteed and can be final flip to ✗ — between new and setSender(...) the object is half-built and mutable. But notice testable with plain new stays ✓, and visible in API is a partial ~. Read the panel: setter injection is the right pick only for genuinely optional or reconfigurable dependencies.
Switch to Field and see why it's discouraged
Click the Field tab. The code shows an @Autowired private field with no constructor and no setter. Watch the scorecard collapse — guaranteed ✗, final ✗, testable ✗, visible ✗ — and the tally drop to zero green checks. The explain panel spells out the cost: the dependency is hidden, can't be final, and you need the framework or reflection just to inject a fake in a test. Click back to Constructor and confirm it's the only style that wins every row — that's why it's the default.
In practice
When to use it — and what trips people up
Pick the style by what the dependency is
The choice isn't about taste — it follows from whether the dependency is required or optional, and whether you want it immutable.
- Constructor — your default. Use it for every required dependency: a service's repository, its sender, its clock. You get a guaranteed, immutable, self-documenting dependency you can fake with a plain
newin a test. - Setter — only for genuinely optional or reconfigurable deps. An optional cache, a logger that defaults to no-op, something you legitimately reconfigure at runtime. If the object works fine without it, a setter is honest about that.
- Field — avoid in production code. It hides the dependency, blocks
final, and drags the framework into your unit tests. The one common exception is test code in some frameworks, where field injection into a test class is tolerated for brevity.
A required dependency behind a setter is a bug waiting to happen
If the object cannot function without the dependency, do not use a setter or a field for it. Both let the object exist in a state where the dependency is missing, turning a compile-time guarantee into a runtime NullPointerException someone hits in production. Make it a constructor parameter and the problem can't occur.
What it gives you
- Constructor — required dependencies are guaranteed at construction, so no object can exist half-built.
- Constructor — the field can be final/readonly, making the dependency immutable and thread-safe to publish.
- Constructor — dependencies are visible in the signature, so the class documents its own needs.
- Constructor — you can build the object with a plain new + fake in a unit test, with no framework or reflection.
- Setter — the right tool when a dependency is genuinely optional or must be reconfigured after construction.
Common mistakes
- Constructor — a long parameter list can feel heavy, though that usually signals the class is doing too much.
- Setter — the object can exist in a half-built state between construction and the setter call, and the field can't be final.
- Field — the dependency is hidden from the public API, so readers can't see what the class needs.
- Field — the field can't be final and the object can briefly hold a null dependency.
- Field — you can't test the class with a plain new; you need the framework or reflection to inject a fake.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
interface MessageSender {
send(to: string, text: string): void;
}
// 1) CONSTRUCTOR injection — required, immutable, visible, test-friendly.
class OrderServiceCtor {
constructor(private readonly sender: MessageSender) {} // can't build without it
placeOrder(customer: string) {
this.sender.send(customer, "Order confirmed");
}
}
// const svc = new OrderServiceCtor(new FakeSender()); // plain new + fake in a test
// 2) SETTER (property) injection — good for OPTIONAL/reconfigurable deps.
class OrderServiceSetter {
private sender?: MessageSender; // may be unset
setSender(sender: MessageSender) { this.sender = sender; }
placeOrder(customer: string) {
// ⚠️ half-built until setSender() is called
this.sender?.send(customer, "Order confirmed");
}
}
// const s = new OrderServiceSetter(); s.setSender(new FakeSender());
// 3) FIELD injection — no boilerplate, but hidden + can't be readonly.
// (TS has no @Autowired; the closest is a mutable field set from outside.)
class OrderServiceField {
sender!: MessageSender; // set later, e.g. by a container; not readonly
placeOrder(customer: string) {
this.sender.send(customer, "Order confirmed"); // null if never wired
}
}References & further reading
5 sources- Articlemartinfowler.com
Inversion of Control Containers and the Dependency Injection pattern — Martin Fowler
The foundational essay. The 'Constructor Injection with PicoContainer' and 'Setter Injection with Spring' sections lay out the constructor-vs-setter trade-off that every later article restates.
- Docsdocs.spring.io
Dependency Injection — Spring Framework Reference
Spring's own documentation, which explicitly recommends constructor-based injection for required dependencies and reserves setter injection for optional ones. The canonical primary source.
- Articlebaeldung.com
Constructor vs Field Dependency Injection — Baeldung
A code-first Spring walkthrough contrasting constructor and field injection, including how constructor injection enables final fields and framework-free testing while field injection blocks both.
- Bookoreilly.com
Effective Java, 3rd Edition — Item 5: Prefer dependency injection to hardwiring resources
Joshua Bloch's treatment of injecting dependencies through the constructor, and why a class that depends on configurable resources should take them as constructor parameters.
- Talkyoutube.com
Why field injection is evil (and constructor injection is better) — video walkthrough
A short, practical screen-cast that refactors @Autowired field injection into constructor injection and demonstrates the testability and immutability wins live.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 05
Why is constructor injection usually preferred over the other two styles?
question 02 / 05
What is the main risk of using setter injection for a REQUIRED dependency?
question 03 / 05
Why is field injection (e.g., Java's @Autowired on a private field) widely discouraged?
question 04 / 05
Which scenario is the BEST fit for setter (property) injection?
question 05 / 05
A teammate argues for switching to field injection because the constructor now has eight parameters. What's the best response?
0/5 answered