Architecture

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
end

Use 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