Dependency Injection in Practice

Supply dependencies to policy code instead of constructing infrastructure inside it. This makes the collaboration visible and lets tests exercise a decision independently of a database, network client, clock, or filesystem.

The design is complete only when someone also owns configuration, lifetime, and cleanup. Dependency injection can make a boundary easier to test; it does not prove a substitute behaves like the real service or make shared objects safe for concurrent use.

TL;DR

Quick Example

This Python 3 program supplies inventory access to a small policy object. It checks the same read contract with an in-memory substitute and a real SQLite adapter. The data is fictional, and the example decides eligibility only; it does not reserve inventory atomically.

Save it as injection_example.py and run python injection_example.py. The composition at the bottom owns and closes the connection; neither the policy nor the adapter opens a hidden global database. The Python typing documentation describes protocols for structural typing. Annotations do not enforce the full behavioral contract at runtime, which is why the example also has executable cases.

Core Concepts

Injection and dependency inversion

Injection describes supplying a collaborator. Dependency inversion concerns which abstraction policy depends on. A policy that receives a narrow inventory contract can remain independent of the concrete storage client; merely passing a giant database SDK into every object may leave the original coupling intact.

Martin Fowler's injection discussion emphasizes separating configuration from use. The policy should ask its collaborator for the needed result, while composition decides which implementation supplies it.

Constructor injection and composition

For a required long-lived collaborator, constructor injection makes the requirement visible when the object is created. Spring's documentation describes this pattern and distinguishes required constructor dependencies from optional configuration.

You do not need a container for a handful of objects. Ordinary construction in one startup function can be sufficient. A container becomes useful when the application needs systematic registration, scopes, integration hooks, or a large object graph. Its configuration still needs an owner and a test.

A service locator can hide the same coupling

If policy receives a general container and looks up arbitrary services during execution, readers cannot see its requirements from the constructor alone. This also lets a new hidden collaborator appear without an obvious signature change.

Prefer an explicit dependency or a narrowly typed factory when deferred creation is actually required. A factory that creates a scoped operation is different from giving every object unrestricted access to the application's service registry.

Test Three Different Questions

The example's missing-SKU rule is part of the interface contract: return zero. Both implementations are checked against it. Without that rule, a fake might return zero while a real adapter throws, and policy tests would silently exercise a different system.

Extend the contract with the distinctions that matter: missing versus unavailable data, exception translation, timeout handling, consistency, and transaction scope. Do not make the fake magically succeed where the real dependency can fail. A substitutable API needs compatible behavior, not just matching method names.

The SQLite adapter check is still not a test of PostgreSQL, a remote inventory API, or production concurrency. Run the relevant adapter in its intended environment before claiming that boundary is verified.

Own Lifetimes and Failure Boundaries

A dependency must remain valid for the period its consumer uses it. A process-wide service that retains a request-specific connection or user context can accidentally extend that state into later requests. Frameworks have different scope rules; .NET's scope guidance shows how its provider validates scoped services captured by singletons.

Also separate container safety from service safety. .NET's guidelines explain that thread-safe resolution does not make resolved instances thread-safe. Apply the same design question to your own collaborators: what mutable state is shared, and who synchronizes access?

In the example, the connection exists only within a bounded composition scope. A web application might instead create a request-scoped session through a supported framework pattern. The correct owner also handles cleanup after errors and cancellation, not only after the happy path.

Finally, keep the policy's evidence modest. A successful inventory read followed by can_fulfill does not reserve stock. Another request can change availability before a later write. If the operation promises a reservation, expose and test an atomic boundary that can actually uphold that promise.

Best Practices

Inject the decision boundary

Prefer available(sku) or a deliberate reservation operation to a general-purpose query executor when that keeps policy independent of storage details. Choose the abstraction from the caller's contract rather than copying every method of the vendor client.

Keep assembly visible

Centralize configuration and construction at a suitable application boundary. Include the selected implementation and resource scope in integration checks so a correct class is not undermined by incorrect wiring.

Retain an adapter verification path

Share meaningful contract cases where possible, then add dependency-specific tests for transactions, errors, and concurrency. A fast fake is useful precisely when its limits are understood.

Common Mistakes

Bad: Add an interface that exposes the entire infrastructure SDK unchanged.

Correct: Define the narrow behavior policy needs, including error and lifetime semantics that matter to callers.

Bad: Replace constructor dependencies with calls to a global singleton or registry.

Correct: Make required collaborators explicit and scope them through owned composition.

Bad: Treat a fake's successful return value as proof of a production transaction guarantee.

Correct: Verify the real adapter and atomic operation at the boundary where that guarantee must hold.

FAQ

Do I need a dependency-injection framework?

No. Passing dependencies through ordinary constructors or functions is injection. Adopt a container when its lifecycle and assembly support solves a concrete application problem.

Should every class have an interface?

No. Add a boundary when it separates policy from a changing or consequential dependency, supports a real substitution, or clarifies ownership. Extra indirection without a purpose makes maintenance harder.

Is a mock enough to test an integration?

A mock can verify an expected collaboration, but it cannot prove the real dependency accepts a query or honors the same contract. Use real adapter checks for those questions.

Can injection fix a race condition?

It can expose a better boundary for testing and implementation. The race is fixed by a correct synchronization or transaction design, not by the act of passing an object into a constructor.

Related Topics

References