📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

πŸ“š Interview question

What is the Dependency Inversion Principle, how is it different from dependency injection, and how do you apply it in a layered .NET application?

High-level policy not depending on low-level details, abstraction ownership, DIP versus DI versus IoC containers, ports and adapters, and when not to invert.

Senior Architecture Advanced clean-architecture dependency-injection dip solid

🎯 Key points

  • High-level policy should not depend on low-level details; both depend on abstractions
  • The consuming (high-level) layer owns the abstraction
  • DI is a technique; the container is a tool; neither guarantees DIP
  • Ports in the application layer, adapters in infrastructure, wiring in the host
  • Abstractions should be in business terms, not vendor shapes
  • Do not invert stable dependencies such as primitives, ILogger<T> or TimeProvider
  • Enforce with project references and architecture tests

⏱ 30-second answer

DIP says high-level policy should not depend on low-level details; both should depend on abstractions, and the abstraction belongs to the high-level side. For example, the domain or application layer defines IPaymentGateway, and the infrastructure project implements it with Stripe; the arrow of source dependency points inwards. Dependency injection is a technique for supplying those implementations (constructor injection), and the .NET DI container is a tool that automates it. You can use a container without inverting anything (if interfaces live next to their implementations in infrastructure), and you can invert dependencies without a container.

πŸ—£ Full answer (senior)

How I would answer this in an interview

"The Dependency Inversion Principle has two parts. High-level policy shouldn't depend on low-level details: both should depend on abstractions. And abstractions shouldn't depend on details: details depend on abstractions. The part people miss is ownership: the abstraction belongs to the consumer, the high-level side. So the application layer defines IPaymentGateway in business terms, the infrastructure project references the application project and implements it with the vendor SDK, and the host wires them together. The source-code arrow points inwards, against the flow of control: that's the 'inversion'. Dependency injection is just the technique of handing a class its dependencies (usually through the constructor), and the .NET DI container is a tool that automates building the object graph. Neither guarantees DIP: you can register IStripeClient from the infrastructure project in a container and still have your domain depend on Stripe concepts. I invert dependencies on volatile details (vendors, I/O, messaging) and leave stable ones alone (string, collections, ILogger<T>, TimeProvider), and I enforce the direction with project references and architecture tests."


1. Three related ideas

Term Kind What it is Guarantees DIP?
Dependency Inversion Principle Design rule Direction of source dependencies; abstraction ownership β€”
Dependency injection Technique Pass dependencies in (constructor/method/property) instead of new-ing them No
IoC container Tool Microsoft.Extensions.DependencyInjection builds object graphs, manages lifetimes No
Inversion of Control Broader idea Framework calls your code (e.g. ASP.NET Core calling controllers) No

2. Applying DIP in a layered .NET solution

Project Contains References
Shop.Domain Entities, value objects, domain rules Nothing infrastructure
Shop.Application Use cases/handlers, ports (IPaymentGateway, IOrderRepository, IEmailSender) Domain
Shop.Infrastructure Adapters: EF Core, vendor SDKs, HTTP clients, message bus Application (+ Domain)
Shop.Api (host) Program.cs composition root, endpoints Application + Infrastructure

This is the core of ports-and-adapters, onion and clean architecture.

3. Ownership: the detail that makes it work

Abstraction Owned by Shape DIP?
IStripeChargesApi in Infrastructure Detail Vendor-shaped (CreateCharge(stripeToken, ...)) No: app still thinks in Stripe terms
IPaymentGateway in Application Consumer Business-shaped (ChargeAsync(orderId, amount)) Yes
IHttpClientWrapper Nobody useful Too low-level No real decoupling
IExchangeRates Consumer Business capability Yes

4. When not to invert

Dependency Invert? Why
Payment, email, SMS, storage vendors Yes Volatile; you'll switch or test without them
Message broker Usually Keeps handlers testable and portable
Database Often, or use DbContext directly in simple apps DbContext is already an abstraction; wrapping it can add little
Clock Use TimeProvider Already an abstraction (.NET 8)
Logging ILogger<T> Already an abstraction
string, List<T>, LINQ No Stable; inverting adds noise
Pure functions No No I/O to decouple

5. Enforcing it

  • Project references: Application must not reference Infrastructure or vendor packages.
  • Architecture tests (NetArchTest, ArchUnitNET) in CI fail the build on forbidden dependencies.
  • Composition root only in the host: no service-locator calls in application code.

Production example

Our [retail platform's] domain project referenced the payment vendor's SDK directly: vendor types appeared in [140] files across domain and application code, and domain unit tests needed the SDK plus network stubs. When the business decided to switch vendors, the estimate was [six weeks]. I defined an application-owned IPaymentGateway port in business terms (ChargeAsync(orderId, amount, ct) returning our own PaymentResult), moved all vendor code into an infrastructure adapter registered via AddHttpClient<IPaymentGateway, VendorGateway>, and added NetArchTest rules so the application layer can't reference infrastructure or vendor packages. The vendor migration itself then touched only the new adapter and one registration line, domain tests became fast and dependency-free, and the architecture tests have kept the boundary intact since.

πŸ” What the interviewer is checking

Checks architectural understanding beyond DI mechanics.

Green flags

  • Distinguishes DIP, DI and containers clearly.
  • Explains abstraction ownership and dependency direction.
  • Describes a layered or ports-and-adapters structure in .NET.
  • Knows when not to invert.

Red flags

  • "DIP means everything has an interface."
  • Interfaces defined alongside implementations with vendor shapes.

πŸ“š Learn in depth

1. The "inversion", visually

Flow of control (runtime):   PlaceOrderHandler  ──calls──▢  VendorGateway  ──▢  vendor API

Without DIP (source deps):   Application  ──▢  Infrastructure  ──▢  Vendor SDK

With DIP (source deps):      Application (owns IPaymentGateway)  ◀──  Infrastructure (VendorGateway : IPaymentGateway)
                             Host references both and composes them

Control still flows outward at runtime; source dependencies now point inward. That's the inversion.

2. Origin

Robert C. Martin (1996) stated it as: (A) high-level modules should not depend on low-level modules; both should depend on abstractions; (B) abstractions should not depend on details; details should depend on abstractions. The "plug-in" framing in Clean Architecture (the database and web are plug-ins to the business rules) is the same idea at system level.

3. Composition root

// Shop.Api/Program.cs: the only place that knows concrete types
builder.Services.AddScoped<PlaceOrderHandler>();
builder.Services.AddHttpClient<IPaymentGateway, VendorGateway>(c =>
    c.BaseAddress = new Uri(builder.Configuration["Payments:BaseUrl"]!));
builder.Services.AddDbContext<ShopDb>(o => o.UseSqlServer(builder.Configuration.GetConnectionString("Shop")));
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();

Application code never calls IServiceProvider.GetService (service locator): dependencies arrive through constructors, so they're visible and testable.

4. Architecture tests

[Fact]
public void Application_does_not_depend_on_infrastructure()
{
    var result = Types.InAssembly(typeof(PlaceOrderHandler).Assembly)
        .ShouldNot().HaveDependencyOnAny("Shop.Infrastructure", "Stripe", "Microsoft.EntityFrameworkCore")
        .GetResult();
    Assert.True(result.IsSuccessful, string.Join(", ", result.FailingTypeNames ?? []));
}

(Using NetArchTest; ArchUnitNET offers similar rules.)

5. Lifetimes still matter

Inverting doesn't remove DI lifetime bugs:

  • A singleton depending on a scoped service (e.g. DbContext) is a captive dependency: stale data and thread-safety bugs.
  • ValidateScopes and ValidateOnBuild (on by default in Development) catch many of these at startup.
  • Typed HttpClients are transient by design; don't capture them in singletons.

6. Testing benefits

Layer Test with
Application/domain Simple fakes of ports (in-memory IPaymentGateway)
Adapters Integration tests against real dependencies (Testcontainers, WireMock.Net for HTTP vendors)
Host A few end-to-end tests via WebApplicationFactory

7. Pitfalls of over-inversion

  • IDateTimeWrapper, IStringHelper, IListFactory: interfaces for stable things.
  • A generic repository over DbContext that exposes IQueryable everywhere: the abstraction doesn't hide anything.
  • Ports that mirror vendor APIs one-to-one: the vendor's model leaks anyway.

8. Try it yourself

  1. Create App, Infra and Host projects; put IPaymentGateway in App, implement it in Infra, wire it in Host.
  2. Add the architecture test above and try adding an Infra reference to App to see it fail.
  3. Write an application test with an in-memory fake gateway.
  4. Write an adapter test with WireMock.Net simulating the vendor.
  5. Register a scoped service into a singleton and observe ValidateScopes catching it.

πŸ’» Code

In a real solution each block below is its own project. They are shown in one snippet with block-scoped namespaces so it compiles as a single file (two file-scoped namespace X; declarations in one file are a compile error).

using System.Net.Http.Json;

// ---- Application project (owns the port) ----
namespace Shop.Application
{
    public sealed record PaymentResult(bool Success, string Reference);

    public interface IPaymentGateway
    {
        Task<PaymentResult> ChargeAsync(Guid orderId, decimal amountAud, CancellationToken ct);
    }

    public sealed class PlaceOrderHandler(IPaymentGateway payments)
    {
        public async Task<string> HandleAsync(Guid orderId, decimal total, CancellationToken ct)
        {
            var result = await payments.ChargeAsync(orderId, total, ct);
            return result.Success ? $"Paid ({result.Reference})" : "Payment declined";
        }
    }
}

// ---- Infrastructure project (adapter, references Application) ----
namespace Shop.Infrastructure
{
    using Shop.Application;

    public sealed class CardVendorGateway(HttpClient http) : IPaymentGateway
    {
        public async Task<PaymentResult> ChargeAsync(Guid orderId, decimal amountAud, CancellationToken ct)
        {
            // Vendor-specific request and mapping live only here.
            using var response = await http.PostAsJsonAsync("charges",
                new { reference = orderId, amount_cents = (int)(amountAud * 100), currency = "aud" }, ct);
            return new PaymentResult(response.IsSuccessStatusCode, orderId.ToString("N"));
        }
    }
}

// ---- Host project (Program.cs wires them together) ----
// builder.Services.AddScoped<PlaceOrderHandler>();
// builder.Services.AddHttpClient<IPaymentGateway, CardVendorGateway>(c =>
//     c.BaseAddress = new Uri(builder.Configuration["Payments:BaseUrl"]!));

⚠ Common mistakes

1. Equating DIP with "we use a DI container"

What happens: everything is registered in Program.cs, but application code depends on infrastructure types. Why it is wrong: a container wires dependencies in any direction. Do instead: check source dependency direction and abstraction ownership.

2. Interfaces in the infrastructure project, shaped like the vendor

What happens: IStripeClient with Stripe-specific methods used by the application. Why it is wrong: the application still depends on vendor concepts. Do instead: ports defined by the consuming layer in business terms.

3. Interfaces for everything

What happens: wrappers around DateTime, collections, string, framework types. Why it is wrong: indirection with no decoupling benefit. Do instead: invert volatile details (I/O, vendors); use built-in abstractions (TimeProvider, ILogger<T>).

4. Service locator in application code

What happens: serviceProvider.GetRequiredService<IPaymentGateway>() inside handlers. Why it is wrong: hides dependencies, complicates tests, couples to the container. Do instead: constructor injection; resolve only in the composition root.

5. Ignoring lifetimes

What happens: singleton handlers capturing scoped DbContexts. Why it is wrong: stale data, thread-safety bugs. Do instead: align lifetimes; keep scope validation on.

6. Leaking vendor types through the port

What happens: Task<Stripe.Charge> ChargeAsync(...). Why it is wrong: consumers depend on vendor types anyway. Do instead: map to your own result types inside the adapter.

7. No enforcement

What happens: boundaries erode as developers add "just one" reference. Why it is wrong: DIP becomes folklore. Do instead: project reference rules and architecture tests in CI.

8. Wrapping DbContext in a thin generic repository by default

What happens: IRepository<T> exposing IQueryable<T> over EF Core. Why it is wrong: little isolation, more code; persistence concerns still leak. Do instead: use DbContext directly in simple apps, or use-case-specific repository ports where isolation matters.

9. Ports that are too low-level

What happens: IHttpClientWrapper, ISqlExecutor. Why it is wrong: the application still orchestrates infrastructure details. Do instead: ports at the level of business capability (IExchangeRates, IOrderHistory).

10. Confusing DIP with "depend on interfaces, not classes"

What happens: every class gets an I-prefixed twin in the same project. Why it is wrong: no change in dependency direction; doubled code. Do instead: introduce abstractions at architectural boundaries, owned by the consumer.

πŸ› Senior insight: trade-offs

DIP is the principle that makes architecture boundaries real. I enforce it with project references and architecture tests, keep ports in business language, and avoid inverting stable dependencies just to have an interface.

Dependency Invert?
Payment, email, SMS vendors Yes, port in application layer
Database Often; or use DbContext directly in simple apps
Clock Use TimeProvider (already abstract)
Logging ILogger<T> (already abstract)
Collections, primitives No

πŸ”— Follow-ups the interviewer may ask

  1. When: Candidate talks about constructor injection Why: Checks DI mechanics and lifetimes
    How to answer

    A dependency is the weakest relationship: class A uses class B, so a change to B may affect A, but A does not keep B as part of its state. It shows up as a method parameter, a local variable, a return type, a static call or a new B() inside a method. Association is stronger: A stores a reference to B. Direction matters: high-level policy (business rules) should not depend on low-level details (SQL, HTTP). The Dependency Inversion Principle says both should depend on abstractions owned by the high-level side. Dependency injection supplies those abstractions from outside (usually through constructors), so classes do not create or find their own dependencies, which makes them testable and replaceable.

  2. When: Candidate designs small consumer-owned ports Why: Links DIP to interface segregation
    How to answer

    ISP says clients should not be forced to depend on methods they do not use. A fat interface such as IOrderService with 25 members couples every consumer to all of them, makes test doubles painful and pushes implementers to throw NotImplementedException. Split interfaces by what clients need (role interfaces): IOrderReader for queries, IOrderWriter for commands, IClock instead of a giant IUtilities. The BCL does this well (IReadOnlyCollection<T> versus ICollection<T>, IAsyncDisposable). Do not go to the extreme of one interface per method; group members that the same client uses together.

  3. When: Candidate swaps adapters without changing the application Why: Connects DIP to extension without modification
    How to answer

    The Open/Closed Principle says modules should be open for extension but closed for modification: you add new behaviour by adding code, not by editing tested code. In C# this usually means a strategy interface with one implementation per variant, registered in DI, so adding a new payment method is a new class rather than another case in a growing switch. I apply it where change actually happens repeatedly. A small, stable switch (or a switch expression over a closed set) is fine; building extension points for variants that never arrive is speculative generality.

πŸ›  Real-world problems: how you handle them

Vendor switch touching 140 files
Situation
A [retail platform's] domain project referenced the payment vendor SDK directly.
Signals
A vendor change estimate of [six weeks]; domain unit tests required the SDK and network stubs.
Investigation
Dependency analysis showed vendor types used in [140] files across domain and application layers.
Resolution
Defined an application-owned `IPaymentGateway` port, moved vendor code into an infrastructure adapter, and added NetArchTest rules.
Prevention
Architecture tests in CI prevent the application layer from referencing infrastructure or vendor packages.

How to tell it (STAR)

When we had to switch [payment vendors], the change touched 140 files because the vendor SDK was used everywhere. I introduced a payment port owned by the application layer and moved the vendor code into one adapter, with architecture tests to keep it that way. The next vendor change only touched that adapter and one registration line.

↩ Usually asked after

More in SOLID Principles

Last updated 31 Oct 2026.

🌐 Sangam's Webring

βͺ Previous 🏠 Main Next ⏩
πŸ’¬ Chat with me