Dependency Injection
Supplying dependencies from outside rather than creating them internally, enabling testability.
Overview
Dependency Injection (DI) is a pattern where a class receives its dependencies from outside rather than creating them internally. It inverts the control of dependency creation from the consumer to the caller or a DI container. This makes classes independently testable and the system configuration explicit.
Origin
The term was popularised by Martin Fowler in "Inversion of Control Containers and the Dependency Injection pattern" (2004). The pattern itself appeared in earlier work by Kent Beck and others. Spring Framework (Java, 2002) made container-managed DI mainstream.
Examples
Constructor injection and test doubles
// Without DI: impossible to test without hitting the real API
class OrderService {
async placeOrder(data) {
const payment = await new StripeClient().charge(data.total) // hardcoded!
const order = await db.orders.create({ ...data, paymentId: payment.id })
return order
}
}
// With DI: swap any dependency in tests
class OrderService {
constructor({ paymentClient, orderRepo }) {
this.paymentClient = paymentClient
this.orderRepo = orderRepo
}
async placeOrder(data) {
const payment = await this.paymentClient.charge(data.total)
return this.orderRepo.create({ ...data, paymentId: payment.id })
}
}
// In tests:
const service = new OrderService({
paymentClient: { charge: async () => ({ id: 'ch_test_123' }) },
orderRepo: { create: async (d) => ({ id: 1, ...d }) },
})DI container in Ruby (dry-container)
require 'dry-container'
require 'dry-auto_inject'
class Container
extend Dry::Container::Mixin
register('repo.users') { UserRepository.new }
register('repo.orders') { OrderRepository.new }
register('service.email') { EmailService.new(api_key: ENV['SENDGRID_KEY']) }
register('service.order') do
OrderService.new(
user_repo: resolve('repo.users'),
order_repo: resolve('repo.orders'),
mailer: resolve('service.email')
)
end
end
Import = Dry::AutoInject(Container)
class OrdersController < ApplicationController
include Import['service.order'] # auto-injected
def create
order_service.place(order_params)
end
endUse Cases
- 01Any class that collaborates with external systems (database, API, file system, clock)
- 02Test doubles: mock payment clients, fake email senders, in-memory repositories
- 03Feature flags: inject a different implementation based on a feature switch
- 04Multi-tenancy: inject a tenant-scoped repository without changing business logic
When Not to Use
- //Value objects and entities that have no external dependencies, DI adds unnecessary ceremony
- //Simple scripts where the dependency is always the same and testing is not a concern
- //Over-engineering: injecting a logger into every class creates noise rather than testability
Technical Notes
- Constructor injection is preferred over setter injection: dependencies are explicit and the object is always fully initialised
- DI is not the same as a service locator. A service locator hides dependencies; DI makes them explicit in the constructor signature
- In Rails, ActiveSupport::CurrentAttributes provides a form of ambient dependency injection for request-scoped data
- TypeScript + InversifyJS or tsyringe provide decorator-based DI containers with type-safe resolution
More in Architecture