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

πŸ“š Interview question

What is a dependency relationship between classes, and how is it different from association? Explain how dependencies appear in C# code, why direction matters (Dependency Inversion Principle), and how dependency injection manages them.

The 'uses' relationship: temporary dependencies versus stored associations, dependency direction and inversion, and DI in .NET.

Mid-Level Theory Intermediate coupling dependency dependency-injection dependency-inversion oop

🎯 Key points

  • A dependency is a temporary 'uses' relationship; association stores a reference
  • Dependencies appear as parameters, locals, return types, static calls and `new` inside methods
  • Hidden dependencies (statics, service locators, `new` of infrastructure) hurt testing and change
  • Dependency Inversion: policy and detail both depend on abstractions owned by the policy side
  • Dependency injection supplies dependencies from outside, usually via constructors
  • DI lifetimes: singleton, scoped, transient; never capture scoped services in singletons
  • Use `TimeProvider`, `IOptions<T>` and `IHttpClientFactory` instead of static environment access
  • Many constructor dependencies signal a class doing too much

⏱ 30-second 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.

πŸ—£ Full answer (senior)

How I would answer this in an interview

A dependency is the weakest relationship: class A uses class B to do its job β€” as a parameter, a local variable, a return type, a static call or an injected service β€” so a change to B can force a change to A. An association is stronger and structural: A holds a persistent link to B as part of its state. Then the senior part: the direction of dependencies determines what breaks when things change, which is what the Dependency Inversion Principle and DI are about.

1. Dependency vs association

// Dependency: uses a calculator temporarily (parameter); no lasting link
public decimal Total(Order order, ITaxCalculator tax) => order.Subtotal + tax.For(order);

// Association: Order structurally holds its customer reference as state
public sealed class Order { public CustomerId CustomerId { get; } }
Dependency Association
Meaning "uses" "knows/has"
Duration During a call (or as a collaborator) For the object's lifetime
UML Dashed arrow Solid line
Example IEmailSender used by a service, DateTime.Now called, File.ReadAllText used Order.Customer, Team.Players

Injected services stored in readonly fields are technically associations in UML terms, but in design discussions we call them dependencies because they're collaborators the class uses, not part of its domain state.

2. How dependencies appear in C#

  • Explicit: constructor parameters, method parameters, return types. Visible and replaceable.
  • Hidden: new SmtpClient() inside a method, static calls (DateTime.UtcNow, File.*, HttpContext.Current-style ambient context), service locator (provider.GetService<T>() deep in code), global singletons.

Hidden dependencies make code hard to test (you can't control time or the file system) and hard to reason about (the constructor lies about what the class needs).

3. Why direction matters: Dependency Inversion

In a naive layered design, high-level policy depends on low-level detail:

OrderService  --->  SqlOrderRepository  --->  SQL Server
(business)          (infrastructure)

Every infrastructure change (new database, new email provider) ripples into business code, and business code can't be tested without infrastructure.

The Dependency Inversion Principle: high-level modules shouldn't depend on low-level modules; both should depend on abstractions; abstractions shouldn't depend on details. Concretely, the abstraction is owned by the high-level module:

Application:     OrderService  --->  IOrderRepository (defined here)
                                          ^
Infrastructure:          SqlOrderRepository (implements it)

Source-code dependencies now point toward the business core (Clean/Onion/Hexagonal architecture). The domain and application layers compile without infrastructure; infrastructure plugs in.

4. How dependency injection manages them

DI is the mechanism that supplies dependencies from outside instead of the class creating them:

public sealed class OrderService(IOrderRepository orders, IPaymentGateway payments, TimeProvider clock) { ... }

builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddHttpClient<IPaymentGateway, StripePaymentGateway>();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddScoped<OrderService>();
  • The composition root (Program.cs) is the only place that knows concrete types.
  • The container manages lifetimes: transient, scoped (per request), singleton β€” and disposal.
  • Tests construct the class with fakes (FakeTimeProvider, in-memory repository).
  • Dependencies are visible in the constructor; too many parameters is a design smell (too many responsibilities).

5. A production example

A billing service called DateTime.Now in a dozen places and newed up an SmtpClient inside its methods. Month-end tests were impossible ("what happens on 31 March?") and a timezone bug β€” servers in UTC after migrating to Azure, invoices dated a day early for Australian customers β€” went unnoticed. We injected TimeProvider (with an explicit Australia/Sydney conversion) and an IEmailSender, which made date logic testable with FakeTimeProvider and surfaced the timezone bug in a unit test.

πŸ” What the interviewer is checking

Checks understanding of coupling, DIP and practical DI in .NET.

  • Green flags: distinguishes dependency from association, lists hidden dependencies, explains DIP with interface ownership, knows DI lifetimes and captive dependency, uses TimeProvider.
  • Red flags: equates DI with DIP, uses service locators, hard-codes DateTime.Now and new of infrastructure, or puts interfaces next to implementations in infrastructure.

πŸ“š Learn in depth

1. Types of coupling via dependencies

From loosest to tightest:

  1. Data coupling β€” passing simple values.
  2. Stamp coupling β€” passing a whole object when only a part is used.
  3. Control coupling β€” passing flags that tell the callee what to do.
  4. Common coupling β€” sharing global/static state.
  5. Content coupling β€” reaching into another class's internals.

Good design keeps dependencies at the data/stamp level, through narrow interfaces.

2. Stable Dependencies and Acyclic Dependencies

Robert C. Martin's package principles:

  • Acyclic Dependencies Principle: no cycles between components/projects. .NET enforces this at project level (project references can't be circular), but cycles between namespaces or folders inside one project can still exist.
  • Stable Dependencies Principle: depend in the direction of stability β€” volatile code (UI, infrastructure) depends on stable code (domain), not the reverse.
  • Stable Abstractions Principle: stable components should be abstract (interfaces, domain models) so they can be extended without modification.

Architecture tests (NetArchTest, ArchUnitNET) can enforce "Domain must not reference Infrastructure" in CI.

3. DI container mechanics in .NET

  • Registrations build a ServiceCollection of ServiceDescriptors; BuildServiceProvider creates resolvers (compiled expression or IL-emitted factories for speed).
  • Constructor selection: the constructor with the most resolvable parameters.
  • Scopes: ASP.NET Core creates one per request; scoped services are disposed at the end. Resolving scoped services from the root provider (e.g. in a singleton) is a captive dependency bug β€” ValidateScopes and ValidateOnBuild (default in Development) detect it.
  • .NET 8 added keyed services ([FromKeyedServices("stripe")]) for multiple implementations of one interface.
  • Avoid the service locator anti-pattern (IServiceProvider injected everywhere): it hides dependencies and defers failures to run time.

4. Dependencies on time, randomness and environment

Common hidden dependencies and their abstractions:

Hidden dependency Abstraction
DateTime.Now/UtcNow, Task.Delay, timers TimeProvider (.NET 8), FakeTimeProvider in tests
Guid.NewGuid() for IDs in logic an ID generator interface when determinism matters
Random inject Random or a seed
File system IFileProvider, System.IO.Abstractions
Environment variables / config IOptions<T>
HttpClient created per call IHttpClientFactory / typed clients

5. Dependencies in UML and code metrics

UML shows dependencies as dashed arrows with stereotypes (Β«useΒ», Β«createΒ», Β«callΒ»). Visual Studio's code metrics report class coupling (number of unique types a class depends on); values above ~30 for a class are a smell. NDepend and Rider's dependency diagrams visualise project and namespace dependencies.

6. When not to invert

You don't need an interface for every dependency. Stable, pure dependencies (the BCL, Money value objects, domain services without I/O) can be used directly. Invert dependencies at volatility and I/O boundaries: databases, external APIs, messaging, time, file system.

Try it yourself

  1. Search your codebase for DateTime.Now, new HttpClient, File. and static service calls; list hidden dependencies.
  2. Add NetArchTest rules ensuring the domain project doesn't reference EF Core or ASP.NET Core.
  3. Enable ValidateOnBuild and fix any captive dependencies.
  4. Replace DateTime.UtcNow with TimeProvider in one service and write a month-end test.

πŸ’» Code

// Application layer owns the abstractions (dependency inversion)
public interface IInvoiceRepository
{
    Task<IReadOnlyList<Invoice>> GetDueAsync(DateOnly asOf, CancellationToken ct);
}

public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body, CancellationToken ct);
}

// Policy class: all dependencies explicit, none created inside
public sealed class InvoiceRunner(IInvoiceRepository invoices, IEmailSender email, TimeProvider clock)
{
    public async Task<int> RunAsync(CancellationToken ct)
    {
        var today = DateOnly.FromDateTime(clock.GetLocalNow().DateTime);   // no DateTime.Now
        int sent = 0;
        foreach (var invoice in await invoices.GetDueAsync(today, ct))
        {
            await email.SendAsync(invoice.Email, $"Invoice {invoice.Number}", Format(invoice), ct);
            sent++;
        }
        return sent;
    }

    private static string Format(Invoice i) => $"Amount due: {i.Amount:C}";   // pure helper: fine as static
}

public sealed record Invoice(string Number, string Email, decimal Amount);

// Infrastructure implements the abstraction
public sealed class SqlInvoiceRepository(AppDb db) : IInvoiceRepository
{
    public async Task<IReadOnlyList<Invoice>> GetDueAsync(DateOnly asOf, CancellationToken ct) =>
        await db.Invoices.Where(i => i.DueDate <= asOf)
                         .Select(i => new Invoice(i.Number, i.Email, i.Amount))
                         .ToListAsync(ct);
}

// Composition root
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddScoped<IInvoiceRepository, SqlInvoiceRepository>();
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<InvoiceRunner>();

// Test: control time without waiting for month end
// var clock = new FakeTimeProvider(new DateTimeOffset(2026, 10, 31, 9, 0, 0, TimeSpan.FromHours(11)));

⚠ Common mistakes

1. Hidden dependencies

What happens: classes call static APIs, new up services, or use a service locator internally. Why it is wrong: constructors lie about requirements; code is hard to test and change. Do instead: inject dependencies explicitly through constructors.

2. Interfaces owned by the implementation

What happens: IOrderRepository lives in the infrastructure project next to SqlOrderRepository. Why it is wrong: the application still depends on infrastructure; nothing is inverted. Do instead: define the interface in the consuming (application/domain) layer.

3. Injecting IServiceProvider everywhere

What happens: classes resolve what they need at run time. Why it is wrong: service locator hides dependencies and moves errors to run time. Do instead: inject specific dependencies; use factories only where creation must be deferred.

4. Captive dependencies

What happens: singletons depend on scoped services such as DbContext. Why it is wrong: scoped instances live forever and are shared across requests. Do instead: match lifetimes or use IServiceScopeFactory in singletons.

5. Constructor over-injection

What happens: classes with 10+ constructor dependencies. Why it is wrong: indicates too many responsibilities. Do instead: split the class or group cohesive dependencies behind a facade.

6. Abstracting stable, pure dependencies

What happens: interfaces wrapping Math, string helpers or value objects. Why it is wrong: indirection without benefit. Do instead: invert only volatile/I/O dependencies.

7. Circular dependencies between classes or namespaces

What happens: OrderService depends on InvoiceService, which depends on OrderService. Why it is wrong: tight coupling, DI resolution failures and hard-to-change code. Do instead: extract shared logic, use events, or restructure responsibilities.

8. Depending on time and environment directly

What happens: business rules use DateTime.Now and machine time zones. Why it is wrong: untestable date logic and time-zone bugs after cloud migrations. Do instead: inject TimeProvider and convert explicitly to the business time zone.

πŸ› Senior insight: trade-offs

Seniors manage dependencies as the main driver of coupling and architecture.

Practice Effect
Interfaces owned by the consumer dependencies point inward
Explicit constructor dependencies visible, testable design
TimeProvider, IOptions<T>, IHttpClientFactory no hidden environment coupling
Scope validation and ValidateOnBuild lifetime bugs caught at startup
Architecture tests (NetArchTest, ArchUnitNET) layering enforced in CI
Watch constructor size early signal of SRP violations

They also avoid over-abstracting stable dependencies: string, List<T> or a pure static helper are dependencies too, and wrapping them adds no value.

πŸ”— Follow-ups the interviewer may ask

  1. When: Candidate contrasts dependency with association Why: Checks association multiplicity and direction
    How to answer

    Association is a lasting structural relationship where one object knows about another and can reach it, usually through a field or property: a Customer places Orders, a Teacher teaches Courses. It is the general relationship; aggregation and composition are stronger special cases that add ownership rules, and dependency is a weaker, temporary use. An association has multiplicity (one-to-one, one-to-many, many-to-many) and direction: unidirectional (Order.Customer only) or bidirectional (Customer.Orders and Order.Customer). In C# it appears as a reference property, a collection, or just an ID; in EF Core as navigation properties and foreign keys. Prefer unidirectional associations and IDs across aggregate boundaries; bidirectional links must be kept in sync and create cycles for serialisers.

  2. When: Candidate says depend on abstractions Why: Probes what makes a good abstraction
    How to answer

    Abstraction means showing only what a caller needs and hiding how it is done: an IPaymentGateway.ChargeAsync(amount) hides HTTP calls, retries and provider formats. Encapsulation protects an object's state and invariants; abstraction simplifies how others think about and use something. In C# you express abstractions with interfaces, abstract classes and well-named public methods. A good abstraction is named in the caller's terms, small, stable, and has more than one real reason to exist. A leaky abstraction forces callers to know the hidden details (provider-specific exceptions, SQL in a repository method name). A premature abstraction is an interface invented for one implementation that never changes, adding indirection without value.

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

Invoices sent a day early because of a hidden DateTime.Now dependency
Situation
A monthly billing job decided which invoices were due using `DateTime.Now` and sent them with `new SmtpClient()` created inside the method.
Signals
After a hosting change, invoices went out a day early for [Australian] customers; there were no tests for month-end logic because they could only pass on the last day of a month.
Investigation
The new servers ran in UTC. `DateTime.Now` returned the previous or next day relative to the business time zone near midnight. The hidden static dependency made this invisible in code review.
Resolution
Injected `TimeProvider` and an `IEmailSender`, converted to the business time zone explicitly, and wrote tests with `FakeTimeProvider` for month boundaries and daylight saving changes.
Prevention
Analyzer rule banning `DateTime.Now` and `DateTime.UtcNow` outside the composition root, plus a test suite for date boundaries.

How to tell it (STAR)

**Situation:** At [company] after a hosting move, our billing job sent invoices a day early to [Australian] customers. **Task:** I was asked to fix it and make sure date logic could be tested. **Action:** The job used `DateTime.Now`, a hidden dependency on the server's time zone, which was now UTC. I injected `TimeProvider`, made the business time zone explicit, and replaced the inline `SmtpClient` with an injected sender. Then I wrote tests that set the clock to month ends and daylight saving changes. A banned-API analyzer now blocks `DateTime.Now` in business code. **Result:** Invoices went out on the right day, we got [N] new boundary tests that run on any day, and [one more] time zone bug was caught by the analyzer before release.

↩ Usually asked after

More in Core OOP

Last updated 30 Oct 2026.

🌐 Sangam's Webring

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