Waqas Ahmad — Software Architect & Technical Consultant - Available USA, Europe, Global

Waqas Ahmad — Software Architect & Technical Consultant

Specializing in

Distributed Systems

.NET ArchitectureCloud-Native ArchitectureAzure Cloud EngineeringAPI ArchitectureMicroservices ArchitectureEvent-Driven ArchitectureDatabase Design & Optimization

👋 Hi, I'm Waqas — a Software Architect and Technical Consultant specializing in .NET, Azure, microservices, and API-first system design..
I help companies build reliable, maintainable, and high-performance backend platforms that scale.

Experienced across engineering ecosystems shaped by Microsoft, the Cloud Native Computing Foundation, and the Apache Software Foundation.

Available for remote consulting (USA, Europe, Global) — flexible across EST, PST, GMT & CET.

services
Article

Clean Architecture with .NET: Layers, Dependency Rule, and Structure

Clean Architecture with .NET: layers, dependency rule, and where business logic lives.

services
Read the article

Introduction

This guidance is relevant when the topic of this article applies to your system or design choices; it breaks down when constraints or context differ. I’ve applied it in real projects and refined the takeaways over time (as of 2026).

Business logic mixed with databases and UI becomes hard to test and change when you swap persistence or add a new frontend. Clean Architecture (Uncle Bob) structures the app so domain and use cases stay at the centre and infrastructure and presentation depend inward—layers, dependency rule, and where DI and business logic live. For architects and tech leads, keeping the dependency rule clear keeps core logic testable and portable and avoids costly rework when requirements or technology change.

If you are new: start with Topics covered and Clean Architecture at a glance. This article explains what each layer is in plain language, where DI, services, providers, and business logic live, full .NET Core solution and code examples, whether to keep the frontend in the same solution or separate, and FAQs.

For a deeper overview of this topic, explore the full .NET Architecture guide.

Decision Context

  • System scale: Typically one or a few applications (monolith or bounded-context microservices); the approach applies when you have non-trivial business logic and expect to change persistence, UI, or integrations over time. Overkill for tiny CRUD apps or throwaway prototypes.
  • Team size: One to several teams; someone must own the dependency rule and layer boundaries. Works when at least one person can enforce “domain doesn’t reference EF” and review cross-layer violations.
  • Time / budget pressure: Fits greenfield and incremental refactors; breaks down when you have a one-week deadline and no time for interfaces and layers—then a flatter structure may be pragmatic short-term.
  • Technical constraints: .NET and related stack; DI and interfaces are first-class. Assumes you can separate Domain/Application/Infrastructure/Presentation; legacy monoliths with everything in one project need a phased split.
  • Non-goals: This article does not optimize for minimal file count, for “no abstractions,” or for apps that will never swap DB or UI; it optimizes for testability, portability, and clear ownership of business rules.

What is Clean Architecture and why it matters

Clean Architecture is a set of layers and a dependency rule:

  • Layers: Domain (centre), Application (use cases), Infrastructure (database, external APIs, files), Presentation (API, UI).
  • Dependency rule: Dependencies point inward. Domain has no references to anything. Application depends only on Domain and defines interfaces (e.g. IOrderRepository) that outer layers implement. Infrastructure and Presentation depend on Application (and Domain).

Why it matters: Your business rules and use cases stay in one place. You can unit test them with mocks (no database, no HTTP). You can swap SQL for a NoSQL store, or add a mobile app, without rewriting the core. New team members can understand “domain here, use cases here, database there.”


Clean Architecture at a glance

Layer What it is (plain English) What lives here Dependencies
Domain The heart: entities and business rules Entities, value objects, domain logic None
Application Use cases: “what the app does” Use cases, commands, interfaces (e.g. IOrderRepository) Domain only
Infrastructure “How” we do things (DB, email, APIs) Repository implementations, EF Core, HTTP clients, file I/O Application + Domain
Presentation Entry points (API, UI) Controllers, views, DI registration for Infrastructure Application + Infrastructure

The dependency rule

Inner layers do not know about outer layers.

  • Domain does not reference Application, Infrastructure, or Presentation. No using for EF Core, no HTTP, no “controller”.
  • Application does not reference Infrastructure or Presentation. It defines interfaces (e.g. IOrderRepository, IEmailSender); Infrastructure implements them.
  • Infrastructure and Presentation reference Application (and Domain) so they can call use cases and implement interfaces.

So: business logic and use cases never depend on “where data is stored” or “how the user hits the app”. That’s what makes the core testable and stable.


Layer diagram: dependencies point inward

The diagram below shows layers and dependency direction: API and Infrastructure depend on Application; Application depends on Domain. Nothing points outward.

Loading diagram…

Layers explained in human terms

Each layer has a single responsibility: Domain holds entities and rules, Application holds use cases and interfaces, Infrastructure implements them, and Presentation is the entry point and DI composition root.

Domain (centre)

What it is: The core of your product. “What is an Order? What is a User? What rules must always be true?”

What lives here: Entities (e.g. Order, OrderItem), value objects (e.g. Money, Email), and domain logic (e.g. “order total must be positive”). No database, no HTTP, no UI. Just C# classes and rules.

Who uses it: Application (use cases) and Infrastructure (repositories work with Order etc.).

Application (use cases)

What it is: The actions your app supports. “Place order”, “Get order by ID”, “Send notification.”

What lives here: Use cases (or “application services”), commands/queries, and interfaces that describe “what we need from the outside” without saying how: IOrderRepository, IEmailSender, ICurrentUserProvider. Implementations live in Infrastructure.

Who uses it: Presentation (controllers call use cases); Infrastructure implements the interfaces.

Infrastructure (“how” we do things)

What it is: Concrete implementations of persistence, external APIs, file storage, messaging.

What lives here: Repository implementations (e.g. OrderRepository using EF Core), HTTP clients, email senders, file providers. These classes implement the interfaces defined in Application. Assets (e.g. config files, certificates) are often read here; paths or URLs can be injected via options.

Who uses it: Presentation (registers them in DI and uses use cases that depend on these interfaces).

Presentation (entry points)

What it is: Where requests enter your app: REST API controllers, Blazor pages, or a minimal API.

What lives here: Controllers, routes, request/response DTOs, and DI setup (Program.cs or Startup.cs): registering use cases and Infrastructure implementations (e.g. AddScoped<IOrderRepository, OrderRepository>).

Who uses it: Users and clients (HTTP, browser).


Where things live: DI, services, providers, business logic, assets

Concept Where it lives Explanation
Business logic Domain (rules on entities) and Application (orchestration in use cases) Invariants and calculations in Domain; “workflow” (load entity, validate, save, notify) in Application.
DI (dependency injection) Presentation (composition root) Program.cs (or Startup.cs) registers interfaces and implementations: AddScoped<IOrderRepository, OrderRepository>, AddScoped<PlaceOrderUseCase>. Application and Domain have no DI container references; they just receive interfaces via constructors.
DI folder Presentation (e.g. MyApp.Api/Extensions/ or DependencyInjection/) Keep Program.cs thin: put registration in extension methods (e.g. AddApplicationServices(this IServiceCollection), AddInfrastructureServices(this IServiceCollection, IConfiguration)) in a dedicated folder. Program.cs then calls builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices(builder.Configuration);. All wiring lives here; no business logic.
Services Application (interfaces) + Infrastructure (implementations) “Service” often means “thing you inject”: e.g. IOrderRepository, IEmailSender. Interface in Application, implementation in Infrastructure. Use cases are also “application services”.
Providers Application (interface) + Infrastructure (implementation) Same idea: e.g. ICurrentUserProvider, IDateTimeProvider. Interface in Application so use cases can be tested with a fake; implementation in Infrastructure (reads from HTTP context, system clock, etc.).
Assets (config, certs, static files) Infrastructure or Presentation Config files, certificates, and static assets are read by Infrastructure or Presentation. Paths/URLs can be provided via options (e.g. IOptions<StorageOptions>) injected into Infrastructure. Domain and Application stay asset-agnostic.

Other components: where they go

Component Where it lives Notes
Middleware (auth, logging, CORS) Presentation (or Infrastructure/Middleware if reusable) Registered in Program.cs or in your DI extension.
Filters (action, exception) Presentation Controllers/Filters/ or Filters/; part of the HTTP pipeline.
Options (strongly typed config) Presentation (binding) + Infrastructure (consumption) Bind in Presentation; inject IOptions<T> in Infrastructure.
Health checks Presentation (registration) + Infrastructure (implementations) AddHealthChecks() in Presentation; DbHealthCheck etc. in Infrastructure.
Background services (hosted services) Application (interface) + Infrastructure (implementation) or a Worker project If large, use a separate MyApp.Worker project.
Mappers (entity ↔ DTO) Application or Presentation Application/Mapping/ or Presentation/Mapping/; keep Domain free of DTOs.

Why DI lives in the API (Presentation) — and what if there’s no API?

Short answer: DI registration (the composition root) lives in whatever project starts the app — the entry-point project. In a typical web solution that’s the API project; in a console app it’s the Console project; in a worker it’s the Worker project; in Blazor it’s the Blazor project. So it’s not “API” per se — it’s the entry point. Clean Architecture calls that the Presentation layer: the layer that hosts the entry point and wires everything. If you have no API, you still have an entry point (Console, Worker, Blazor, tests), and that’s where DI lives.

The API (when you have one) is your composition root: the one place where the app is “wired up.” Someone has to say “when someone asks for IOrderRepository, give them OrderRepository.” That someone is the entry-point project — in a typical .NET solution, the API (or the project that hosts the API). So:

  • DI registration (e.g. AddScoped<IOrderRepository, OrderRepository>) lives in Presentation = the API project (in Program.cs or in Extensions/ServiceCollectionExtensions.cs).
  • Domain and Application have no reference to the DI container; they only receive interfaces via constructors. So the “where” of registration is always the outer layer that starts the app — the API.

Why not Application? Application defines what we need (interfaces like IOrderRepository) and what the app does (use cases). It does not decide which implementation to use. If Application said “when someone asks for IOrderRepository, give OrderRepository,” then Application would have to reference Infrastructure (where OrderRepository lives). That would break the dependency rule: Application would depend on an outer layer. So Application stays pure: it only defines interfaces and use cases; it never references the container or concrete implementations. Who decides “give OrderRepository for IOrderRepository”? The place that starts the app — the entry-point project. So DI registration lives there, not in Application.

Why not Infrastructure? Infrastructure has the implementations (OrderRepository, EmailSender). But Infrastructure doesn’t “start” the app. The caller — the project that runs first (API, Console, Worker) — is the one that says “wire IOrderRepository to OrderRepository.” So the decision of “when someone asks for this interface, give this implementation” belongs in the place that composes the app: the entry point. You can still put extension methods in Infrastructure (e.g. AddInfrastructureServices(this IServiceCollection)) that contain the registration code, and call them from the entry point’s Program.cs. The composition root is still the entry point — it’s the one that invokes the wiring. So: DI registration is invoked from the entry-point project (API, Console, Worker, Blazor). That’s why we say “DI lives in the API” when we have an API — the API is the entry point.

What if there’s no API? Then the entry point is something else, and that’s where DI lives:

You have… Entry-point project Where DI lives
Web API MyApp.Api MyApp.Api/Program.cs or Extensions/
Console app MyApp.Console MyApp.Console/Program.cs
Worker (background service) MyApp.Worker MyApp.Worker/Program.cs
Blazor Server MyApp.Blazor MyApp.Blazor/Program.cs
gRPC service MyApp.Grpc MyApp.Grpc/Program.cs
Unit tests Test project Test setup (e.g. new ServiceCollection().AddApplicationServices().AddTestDoubles())

So Presentation in Clean Architecture = the layer that contains the entry point. When we say “DI lives in the API,” we mean “DI lives in the entry-point project”; in a web app that’s usually the API. If there’s no API, it’s whatever project starts the app — and that’s still Presentation. Summary: DI lives in the composition root = the entry-point project. Not in Domain or Application (they don’t reference the container). Infrastructure can hold extension methods that are called from the entry point. So “where does DI go?” → the project that runs first (API, Console, Worker, Blazor, etc.).


The most important idea: one rule that fixes most confusion

The one rule: If it knows about the database, HTTP, a framework (EF Core, AspNetCore), or the file system, it does not go in Domain. If it defines what we need (an interface), it goes in Application. If it implements how we do it (DB, email, HTTP), it goes in Infrastructure. If it’s the entry point (controller, Program.cs, DI registration), it goes in Presentation (API).

So when you’re unsure: “Does this class reference EF Core, SQL, HttpClient, or [ApiController]?” → If yes, it’s not Domain. Is it the contract (interface)? → Application. Is it the implementation of that contract? → Infrastructure. Is it where requests come in or where we register services? → Presentation (API). That single rule resolves most “where does this go?” questions.


How to decide where to put what

People often struggle with: “Is this Domain or Application?”, “Where does this helper go?”, “Should validation be in Domain or Application?” Use this as a decision guide:

Question Answer → Layer
Does it reference EF Core, SQL, HttpClient, AspNetCore, file I/O, or any framework? Not Domain. If it’s an interfaceApplication. If it’s the class that talks to DB/HTTP/filesInfrastructure.
Is it a business entity (Order, User) or a business rule with no I/O (e.g. “order total must be positive”)? Domain.
Is it a use case (“place order”, “get user”) that orchestrates domain and calls interfaces (e.g. IOrderRepository)? Application.
Is it an interface that says “what we need from the outside” (e.g. IOrderRepository, IEmailSender)? Application.
Is it the implementation of that interface (e.g. OrderRepository using EF Core, EmailSender using SMTP)? Infrastructure.
Is it a controller, middleware, filter, or DI registration (Program.cs, Extensions/)? Presentation (API).
Is it validation? Domain if it’s an invariant (e.g. “order must have at least one item”). Application if it’s input/request validation (e.g. “customer ID required”).
Is it a DTO or request/response model for the API? Presentation. Commands/queries (e.g. PlaceOrderCommand) can live in Application.
Is it configuration or options (connection strings, URLs)? Read in Presentation (binding); consumed in Infrastructure via IOptions<T>. Not in Domain.

Rule of thumb: When in doubt, ask: “If I swapped the database or the UI, would this file change?” If no → it can stay in Domain or Application. If yes → it belongs in Infrastructure or Presentation.


Class structure: how the pieces fit together

The diagram shows the main types: Domain (Order), Application (IOrderRepository, PlaceOrderUseCase), Infrastructure (OrderRepository), Presentation (OrdersController). Arrows mean “depends on” or “implements”; all dependencies point inward.

Loading diagram…

Folder and file structure

A typical .NET Clean Architecture solution looks like this on disk. The important idea: Domain and Application have no project references to Infrastructure or API; Infrastructure and API reference Application (and Domain).

MyApp.sln
  src/
    MyApp.Domain/                    # No dependencies
      Entities/
        Order.cs
      ValueObjects/
        Money.cs                     # optional
    MyApp.Application/               # References: Domain only
      Interfaces/
        IOrderRepository.cs
        IEmailSender.cs              # optional
      UseCases/
        PlaceOrderUseCase.cs
      Commands/
        PlaceOrderCommand.cs         # optional, or in same file as use case
    MyApp.Infrastructure/            # References: Application, Domain
      Persistence/
        OrderRepository.cs
        AppDbContext.cs
      Services/
        EmailSender.cs               # implements IEmailSender
    MyApp.Api/                       # References: Application, Infrastructure, Domain
      Controllers/
        OrdersController.cs
      Extensions/                    # DI folder: keep Program.cs thin
        ServiceCollectionExtensions.cs   # AddApplicationServices(), AddInfrastructureServices()
      Filters/                      # optional: action/exception filters
      Program.cs
      ClientApp/                    # optional: Angular/React/Vue SPA (when frontend lives inside API)
  # Optional sibling frontend:     src/MyApp.Client/   (Angular/React/Vue) or   frontend/   at solution root
  # Optional Worker:               src/MyApp.Worker/   (Persistence/, Services/, HostedServices/)

In human terms:

  • Domain: “What” we model (Order, User, rules).
  • Application: “What” the app does (place order) and “what we need from the outside” (IOrderRepository).
  • Infrastructure: “How” we persist (EF Core, SQL) and “how” we send email or call APIs.
  • Api: “Where” requests come in (controllers) and “where” we wire everything (DI in Program.cs and Extensions/), and optional Filters/.

.NET Core solution structure

Solution file (.sln): One solution, multiple projects. Example (backend only):

MyApp.sln
  Project: MyApp.Domain          (class lib, no refs)
  Project: MyApp.Application     (class lib, refs Domain)
  Project: MyApp.Infrastructure  (class lib, refs Application, Domain)
  Project: MyApp.Api              (web API, refs Application, Infrastructure, Domain)

With optional frontend and/or Worker (same solution):

MyApp.sln
  Project: MyApp.Domain
  Project: MyApp.Application
  Project: MyApp.Infrastructure
  Project: MyApp.Api
  Project: MyApp.Client           # optional: Angular/React/Vue SPA (or use Api/ClientApp/)
  # OR folder at solution root:   frontend/   (Angular/React/Vue, not a .NET project)
  Project: MyApp.Worker           # optional: background/hosted services (refs Application, Infrastructure)

What each project contains (folders):

Project Folders / contents Notes
MyApp.Domain Entities/, ValueObjects/ No dependencies.
MyApp.Application Interfaces/, UseCases/, Commands/, optional Mapping/ References Domain only.
MyApp.Infrastructure Persistence/, Services/, optional Middleware/, health check implementations Implements Application interfaces.
MyApp.Api Controllers/, Extensions/ (DI: AddApplicationServices, AddInfrastructureServices), optional Filters/, Program.cs, optional ClientApp/ (Angular/React/Vue) Extensions = DI folder; ClientApp = frontend when SPA lives inside API project.
MyApp.Client (optional) Angular/React/Vue app (src/, package.json) Sibling frontend project; not a .NET layer.
MyApp.Worker (optional) Hosted services that implement Application interfaces or call use cases For background jobs, sync, etc.

Project references:

  • MyApp.Domain: No project references. Only .NET (e.g. net8.0).
  • MyApp.Application: References MyApp.Domain only.
  • MyApp.Infrastructure: References MyApp.Application and MyApp.Domain (so it can implement IOrderRepository and use Order).
  • MyApp.Api: References MyApp.Application, MyApp.Infrastructure, and MyApp.Domain so it can register implementations in DI (in Extensions/) and use use cases. Optionally contains ClientApp/ for Angular/React/Vue.
  • MyApp.Client (if used): No reference to Domain/Application/Infrastructure; it is a separate frontend app that calls the API.
  • MyApp.Worker (if used): References MyApp.Application and MyApp.Infrastructure (same as Api, but for background services).

Creating in .NET CLI:

dotnet new sln -n MyApp
dotnet new classlib -n MyApp.Domain -o src/MyApp.Domain
dotnet new classlib -n MyApp.Application -o src/MyApp.Application
dotnet new classlib -n MyApp.Infrastructure -o src/MyApp.Infrastructure
dotnet new webapi -n MyApp.Api -o src/MyApp.Api
dotnet sln add src/MyApp.Domain src/MyApp.Application src/MyApp.Infrastructure src/MyApp.Api
dotnet add src/MyApp.Application reference src/MyApp.Domain
dotnet add src/MyApp.Infrastructure reference src/MyApp.Application src/MyApp.Domain
dotnet add src/MyApp.Api reference src/MyApp.Application src/MyApp.Infrastructure src/MyApp.Domain
# Optional: Angular inside Api (then add ClientApp to MyApp.Api)
# dotnet new angular -n MyApp.Api -o src/MyApp.Api  # creates Api with ClientApp
# Optional: sibling frontend (e.g. create frontend/ with npm/vite/angular CLI separately)
# Optional: Worker
# dotnet new worker -n MyApp.Worker -o src/MyApp.Worker
# dotnet sln add src/MyApp.Worker
# dotnet add src/MyApp.Worker reference src/MyApp.Application src/MyApp.Infrastructure

Full code example: end to end

Below is full code for a minimal “Place Order” flow: one entity, one repository interface, one use case, one repository implementation, one controller, and DI setup. Each file is explained so you can see how it fits the layers.

1. Domain: entity (no dependencies)

// MyApp.Domain/Entities/Order.cs
namespace MyApp.Domain.Entities;

public class Order
{
    public Guid Id { get; set; }
    public string CustomerId { get; set; } = "";
    public string Status { get; set; } = "";
    public DateTime CreatedAt { get; set; }
}

What this file is: The Order entity. Pure data and (if you add them) domain rules. No database, no HTTP. This is the centre of Clean Architecture.


2. Application: repository interface

// MyApp.Application/Interfaces/IOrderRepository.cs
using MyApp.Domain.Entities;

namespace MyApp.Application.Interfaces;

public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task AddAsync(Order order, CancellationToken ct = default);
}

What this file is: A contract: “something that can load and save orders.” Application depends on this interface; it does not care whether the implementation uses SQL, NoSQL, or in-memory. That’s Dependency Inversion.


3. Application: use case (orchestration)

// MyApp.Application/UseCases/PlaceOrderUseCase.cs
using MyApp.Application.Interfaces;
using MyApp.Domain.Entities;

namespace MyApp.Application.UseCases;

public class PlaceOrderUseCase
{
    private readonly IOrderRepository _repo;

    public PlaceOrderUseCase(IOrderRepository repo) => _repo = repo;

    public async Task<Guid> Execute(PlaceOrderCommand cmd, CancellationToken ct = default)
    {
        var order = new Order
        {
            Id = Guid.NewGuid(),
            CustomerId = cmd.CustomerId,
            Status = "Placed",
            CreatedAt = DateTime.UtcNow
        };
        await _repo.AddAsync(order, ct);
        return order.Id;
    }
}

public record PlaceOrderCommand(string CustomerId);

What this file is: The use case “place order.” It creates an Order, saves it via IOrderRepository, and returns the ID. It has one dependency: IOrderRepository. No EF Core, no controller—so you can unit test it with a fake repository.


4. Infrastructure: repository implementation (EF Core)

// MyApp.Infrastructure/Persistence/OrderRepository.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Application.Interfaces;
using MyApp.Domain.Entities;

namespace MyApp.Infrastructure.Persistence;

public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _db;

    public OrderRepository(AppDbContext db) => _db = db;

    public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default)
        => await _db.Orders.FindAsync(new object[] { id }, ct);

    public async Task AddAsync(Order order, CancellationToken ct = default)
    {
        await _db.Orders.AddAsync(order, ct);
        await _db.SaveChangesAsync(ct);
    }
}

What this file is: The concrete implementation of IOrderRepository using EF Core. It lives in Infrastructure because it knows about the database. Application does not reference this project; it only references the interface.


5. Infrastructure: DbContext (EF Core)

// MyApp.Infrastructure/Persistence/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Domain.Entities;

namespace MyApp.Infrastructure.Persistence;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Order> Orders => Set<Order>();
}

What this file is: EF Core DbContext. It maps Order to a table. Infrastructure references Domain so it can use Order; Domain has no reference to EF Core.


6. Presentation: API controller and Program.cs

// MyApp.Api/Controllers/OrdersController.cs
using Microsoft.AspNetCore.Mvc;
using MyApp.Application.UseCases;

namespace MyApp.Api.Controllers;

[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly PlaceOrderUseCase _placeOrder;

    public OrdersController(PlaceOrderUseCase placeOrder) => _placeOrder = placeOrder;

    [HttpPost]
    public async Task<ActionResult<Guid>> Post([FromBody] PlaceOrderRequest request, CancellationToken ct)
    {
        var id = await _placeOrder.Execute(new PlaceOrderCommand(request.CustomerId), ct);
        return CreatedAtAction(nameof(Get), new { id }, id);
    }

    [HttpGet("{id:guid}")]
    public async Task<ActionResult<OrderDto>> Get(Guid id, CancellationToken ct)
    {
        // In a full example you'd inject IOrderRepository or a query use case
        return NotFound();
    }
}

public record PlaceOrderRequest(string CustomerId);
public record OrderDto(Guid Id, string CustomerId, string Status);

What this file is: The entry point for “place order.” The controller receives HTTP, maps the request to PlaceOrderCommand, calls the use case, and returns the result. It depends only on PlaceOrderUseCase (Application), not on OrderRepository or EF Core directly.

// MyApp.Api/Program.cs (excerpt)
using Microsoft.EntityFrameworkCore;
using MyApp.Application.Interfaces;
using MyApp.Application.UseCases;
using MyApp.Infrastructure.Persistence;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<PlaceOrderUseCase>();

// ... AddControllers(), etc.

var app = builder.Build();
// ... UseRouting, MapControllers, Run();

What this does: DI composition root. We register the DbContext, the repository implementation (OrderRepository for IOrderRepository), and the use case. So when a request hits OrdersController, the container injects PlaceOrderUseCase, which gets OrderRepository as IOrderRepository. All wiring happens here, in Presentation; Domain and Application stay unaware of the container.

Optional: dedicated DI folder. To keep Program.cs thin, move registration into extension methods in Extensions/ServiceCollectionExtensions.cs:

// MyApp.Api/Extensions/ServiceCollectionExtensions.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Application.Interfaces;
using MyApp.Application.UseCases;
using MyApp.Infrastructure.Persistence;

namespace MyApp.Api.Extensions;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services)
    {
        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddScoped<PlaceOrderUseCase>();
        return services;
    }

    public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration config)
    {
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(config.GetConnectionString("DefaultConnection")));
        return services;
    }
}

Then Program.cs becomes: builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddApplicationServices(); — all DI lives in the Extensions folder; no business logic there.


How the code fits together

  1. Request hits POST /api/ordersOrdersController.Post.
  2. Controller builds PlaceOrderCommand and calls _placeOrder.Execute(cmd).
  3. PlaceOrderUseCase creates an Order, calls _repo.AddAsync(order). It only knows IOrderRepository.
  4. OrderRepository (injected as IOrderRepository) uses AppDbContext to save to SQL.
  5. Use case returns order.Id; controller returns 201 Created with the ID.

Dependency flow: Controller → UseCase → IOrderRepository (interface). At runtime, the container injects OrderRepository for IOrderRepository. So business logic (Domain + Application) never touches Infrastructure; Infrastructure implements the contracts defined by Application.


Frontend: same solution or separate? (Angular, React, Vue)

Question: Should the frontend (e.g. Angular, React, Vue, or Blazor) live in the same solution as the .NET API or in a separate repo? And where exactly do you put it when it’s in the same solution?

Where to put the frontend when it’s in the same solution

Approach Folder / project When to use
Inside the API project MyApp.Api/ClientApp/ (or wwwroot/ClientApp/) Angular: dotnet new angular creates ClientApp/ inside the API; the API serves the SPA (SPA middleware). React / Vue: same idea—ClientApp/ with package.json; API serves built static files or SPA fallback. One repo, one pipeline.
Sibling project src/MyApp.Api/ and src/MyApp.Client/ (or MyApp.Angular/, MyApp.React/) Frontend is its own project with its own package.json and build. CI builds both; deploy API and frontend together or separately (e.g. API to App Service, frontend to CDN).
Solution-root frontend folder frontend/ or clients/web/ at the solution root (sibling to src/) Same idea: frontend/ contains the Angular/React/Vue app; src/ contains the .NET solution. Clear separation; one repo.

Angular: Often ClientApp/ inside the API (from dotnet new angular) or src/MyApp.Client/ / frontend/angular/ as a sibling.
React / Vue: Same options: ClientApp/ inside Api, or src/MyApp.Client/ / frontend/react/ or frontend/vue/.

The frontend is not a layer of Clean Architecture; it’s a client that calls your API. Domain, Application, Infrastructure, and API stay backend-only.

Same solution vs separate repo

Same solution (monorepo):

  • Pros: One clone, one CI pipeline can build API + frontend; shared tooling; easier to keep API and UI in sync for a single team.
  • Cons: Larger repo; frontend and backend may have different release cycles; some teams prefer to deploy frontend (e.g. static site) separately from API.

Separate repo (or separate folder with separate pipeline):

  • Pros: Frontend can be deployed to a CDN or static host; backend and frontend can have different release cadences; clear boundary (API contract only).
  • Cons: Two repos to clone; need to coordinate API changes (e.g. OpenAPI) and versioning.

Recommendation for .NET + frontend:

  • Small team, one product: A single solution with a folder or project for the frontend (e.g. src/MyApp.Web with Angular/React) is often enough. Build both in one pipeline; deploy API and static assets together or to the same host.
  • Larger or multi-team: Keep the API in its own solution (Clean Architecture as above) and the frontend in its own repo. Contract (OpenAPI) and versioning become the integration point. API stays framework-agnostic at the core; frontend consumes the API only.

In both cases, Clean Architecture applies to the .NET backend: Domain, Application, Infrastructure, API. The frontend is a client of the API; it does not become a “layer” inside the same process. So: backend = Clean Architecture; frontend = separate app (same or different repo) that calls the API.


Common issues and enterprise practices

Common issues (what people struggle with)

People often have trouble deciding which asset goes where:

  • “Is this Domain or Application?” — Use the rule: if it knows about DB/HTTP/framework, it’s not Domain. If it’s an interface or use case that only uses Domain and interfaces → Application. If it’s a business entity or rule with no I/O → Domain. See How to decide where to put what.
  • “Where does validation go?”Domain for invariants (e.g. “order must have at least one item”). Application for input/request validation (e.g. “customer ID required”). Not in controllers as business rules.
  • “Where do I put this helper/utility?” — If it’s pure business logic (no I/O) → Domain or Application. If it talks to DB, HTTP, or filesInfrastructure. If it’s API-specific (e.g. mapping request to command) → Presentation.
  • “Should DI be in the API?”Yes. DI registration (composition root) lives in the API (Presentation). Domain and Application have no reference to the container. See Why DI lives in the API.
  • “Is this Application or Infrastructure?”Application = interfaces and use cases (orchestration). Infrastructure = implementations of those interfaces (EF Core, HTTP, email). If the class has using Microsoft.EntityFrameworkCore or HttpClient, it’s Infrastructure.

The most important piece of all this: the dependency rule + the one-question test: “If I swapped the database or the UI, would this file change?” If no → Domain or Application. If yes → Infrastructure or Presentation. See The most important idea.

Enterprise practices (do)

  • Keep Domain pure: no EF, no HTTP, no [ApiController].
  • Put interfaces for repositories and external services in Application; implement in Infrastructure.
  • Register all implementations in one place (e.g. Program.cs or Extensions/ServiceCollectionExtensions.cs in the API project).
  • Unit test use cases with mock IOrderRepository, IEmailSender, etc.
  • Use options (e.g. IOptions<StorageOptions>) for config and paths; inject into Infrastructure, not Domain.

Pitfalls (avoid)

  • Leaking infrastructure into Domain: Avoid referencing EF Core, SQL, or HTTP in Domain. If Domain needs “time” or “current user”, define IDateTimeProvider / ICurrentUserProvider in Application and implement in Infrastructure.
  • Anemic domain: Don’t put all logic in use cases and leave entities as dumb DTOs. Put invariants and domain rules in the Domain where they belong.
  • Over-layering: Not every app needs four projects. Start with Domain + Application + one “outer” project (e.g. Api that also contains persistence); split Infrastructure when the project grows.
  • Frontend as a layer: The frontend is a consumer of the API. Don’t mix frontend code inside the same process as the API layers; keep Clean Architecture for the backend and treat the UI as a separate client.
  • Putting DI in Domain or Application: DI registration belongs in the API (Presentation). Domain and Application only receive interfaces via constructors; they don’t reference the container.

Summary

Clean Architecture gives you layers (Domain → Application → Infrastructure → Presentation) and a dependency rule (dependencies point inward) so business logic stays testable and swappable. Breaking the rule—putting DB or HTTP concerns in the domain—leads to untestable cores and painful refactors; enforcing it in review and adopting incrementally in brownfield keeps systems maintainable. Next, apply the “would this file change if I swapped the database or UI?” test to your current codebase and introduce one vertical slice with clear layer boundaries before expanding.

  • Clean Architecture = layers (Domain → Application → Infrastructure → Presentation) + dependency rule (dependencies point inward).
  • The most important idea: If it knows about DB/HTTP/framework, it doesn’t go in Domain. Use the question: “If I swapped the database or the UI, would this file change?” — No → Domain or Application; Yes → Infrastructure or Presentation.
  • Domain: entities and business rules; no framework or I/O.
  • Application: use cases and interfaces (repositories, services, providers); orchestration and contracts only.
  • Infrastructure: implementations (EF Core, HTTP, email, files); implements Application interfaces.
  • Presentation (API): entry point, controllers, and DI registration (in Program.cs or Extensions/); wires use cases and Infrastructure. DI belongs in the API — it’s the composition root.
  • DI, services, providers = interfaces in Application, implementations in Infrastructure, registered in Presentation (API).
  • How to decide where to put what: See the decision guide; common issues and enterprise practices in Common issues and enterprise practices.
  • Frontend: can live in the same solution or a separate repo; either way, treat it as a client of the API and keep Clean Architecture on the backend.
  • Full .NET Core example: Domain entity → Application interface + use case → Infrastructure repository + DbContext → API controller + Program.cs (or Extensions). Use this as a template and add more use cases and interfaces as needed.

Position & Rationale

I apply Clean Architecture when the app has non-trivial business logic and I expect to swap persistence, UI, or integrations later—or when multiple teams touch the same codebase and I want a clear “domain here, infrastructure there” rule. I avoid it for tiny CRUD apps, prototypes, or when the team has no appetite for interfaces and layers—then a simpler structure is better. I put DI registration in the API (Presentation) as the composition root; domain and application stay free of framework and I/O. I reject “domain services” that call repositories directly from entities; orchestration belongs in use cases. I also avoid over-layering: four layers (Domain, Application, Infrastructure, Presentation) are enough; extra “cross-cutting” or “shared” layers often become dumping grounds.


Trade-Offs & Failure Modes

  • What this sacrifices: More files, interfaces, and indirection; onboarding takes longer until people internalise “where does this go?” In return you get testable use cases and swappable infrastructure.
  • Where it degrades: When nobody enforces the dependency rule—then Infrastructure creeps into Domain (e.g. EF attributes on entities) or Presentation holds business logic. It also degrades when every tiny feature gets a new layer or project.
  • How it fails when misapplied: Using it for a 5-endpoint API with no real business rules; or the opposite—putting everything in Domain “because it’s important.” Another failure: treating the diagram as dogma and adding layers that don’t correspond to real boundaries.
  • Early warning signs: “We have Clean Architecture but our domain references DbContext”; “we’re not sure if this is Application or Infrastructure”; “our use cases are just pass-through to the repository.”

What Most Guides Miss

Most guides show the onion diagram and stop. The hard part is where DI lives: it belongs in Presentation (API), not in Application or a “Composition Root” project that nobody owns. The other gap: what goes in Domain vs Application. Domain = entities, value objects, and domain logic that doesn’t need I/O. Application = use cases, interfaces (e.g. IOrderRepository), and orchestration. People often put “services” in Domain or put orchestration in controllers; that blurs the rule. Finally: incremental adoption. You can introduce Clean Architecture in one vertical (e.g. Orders) and leave the rest flat until you refactor; you don’t need a big-bang rewrite.


Decision Framework

  • If greenfield and non-trivial business logic → Start with Domain, Application (interfaces + use cases), Infrastructure, Presentation; enforce dependency rule from day one.
  • If brownfield monolith → Extract one bounded context (e.g. Orders) into the four layers; leave the rest as-is until you have capacity to refactor.
  • If team is small and deadline is tight → Use a minimal subset: Domain + Application interfaces; single Infrastructure project; DI in API. Add structure as you go.
  • If “where does this go?” is unclear → Ask: “If I swapped the database or UI, would this change?” No → Domain or Application; Yes → Infrastructure or Presentation.
  • If domain is referencing EF or HTTP → Move that out; domain must have zero references to outer layers.

You can also explore more patterns in the .NET Architecture resource page.

Key Takeaways

  • Clean Architecture optimises for testable business logic and swappable infrastructure; inner layers never depend on outer layers.
  • Put DI registration in Presentation (API); domain and application define interfaces, infrastructure implements them.
  • Domain = entities and domain logic only; no EF, no HTTP, no framework.
  • Application = use cases and interfaces; orchestration lives here, not in controllers or entities.
  • Enforce the dependency rule in review; “domain references DbContext” is a broken build.
  • Use incremental adoption in brownfield: one vertical at a time, don’t big-bang.

Need architectural guidance for real-world .NET platforms? I offer consulting for .NET architecture, API platforms, and enterprise system design.

When I Would Use This Again — and When I Wouldn’t

I would use Clean Architecture again when I’m building or refactoring an app with non-trivial business logic and I expect to swap persistence, UI, or integrations—or when multiple teams need a clear dependency rule. I wouldn’t use it for tiny CRUD apps, prototypes, or when the team has no appetite for layers; then a flatter structure is better. I also wouldn’t use it when the organisation insists on “everything in one project” with no room for interfaces; then focus on at least keeping domain logic out of controllers. Alternative: vertical slice or feature folders with minimal layering can work for smaller teams; add Clean Architecture when you hit testability or portability pain.


services
Frequently Asked Questions

Frequently Asked Questions

What is Clean Architecture?

Layers (Domain, Application, Infrastructure, Presentation) with a dependency rule: dependencies point inward. Domain has no dependencies; Application defines interfaces; Infrastructure and Presentation implement and use them.

What is the dependency rule?

Inner layers do not depend on outer layers. Domain and Application do not reference Infrastructure or Presentation; they define interfaces that outer layers implement.

When should I use Clean Architecture?

Use it for long-lived, complex, or multi-team projects where you need testability and independence from databases and frameworks. For small or throwaway projects, a simpler structure may be enough.

What are the layers?

Domain (centre), Application (use cases, interfaces), Infrastructure (DB, APIs, files), Presentation (API, UI, DI).

Where is business logic?

In Domain (entity rules, invariants) and Application (use case orchestration). Not in controllers or repositories.

Where is database access?

In Infrastructure. Repository classes (e.g. OrderRepository) implement interfaces from Application and use EF Core or another data access technology.

Where do I register DI? Is DI supposed to be in the API?

Yes. DI registration (composition root) lives in the API project (Presentation layer). Program.cs or Extensions/ServiceCollectionExtensions.cs registers use cases and Infrastructure implementations (IOrderRepositoryOrderRepository). Domain and Application have no reference to the DI container; they only receive interfaces via constructors. The API is the entry point, so it’s the right place to “wire” everything.

Where do services and providers live?

Interfaces in Application (e.g. IEmailSender, ICurrentUserProvider); implementations in Infrastructure. Register in Presentation.

Where do assets (config, certs) live?

Read by Infrastructure or Presentation. Use options (e.g. IOptions<StorageOptions>) and inject paths/URLs; keep Domain and Application free of file paths and framework config.

How do I test domain and use cases?

Unit tests: use mocks for IOrderRepository, IEmailSender, etc. Application and Domain have no framework dependencies, so tests run fast without a database or HTTP.

Clean vs Onion vs Hexagonal?

Same idea: domain at the centre, dependencies inward. Different names (Onion Architecture, Ports and Adapters) for the same principle.

Where do DTOs and request/response models go?

Request/response DTOs (e.g. PlaceOrderRequest) in Presentation. Commands/queries (e.g. PlaceOrderCommand) can live in Application. Domain uses entities and value objects, not API shapes.

Should the frontend be in the same solution?

Small team, one product: Same solution is fine (e.g. src/MyApp.Web). Larger or multi-team: Separate repo and treat API contract (OpenAPI) as the boundary. In both cases, Clean Architecture applies to the .NET backend only.

When not to use Clean Architecture?

Simple CRUD, prototypes, or very tight deadlines where the overhead of layers and interfaces doesn’t pay off. You can still keep a clear folder structure and avoid putting SQL in controllers.

How do I add a new use case?

Add the interface (if needed) and use case class in Application; add implementation (e.g. repository method) in Infrastructure; add controller action and DI registration in Presentation (API). Domain changes only if you add new entities or rules.

What’s the most important thing to remember?

The dependency rule + one question: “If I swapped the database or the UI, would this file change?” If no → Domain or Application. If yes → Infrastructure or Presentation. And: if it knows about DB/HTTP/framework, it doesn’t go in Domain. Use How to decide where to put what when you’re unsure.

I don’t know if this class goes in Domain or Application — what do I do?

Ask: does it reference EF Core, SQL, HttpClient, or any framework? If yes → not Domain. Is it an interface or a use case (orchestration)? → Application. Is it a business entity or rule with no I/O? → Domain. See the decision guide.

services
Core concepts

Concepts defined or covered in this article:

services
Related patterns & principles

services
Related Guides & Resources

services
Related services