LyraLearn AI Learning Platform
Exams
← Module 6 Β· Layered Architecture
🎧 Listen

Services and Interfaces

The service layer only pays off if services are replaceable and testable β€” which is what interfaces and dependency injection buy you. This lesson is the plumbing that makes the architecture from lesson 1 actually work.

Interface-first services

Every service gets an interface (IPermitService) and controllers depend on that, injected through the constructor:

public PermitsController(IPermitService permits)
{
    _permits = permits;
}

The controller no longer knows or cares how permits are approved β€” it can be unit-tested with a mock (Moq, NSubstitute), and the implementation can gain caching or logging without any caller changing. Keep interfaces honest: they should describe operations (ApprovePermit(id)) not leak persistence (GetQueryable()), and one interface per service is plenty β€” resist the urge to build deep hierarchies.

DI registration and lifetimes

Someone has to construct the object graph. ASP.NET Core's built-in DI container does it β€” you register in Program.cs:

builder.Services.AddScoped<IPermitService, PermitService>();
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(cs));

The three lifetimes you must be able to recite:

The classic bug is the captive dependency: a singleton service that takes a scoped DbContext in its constructor captures the first request's context forever β€” stale data and thread-safety crashes follow. Rule: a service's lifetime must be no longer than its dependencies'. The built-in container validates this in the Development environment β€” treat any scope-validation exception at startup as the bug it is, not as noise to suppress. Legacy note: MVC 5-era apps wired third-party containers (Unity, Autofac) through DependencyResolver β€” same concepts, but no captive-dependency validation, so review those registrations by hand.

Anti-patterns: service locators and static state

Two shortcuts sabotage the whole design. The service locator β€” HttpContext.RequestServices.GetService<IPermitService>() scattered through the code β€” hides dependencies from constructors, so nothing declares what it needs and tests can't substitute anything without global setup. If a class needs a service, ask for it in the constructor, period. Worse still is static mutable state: static DbContexts (not thread-safe β€” data corruption under load), static caches without eviction or locking, static "current user" holders that bleed across requests. Statics are invisible to the container, shared across all requests, and un-mockable. When you inherit them β€” and in older agency codebases you will β€” migrate the highest-traffic ones first, converting each into a registered singleton with an interface so it at least becomes visible, testable, and thread-reviewed.

🧠 Quiz yourself on this lesson →

Ask the AI Tutor

Grounded in the course lessons β€” it cites its sources and says when it doesn't know.