What is dependency injection and why does it help?
Dependency injection means a component receives the things it depends on from outside, rather than constructing them itself.
The contrast. Without it, a class creates its own collaborators:
A UserService that internally does new PostgresRepository() is permanently bound to Postgres. It cannot be used with anything else, and it cannot be tested without a real database.
With injection, the repository is passed in — through the constructor, a setter, or a parameter. The service now depends on an interface, and whoever constructs it decides the implementation.
Why this helps:
Testability, which is the main practical benefit. You can pass a fake or in-memory implementation in a test, making it fast, deterministic and independent of external systems. Code that constructs its own dependencies is frequently untestable without elaborate mocking of module internals — a strong signal the design is wrong.
Swappability. Changing implementations becomes a configuration decision rather than a code change throughout the codebase.
Explicit dependencies. A constructor signature documents exactly what a class needs. A class that reaches out to global state hides its requirements, and you discover them when something breaks.
Separation of construction from use, so wiring lives in one place rather than scattered.
A common confusion: dependency injection is the pattern; a DI container or framework is an optional tool for wiring things up automatically. You do not need a container — passing arguments to a constructor is dependency injection, and in small codebases manual wiring is clearer.
The honest costs: indirection makes it harder to see what actually runs; container-based systems can fail at runtime rather than compile time; and over-application produces interfaces with exactly one implementation, which adds ceremony without benefit.
A reasonable rule: inject things that vary, that cross a boundary, or that are slow — not every collaborator.