Dependency Injection
Principle Link
Dependency Injection is a pattern that implements Dependency Inversion. The principle defines dependency direction. The pattern defines how objects receive dependencies.
Core Idea
A class should receive collaborators from the outside. It should not create them inside business methods. This keeps construction separate from behavior.
Conceptual Overview
Dependency injection gives explicit boundaries. Object creation happens in one assembly point. Business objects focus on domain behavior.
All examples below use the shared handbook application context: Hypothetical Scenario.
Common forms:
- Constructor injection
- Method parameter injection
- Framework managed injection
Composition root creates objects and wires contracts. Use case code receives ready collaborators.
Request flow with explicit construction and pass through steps.
Computing History
In 2004, Martin Fowler documented Dependency Injection as a core Inversion of Control pattern. The pattern became common in web frameworks and enterprise systems. Teams adopted it to reduce hidden state and test friction.
Sources: Fowler (2004)
Generic Example: Constructor Injection
from dataclasses import dataclass
class EventPublisher:
async def publish(self, topic: str, payload: dict) -> None:
raise NotImplementedError
@dataclass
class CarDealNotifier:
publisher: EventPublisher
async def notify_match_score(self, profile_id: str, listing_id: str, score: int) -> None:
payload = {"profile_id": profile_id, "listing_id": listing_id, "score": score}
await self.publisher.publish("car.match.score.updated", payload)
CarDealNotifier receives its dependency.
It does not construct a broker client.
Generic Example: FastAPI Wiring
from fastapi import Depends, FastAPI
app = FastAPI()
def get_event_publisher() -> EventPublisher:
return KafkaPublisher(brokers=["broker:9092"])
@app.post("/profiles/{profile_id}/listings/{listing_id}/score")
async def update_match_score(
profile_id: str,
listing_id: str,
score: int,
publisher: EventPublisher = Depends(get_event_publisher),
):
use_case = CarDealNotifier(publisher=publisher)
await use_case.notify_match_score(
profile_id=profile_id,
listing_id=listing_id,
score=score,
)
return {"status": "accepted"}
The route receives infrastructure dependency. The use case receives an abstraction instance.
Generic Example: Rust with Traits
Rust traits provide a strong abstraction boundary. The use case depends on a trait contract. Any struct that implements the trait can be injected.
#[derive(Debug)]
struct PublishError;
trait EventPublisher {
fn publish(&mut self, topic: &str, payload: String) -> Result<(), PublishError>;
}
struct CarDealNotifier<P: EventPublisher> {
publisher: P,
}
impl<P: EventPublisher> CarDealNotifier<P> {
fn new(publisher: P) -> Self {
Self { publisher }
}
fn notify_match_score(
&mut self,
profile_id: &str,
listing_id: &str,
score: u32,
) -> Result<(), PublishError> {
let payload = format!(
"{{\"profile_id\":\"{}\",\"listing_id\":\"{}\",\"score\":{}}}",
profile_id, listing_id, score
);
self.publisher.publish("car.match.score.updated", payload)
}
}
Composition root injects a concrete publisher:
struct KafkaPublisher;
impl EventPublisher for KafkaPublisher {
fn publish(&mut self, _topic: &str, _payload: String) -> Result<(), PublishError> {
Ok(())
}
}
fn build_notifier() -> CarDealNotifier<KafkaPublisher> {
CarDealNotifier::new(KafkaPublisher)
}
Trait object variant for runtime selection:
struct CarDealNotifierDyn {
publisher: Box<dyn EventPublisher>,
}
impl CarDealNotifierDyn {
fn new(publisher: Box<dyn EventPublisher>) -> Self {
Self { publisher }
}
}
Unit test with a fake implementation:
#[derive(Default)]
struct FakePublisher {
events: Vec<(String, String)>,
}
impl EventPublisher for FakePublisher {
fn publish(&mut self, topic: &str, payload: String) -> Result<(), PublishError> {
self.events.push((topic.to_string(), payload));
Ok(())
}
}
#[test]
fn notify_match_score_publishes_one_event() {
let fake = FakePublisher::default();
let mut notifier = CarDealNotifier::new(fake);
let result = notifier.notify_match_score("profile-42", "vin-4Y1SL65848Z411439", 91);
assert!(result.is_ok());
assert_eq!(notifier.publisher.events.len(), 1);
assert_eq!(notifier.publisher.events[0].0, "car.match.score.updated");
}
Core behavior stays unchanged. Only injected implementations change across environments.
Testing with Stable Fakes
class FakePublisher(EventPublisher):
def __init__(self):
self.events = []
async def publish(self, topic: str, payload: dict) -> None:
self.events.append((topic, payload))
async def test_notify_match_score_publishes_one_event():
fake = FakePublisher()
notifier = CarDealNotifier(publisher=fake)
await notifier.notify_match_score(
profile_id="profile-42",
listing_id="vin-4Y1SL65848Z411439",
score=91,
)
assert fake.events == [
(
"car.match.score.updated",
{
"profile_id": "profile-42",
"listing_id": "vin-4Y1SL65848Z411439",
"score": 91,
},
)
]
The test verifies behavior from input to output. No patching of internals is needed.
Anti Pattern and Better Version
Non compliant:
class CarDealNotifier:
async def notify_match_score(self, profile_id: str, listing_id: str, score: int) -> None:
broker = KafkaPublisher(brokers=["broker:9092"])
await broker.publish(
"car.match.score.updated",
{"profile_id": profile_id, "listing_id": listing_id, "score": score},
)
Issue:
- Class creates infrastructure detail directly
- Tests must patch constructors and internal calls
Compliant:
- Create dependencies in a composition root
- Inject dependencies through constructor or parameters
- Keep use case code free of concrete client creation
Testing cost comparison. Injected dependencies keep tests short and stable.
Foundational Principle Links
This pattern links to Abstraction and Boundaries. Injection consumes contracts instead of concrete internals.
This pattern links to Modularity and Composition. Composition roots assemble modules through explicit collaborators.
This pattern links to Simplicity First. Construction and behavior stay separated.
This pattern links to Correctness and Testing. Injected fakes keep tests small and deterministic.
Practice Checklist
- Build concrete objects in one composition root
- Inject abstractions into use case classes
- Avoid hidden singletons with mutable global state
- Keep tests focused on outcomes, not call mechanics
- Replace mocks with deterministic fakes when possible
Written by: Pedro Guzmán
See References for complete APA-style bibliographic entries used on this page.