Dependency injection means a class receives the things it needs from outside instead of creating them itself. That one change makes code easier to test, easier to reconfigure and easier to read, which is why so many frameworks are built around it.
What counts as a dependency
A dependency is any object a piece of code needs to do its job: a database connection, an HTTP client, a logger, a payment gateway, a clock. If class A calls methods on B, then B is a dependency of A.
The question dependency injection answers is simple: who creates B? Either A builds it for itself, or something else builds it and hands it over. Dependency injection is the second option. The class that uses the dependency just works with whatever it is given, and a separate piece of code, the injector, decides what that is.
A class that builds its own dependencies
Here is an order service that sends a confirmation email. It creates its own email sender:
from dataclasses import dataclass
@dataclass
class Order:
id: int
customer_email: str
class SmtpEmailSender:
def send(self, to, subject, body):
... # talks to a real mail server
class OrderService:
def __init__(self):
self.email = SmtpEmailSender()
def place_order(self, order):
# save the order, take payment...
self.email.send(
order.customer_email,
"Order confirmed",
f"Thanks for order {order.id}",
)It works, but OrderService is now welded to SmtpEmailSender. Three problems follow:
- You can't test it without sending email. Every test of
place_ordertalks to a real mail server, or fails because there isn't one. - You can't swap it. Moving to a different email provider means editing
OrderService, even though nothing about placing orders changed. - You can't see what it needs. The constructor takes no arguments, so the only way to find out it sends email is to read its body.
The same class with injection
Move the creation out, and ask for the dependency in the constructor instead:
class OrderService:
def __init__(self, email_sender):
self.email = email_sender
def place_order(self, order):
# save the order, take payment...
self.email.send(
order.customer_email,
"Order confirmed",
f"Thanks for order {order.id}",
)OrderService no longer knows or cares which email sender it has. It only relies on the object having a send method. Whoever creates the service picks the sender:
service = OrderService(SmtpEmailSender())Tomorrow that line could pass a different sender, and OrderService would not change at all. Same class, different dependency: that is the whole idea.
In statically typed languages the constructor usually asks for an interface rather than a concrete class, so the compiler enforces the contract. In Python you can say the same thing with a Protocol, which type checkers such as mypy understand:
from typing import Protocol
class EmailSender(Protocol):
def send(self, to: str, subject: str,
body: str) -> None: ...
class OrderService:
def __init__(self, email_sender: EmailSender):
self.email = email_senderWhy it makes testing easy
The biggest everyday win is in tests. Because the service takes its sender from outside, a test can hand it a fake that records calls instead of sending anything:
class FakeEmailSender:
def __init__(self):
self.sent = []
def send(self, to, subject, body):
self.sent.append((to, subject))
def test_place_order_sends_confirmation():
fake = FakeEmailSender()
service = OrderService(fake)
service.place_order(Order(7, "cat@example.com"))
assert fake.sent == [
("cat@example.com", "Order confirmed"),
]The test is fast, needs no network and checks exactly the behaviour you care about. There is no need to patch module globals or reach inside the class: the seam is right there in the constructor.
Ways to inject
Constructor injection
Passing dependencies to the constructor, as above, is the default choice. The object is complete the moment it exists, its needs are listed in one place, and it can't be used half-configured.
Setter and method injection
A dependency can also be set through a property after construction, or passed straight to the one method that needs it. Setter injection suits a genuinely optional dependency with a sensible default. Method injection suits something that changes on every call, such as the current user. Both are less common, because an object that can exist without its dependencies can also be used by mistake without them.
Who does the injecting
Something still has to create the real objects and connect them. That place is often called the composition root: usually the program's entry point, where the whole object graph is built once.
def main():
sender = SmtpEmailSender()
orders = OrderService(sender)
app = WebApp(orders)
app.run()Wiring by hand like this is sometimes called 'pure DI', and for small programs it is all you need. It is plain code, so a missing piece shows up as an ordinary error.
In bigger applications the object graph grows large, and writing it out by hand becomes a chore. A DI container does the wiring for you. You register which implementation to use for each dependency, and the container builds an object and everything it needs on request. ASP.NET Core has one built in, Spring's IoC container does the same for Java, Angular has its own injector, and FastAPI resolves the dependencies a route function declares with Depends.
Containers also manage lifetimes: whether every consumer shares one instance, gets a fresh one each time, or gets one per web request. ASP.NET Core calls these singleton, transient and scoped.
A container is a convenience, not the pattern itself. The pattern is just 'pass dependencies in'.
How it relates to its neighbours
Three terms get mixed up with dependency injection:
- Inversion of control is the broad idea that outside code, often a framework, makes decisions your code would otherwise make for itself. Dependency injection is one form of it: control over creating dependencies moves out of the class.
- The dependency inversion principle, the 'D' in SOLID, says high-level code should depend on abstractions, not on concrete low-level details. Injection is the usual way to put it into practice:
OrderServicedepends on 'something that can send email', and the concrete sender is plugged in from outside. - A service locator is an alternative where a class asks a global registry for its dependencies. It removes the direct construction, but the class still reaches out for what it needs, and its dependencies are hidden inside its methods again. Many developers treat it as an anti-pattern for that reason.
Common mistakes
- Injecting the container. Passing the container into a class and pulling dependencies out of it turns DI back into a service locator. Ask for the specific things the class needs.
- Constructors with a dozen parameters. DI makes a class's needs visible, and a very long list is a sign the class is doing too much. Split the class rather than hiding the list.
- A hard-coded dependency deep inside. One object created inside a method undoes the benefit for that path. If it talks to the outside world, inject it.
- Mismatched lifetimes. A singleton that holds on to a per-request dependency keeps it alive long after its request ends, which can leak one user's data into another's. This is often called a captive dependency. In development, ASP.NET Core checks for scoped services injected into singletons and throws when it finds one.
- Abstracting everything. Plain values and simple helpers with no side effects, such as a string formatter or a maths function, don't need injecting. Inject the things you'd want to swap or fake: I/O, time, randomness and external services.
When to use it
Use dependency injection for anything that talks to the outside world or that you'd want to replace in a test: databases, HTTP clients, file systems, clocks, message queues, payment and email providers. It pays off most in code that will live a long time and be tested.
For a short script, a tiny tool or pure calculations, a container adds ceremony without much return. You can still pass things in as arguments, which is dependency injection at its simplest, with no container and no interfaces.
Key takeaways
- A class receives its dependencies from outside instead of creating them.
- The class stays the same while what it's given can change: a real service in production, a fake in tests.
- Constructor injection is the default: it makes a class's needs visible and complete from the start.
- A container automates the wiring in large apps, but the pattern works with plain code.
- Injecting the container itself, or piling on constructor parameters, are warning signs.