Design & Architecture

Inversion of Control & Dependencies

Intermediate

Code that creates its own dependencies is tied to them. You cannot replace, test, or reconfigure it without major changes. Inversion of control reverses this: a component states what it needs and is given it, so it depends on abstractions, not concrete types. The result is code that is testable, swappable, and clear about what it relies on.

IoC and dependency injection are tools, not goals. The goal is the Dependency Inversion Principle: high-level policy (your business logic) should not depend on low-level details (a specific database, HTTP client, or third-party SDK). Both should depend on an abstraction. When a service is given an interface instead of creating a concrete class itself, you can test it with a fake, swap the implementation, and see its dependencies in the constructor.

This matters most for things that change often or need isolating: databases, external providers (Veriff, Sumsub, Stripe), clocks, and randomness. Depending on an interface for these lets us test business and security logic in a repeatable way, and keep volatile integrations behind a layer we control.

Depend on abstractions

Hard-wired dependency public class Onboarding {
public Result Approve(Guid id) {
var screening = new VeriffClient(apiKey); // newed up inside
...
}
}

Onboarding is now tied to Veriff and a live API key. You cannot unit-test the approval logic without calling the real provider, and swapping or mocking it means editing this class.

Injected abstraction public class Onboarding(IScreeningProvider screening) {
public Result Approve(Guid id) { var r = screening.Check(id); ... }
}
// startup: services.AddScoped();

The approval rule depends only on an interface. Tests inject a fake to cover the fail-closed path, and the provider can be swapped in one place without touching the logic.

Keep the seams clean

Self-review checklist

Why it matters: Hard-wired dependencies make code untestable and rigid. The exact logic we most need to verify — fail-closed screening, authorization, money handling — becomes the hardest to test in isolation. Depending on abstractions and injecting them keeps that logic testable, the volatile integrations swappable, and the whole system clear about what it relies on.