Dependency Injection — Detailed#
Without DI vs with DI#
classDiagram
direction LR
class OrderService_Bad {
+place()
}
class HttpClient
OrderService_Bad ..> HttpClient : new HttpClient
note for OrderService_Bad "Hidden dependency. Can't swap, can't mock."
class OrderService_Good {
-client: HttpClient
+OrderService(HttpClient)
+place()
}
OrderService_Good --> HttpClient : injected
note for OrderService_Good "Explicit dependency.\nSwap real vs fake at the composition root."
Forms of injection#
| Form | Example | When to use |
|---|---|---|
| Constructor injection | new OrderService(repo, gateway) |
default; makes deps required & immutable |
| Setter injection | svc.setLogger(l) |
optional collaborators |
| Interface injection | callback svc.inject(this) |
rare; OSGi-style |
| Field injection | @Autowired Repo repo; |
framework-only; harder to test |
Prefer constructor injection.
Containers#
Frameworks that do the wiring for you by reading metadata (annotations / XML / code):
- Java: Spring, Guice, Dagger
- C#: built-in
Microsoft.Extensions.DependencyInjection - Python:
dependency-injector, FastAPI'sDepends - Go: explicit, no container (functional options preferred)
Containers solve the "object graph wiring" problem but introduce magic and a learning curve. In small apps, hand-wired DI in main() is often clearer.
Lifecycle scopes#
| Scope | What it means |
|---|---|
| Singleton | one instance per container (default for stateless services) |
| Request / per-call | new instance per HTTP request — for stateful per-request work |
| Transient | new instance every time it's resolved |
| Scoped | matches a custom scope (e.g. background job) |
Composition root#
The one place that knows about every concrete class in your app. Keep it small. Everything else only knows abstractions.
public static void main(String[] args) {
var clock = new SystemClock();
var repo = new PostgresOrderRepo(dataSource);
var gateway = new StripeGateway(httpClient, apiKey);
var notifier = new EmailNotifier(smtpHost);
var service = new OrderService(repo, gateway, notifier, clock);
new HttpServer(service).start();
}
Anti-patterns#
- Service Locator — code asks a global for its deps (
Locator.get(Foo.class)). Looks like DI, smells like singletons. Prefer constructor injection. - Newing inside constructors — defeats DI:
Foo() { this.bar = new Bar(); }. - Auto-wiring everything — opaque object graphs. Frameworks should help, not hide.
- Container in the domain layer — only the composition root (or a thin facade) should reference the container.
Where DI shows up in this site#
Practically every microservice has a composition root (or framework equivalent). Notable cases: - Payment gateway / Digital wallet — injection of network clients, KMS, ledger. - Logger framework — appenders + formatters injected at config time. - A/B testing platform / Feature flags — injection of evaluator + storage. - Job scheduler — injection of executor + retry policy + clock.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Idempotency & retries | safe re-execution, backoff + jitter | idempotency-retries |
LLD |
Creational patterns | Singleton, Factory, Builder, Prototype | creational-patterns |
LLD |
Structural patterns | Adapter, Decorator, Facade, Proxy, Composite | structural-patterns |
LLD |
Dependency injection | DI / IoC, constructor injection, containers | dependency-injection |
LLD |
Immutability | immutable types, persistent collections | immutability |