Skip to main content

Dependency Injection

complexitypopularity

Hand a class its collaborators from the outside (constructor, setter, or a framework) instead of letting it construct them itself, so it depends on an interface without knowing how to build what implements it.

The problem

OrderService needs to charge a card, so it builds a StripeGateway in its own constructor and calls it a day. Simple, until two things happen: a test tries to run checkout() and ends up needing a real Stripe API key, and the business decides to add PayPal as a second provider next quarter.

The dependency is not the problem - OrderService obviously needs some way to charge cards. The problem is who decides which one, and when. Baking new StripeGateway() into the constructor answers that question at compile time, permanently, for every caller everywhere.

The solution

OrderService still depends on a PaymentGateway interface - that part does not change. What changes is who builds the concrete implementation: not OrderService, but whoever assembles the program. OrderService's constructor simply asks for a PaymentGateway and uses whatever arrives.

Production code hands it a StripeGateway wired to a real API key. Test code hands it a SandboxGateway that always succeeds instantly. OrderService never changes, never recompiles differently, and never contains the word "Stripe".

Application startupTest setupOrderServicePaymentGateway (whichever is handed in)new StripeGateway(apiKey)1new OrderService(stripeGateway)2gateway.charge(total, card)3POST to Stripe API4new SandboxGateway()5new OrderService(sandboxGateway)6gateway.charge(total, card)7
  1. 1Startup code builds the real gateway - the one place in the whole program that knows Stripe exists.
  2. 2The gateway is handed to OrderService through its constructor. OrderService never called `new` on it.
  3. 3Checkout logic calls the interface method it was given. It could be Stripe, PayPal, or a mock - the call site is identical.
  4. 4Whatever the real gateway does to actually move money happens entirely behind the interface.
  5. 5A test builds a stand-in gateway that always "succeeds" with no network call.
  6. 6The exact same constructor, a different argument. OrderService's source code does not change at all.
  7. 7Same call as production. This time it resolves instantly and deterministically, with zero network flakiness.

Structure

The diagram has no line at all connecting OrderService to either concrete gateway - only to the interface. That missing line is the entire pattern.

«interface»PaymentGatewaycharge(amount, card)DEPENDENCYStripeGatewaycharge(amount, card)REALSandboxGatewaycharge(amount, card)TESTOrderServicegateway: PaymentGatewayconstructor(gateway)checkout(order)CONSUMER
implementsuses

Code

Same example three ways: a checkout flow that receives its payment gateway instead of building one.

// OrderService builds its own dependency, so it is welded to Stripe forever.
class OrderService is
field gateway: StripeGateway
 
constructor OrderService() is
gateway = new StripeGateway(readApiKeyFromEnv())
// Testing this class means testing Stripe, whether you wanted to or not.
 
method checkout(order) is
gateway.charge(order.total, order.card)
// OrderService's own source has no idea Stripe exists. It only knows the
// PaymentGateway interface, handed in from outside.
class OrderService {
constructor(private gateway /* : PaymentGateway */) {}
 
checkout(order) {
const result = this.gateway.charge(order.total, order.card);
if (result.succeeded) order.markPaid();
return result;
}
}
// Startup: new OrderService(new StripeGateway(apiKey))
// Test code: new OrderService(new SandboxGateway())
// The interface OrderService is allowed to depend on.
interface PaymentGateway is
method charge(amount, card)
 
// The real implementation. It knows Stripe exists; nothing else does.
class StripeGateway implements PaymentGateway is
field apiKey: string
 
constructor StripeGateway(apiKey) is
this.apiKey = apiKey
 
method charge(amount, card) is
return httpPost("https://api.stripe.com/charges", apiKey, amount, card)
 
// A second implementation for tests. Same interface, no network.
class SandboxGateway implements PaymentGateway is
method charge(amount, card) is
return ChargeResult.success("sandbox-" + randomId())
 
// OrderService never builds its own gateway - it receives one.
class OrderService is
field gateway: PaymentGateway
 
constructor OrderService(gateway) is
this.gateway = gateway
 
method checkout(order) is
result = gateway.charge(order.total, order.card)
if result.succeeded then
order.markPaid()
return result
 
// Wiring happens once, at the edge of the program.
method main() is
gateway = new StripeGateway(readApiKeyFromEnv())
service = new OrderService(gateway)
 
// A test wires the same class differently, with no code change to OrderService.
method testCheckout() is
service = new OrderService(new SandboxGateway())
result = service.checkout(sampleOrder())
assert result.succeeded

When to use it

  • A class currently constructs its own collaborators (new SomeService() inside a constructor or method) and that collaborator is exactly the thing a test would want to replace.
  • More than one implementation of a dependency is expected to exist, now or eventually (multiple payment providers, multiple storage backends, a mock for tests).

Pitfalls

  • Parameter explosion as a disguise. Injecting ten dependencies into one constructor does not fix a class doing ten jobs - it just makes the ten jobs visible in the parameter list instead of buried in new calls scattered through the method bodies.
  • Injecting concretions instead of interfaces. OrderService(StripeGateway gateway) still couples to Stripe; the parameter type has to be the interface, or nothing was actually decoupled.
  • Container magic replacing understanding. A DI framework wiring dozens of beans by convention can make it genuinely hard to trace, by reading, which concrete class ends up behind an interface at runtime. That is a real cost, not just a learning curve.

Don't confuse it with

  • Factory Method. Factory Method still creates the object, inside a method the consumer calls; the consumer just delegates the "which concrete class" decision to a subclass. DI removes creation from the consumer's code entirely - it receives a finished object and never calls a constructor for it at all.
  • Service Locator. A Service Locator is a global registry a class actively queries for its dependencies (Locator.get(PaymentGateway.class)) - the class still reaches out and asks. DI hands the dependency in without the class ever asking; the class is passive.
  • Strategy. The mechanics can be identical code, but Strategy names the pattern of swapping an interchangeable algorithm at runtime for behavioral reasons; DI names how any collaborator, algorithmic or not, gets into a class in the first place.

Check yourself

Question 1 of 5

What does OrderService know about how to construct a PaymentGateway?