Intermediate15 min readDesign Principles & Heuristicslive prototype

Injection styles: constructor vs setter vs field

There are three ways to hand a dependency to an object: through its constructor, through a setter you call later, or by letting a framework reach in and set a private field. They look similar but they are not equal — constructor injection makes required dependencies guaranteed, fields immutable, and tests framework-free, which is why it usually wins.

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.

typescript
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 sender

This 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.

typescript
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 this

The 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.

java
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.

Constructor Setter Field Required dep guaranteed at construction? Can be final / immutable? Testable with plain new + fake, no framework? Dependency visible in the public API? ~ Green checks 4 0
The four properties that matter, scored across the three styles. Constructor is the only one that wins every row — guaranteed, immutable, framework-free to test, and self-documenting. Setter trades the guarantee away (its point is to be optional) but stays testable with a plain 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.

try 01

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.

try 02

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.

try 03

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 new in 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

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