Dependency Inversion
Dependency Inversion makes testing and infrastructure swaps possible.
Introduction
Dependency Inversion makes testing and infrastructure swaps possible. Without DIP, domain code calls Postgres, Stripe, and Slack directly. With DIP, domain depends on OrderRepository, PaymentGateway, and Notifier — infrastructure implements those abstractions.
The story
Every service used new StripeClient() inline. Tests needed Stripe sandbox, ran four minutes, flaked often. One sprint of interfaces + DI made tests offline — twelve seconds, zero flakiness.
The business problem
Concrete dependencies in domain code cause:
- Slow flaky tests tied to external systems.
- Vendor lock-in — Stripe types leak into domain.
- Impossible local dev without full infra stack.
- Big-bang migrations when swapping database or provider.
The problem teams faced
Pre-DIP architecture:
- Domain imports SDK headers/types.
- Services construct dependencies with `new`.
- Tests spin real DB/API or skip coverage.
Understanding the topic
DIP rule: high-level modules define abstractions; low-level modules implement them. The dependency arrow inverts.
- Domain — owns interface (port).
- Infrastructure — implements interface (adapter).
- Composition root — only place knowing both sides.
Internal architecture
Layer dependencies (Clean Architecture):
HTTP Controller → Use Case → Domain Entity↓ depends onRepository interface (domain layer)↑ implemented byPostgresRepository (infra layer)
Visual explanation
DIP in three diagrams:
Informative example
Before / after DIP:
// ❌ Domain depends on concretionclass PlaceOrder {async run(cart: Cart) {const stripe = new Stripe(process.env.STRIPE_KEY);await db.query("INSERT ...");await stripe.charges.create({ amount: cart.total() });}}// ✅ Domain depends on abstractioninterface PaymentGateway { charge(amount: Money): Promise<Receipt>; }interface OrderRepository { save(order: Order): Promise<void>; }class PlaceOrder {constructor(private payments: PaymentGateway,private orders: OrderRepository,) {}async run(cart: Cart) {const receipt = await this.payments.charge(cart.total());await this.orders.save({ cart, receipt });}}
Execution workflow
List external needs
DB, payment, email, clock, random.
Real-world use
Clean Architecture, Hexagonal, and Onion are DIP at scale. NestJS modules, Spring Boot, ASP.NET Core enforce DIP by convention. Repository and Gateway patterns are named DIP applications.
Production case study
Stripe-coupled codebase rescue (above): tests 4 min → 12 sec; flakiness eliminated; Postgres swapped to in-memory in CI.
Trade-offs
- Fast reliable tests; swappable infra.
- More interfaces and wiring ceremony.
- Composition root complexity at scale — DI container helps.
Decision framework
- DIP whenever domain touches DB, HTTP, queues, time, randomness.
- Skip on pure functions and scripts with no tests.
Best practices
- Domain folder must not import `stripe`, `pg`, `@aws-sdk`.
- Factory functions work for DIP in functional style: `(deps) => (input) => result`.
- Contract tests on adapters, unit tests on domain with fakes.
Anti-patterns to avoid
- Interface in infra layer that domain imports — inversion backwards.
- Service locator hidden inside domain classes.
Common mistakes
- DIP everywhere on day one greenfield — inject real deps first, extract when test pain hits.
Debugging tips
- Production vs test behavior? Diff composition root wiring.
Optimization strategies
- Manual constructor injection until ~10 wired services — then consider container.
Common misconceptions
- DIP ≠ DI framework required. Constructor params suffice.
- Domain owns the port — not the database team.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1AdvancedQuestionWho owns the interface in DIP?+
Answer
Follow-up
2AdvancedQuestionDIP enable Hexagonal?+
Answer
Follow-up
Summary
You can invert dependencies, wire at composition root, and test with fakes. Tell the four-minute-to-twelve-second test story.