Skip to content

// SOLID & Principles · 05

Dependency Inversion

Logic tied to infrastructureleads toBoth depend on abstractions

High-level policy owns the interface; details implement it.

How it fits together

Dependency Inversion structureBeforeAfterInvoiceServiceApplicationIssue(invoice)Find(id)DbInvoiceStoreInfrastructureSave(invoice)FindById(id)InvoiceDbInvoiceServiceApplicationIssue(invoice)Find(id)«interface»InvoiceStoreowned by ApplicationSave(invoice)FindById(id)DbInvoiceStoreInfrastructureSave(invoice)FindById(id)InvoiceDbdirect callORM queryvia abstractionORM query
uses / callsimplements
  • InvoiceService uses / calls DbInvoiceStore (direct call)
  • DbInvoiceStore uses / calls InvoiceDb (ORM query)
  • InvoiceService uses / calls InvoiceStore (via abstraction)
  • DbInvoiceStore implements InvoiceStore
  • DbInvoiceStore uses / calls InvoiceDb (ORM query)

Spot it

  • Business logic opens its own database connection
  • Can't unit test without a real database
  • Swapping a vendor means editing core logic

It costs

Extra indirection; abstracting stable, single-implementation details only adds noise.

In code

public interface IInvoiceStore{    Task SaveAsync(Invoice invoice, CancellationToken ct);    Task<Invoice?> FindByIdAsync(Guid id, CancellationToken ct);}​public sealed class InvoiceService(IInvoiceStore store){    public async Task IssueAsync(Invoice invoice, CancellationToken ct)    {        invoice.MarkIssued();        await store.SaveAsync(invoice, ct);    }​    public Task<Invoice?> FindAsync(Guid id, CancellationToken ct) => store.FindByIdAsync(id, ct);}​public sealed class DbInvoiceStore(InvoiceDb db) : IInvoiceStore{    public Task SaveAsync(Invoice invoice, CancellationToken ct) =>        db.UpsertInvoiceAsync(invoice, ct);​    public Task<Invoice?> FindByIdAsync(Guid id, CancellationToken ct) =>        db.FindInvoiceAsync(id, ct);}​// Program.csbuilder.Services.AddScoped<IInvoiceStore, DbInvoiceStore>();
Go deeperThe full explanation and what to ask for · 1 min readExpand

Two rules: high-level modules should not depend on low-level modules, both should depend on abstractions; and abstractions should not depend on details. The inversion is in ownership: the interface lives with the policy that uses it, not with the database code.

Dependency inversion is not dependency injection. DI is a technique for supplying dependencies; you can inject a concrete DbInvoiceStore and still have the dependency pointing the wrong way.

Invert at boundaries that are volatile or slow to test: I/O, external services, time. Depending directly on stable framework and language types is fine.

Ask for it

  • abstraction
  • dependency injection
  • composition root
  • ports and adapters
  • inversion of control

Invert the dependency in InvoiceService: define an invoice store interface in the Application project, implement it with an ORM in Infrastructure, and wire it in the composition root through the DI container. Application must no longer reference Infrastructure.