Architecture & Patterns110 min total · 26 parts
ACID, SOLID, and Design Patterns: A Complete Software Design Reference
Part 11 of 26 · ~2 min
Dependency Inversion Principle
Code that expresses a business decision shouldn't be wired straight into code that carries out a technical task — route both through a shared abstraction instead. "Business decision" here means something like deciding who gets assigned to a shift; "technical task" means the plumbing underneath it, like a specific database driver. Leave that abstraction out and a change to nothing more than which database is being used ends up forcing an edit to the assignment policy too, even though the policy itself never actually changed.
// Violates DIP — ShiftAssignmentService is wired directly to SqlShiftRepository.
// Testing the assignment policy without a real database, or swapping the
// storage technology later, both require editing ShiftAssignmentService itself.
public class SqlShiftRepository
{
public void Save(ShiftAssignment assignment) { /* talks directly to SQL Server */ }
}
public class ShiftAssignmentService
{
private readonly SqlShiftRepository _repository = new SqlShiftRepository();
public void Assign(ShiftAssignment assignment) => _repository.Save(assignment);
}
// Both ShiftAssignmentService and SqlShiftRepository now depend on the
// IShiftRepository abstraction, instead of the service depending on the
// concrete repository directly.
public interface IShiftRepository
{
void Save(ShiftAssignment assignment);
}
public class SqlShiftRepository : IShiftRepository
{
public void Save(ShiftAssignment assignment) { /* talks directly to SQL Server */ }
}
public class InMemoryShiftRepository : IShiftRepository
{
private readonly List<ShiftAssignment> _assignments = new();
public void Save(ShiftAssignment assignment) => _assignments.Add(assignment); // trivial in a unit test
}
public class ShiftAssignmentService
{
private readonly IShiftRepository _repository;
public ShiftAssignmentService(IShiftRepository repository) => _repository = repository; // supplied, not constructed
public void Assign(ShiftAssignment assignment) => _repository.Save(assignment);
}
Keep two ideas apart that get merged constantly. Dependency Inversion is the design rule itself: reach for an abstraction, never for a concrete implementation, when one piece of code needs another. Dependency Injection is just the mechanical move that satisfies that rule in practice — a class receives what it needs from whoever constructs it, the way ShiftAssignmentService takes an IShiftRepository through its constructor above, instead of reaching out and building that dependency on its own.
A DI container — ASP.NET Core ships one out of the box — is neither of those two things; it's automation, nothing more. All it does is look at the graph of interfaces and concrete classes at startup and wire the correct pairings together, so nobody has to new anything up by hand at the composition root. Delete the container entirely and Dependency Inversion still holds, because the principle was satisfied the moment ShiftAssignmentService started asking for IShiftRepository instead of constructing SqlShiftRepository itself — the container just saves someone from writing that wiring code by hand.