All posts

· 5 min read

Clean Architecture in .NET (Without Dogma) — A Real Project Walkthrough

A deep, real-world explanation of Clean Architecture using the BoricuaCoder Clean Template. Decisions, trade-offs, and extension paths — explained with real code.

On this page
  1. Why this project exists
  2. What Clean Architecture actually solves (and what it doesn’t)
  3. When you should NOT use Clean Architecture
  4. Project structure (real, not theoretical)
  5. Why CQRS (lightweight, without MediatR)
  6. Layer-by-layer explanation (with real examples)
  7. 1. API — Presentation layer
  8. 2. Application — Use cases
  9. 3. Domain — Business rules
  10. 4. Infrastructure — Details
  11. Adding a new feature (real step-by-step)
  12. Step 1 — Domain
  13. Step 2 — Application
  14. Step 3 — Infrastructure
  15. Step 4 — API
  16. Step 5 — Tests
  17. What we intentionally avoided (Without Dogma)
  18. What comes next: DDD
  19. Final thoughts
  20. Project

This article is based entirely on the BoricuaCoder Clean Template (Without Dogma) — a real, production-ready .NET project. No pseudo-code. No imaginary layers. No theory without context.


Why this project exists

Clean Architecture is often presented as an ideal.

In practice, many teams experience it as:

  • too many layers
  • too much abstraction
  • frameworks deciding your design
  • code that feels academic instead of useful

This template exists to answer a simple question:

What is the minimum amount of structure needed to build a long‑lived .NET system safely?

Everything in this repository — and in this article — is a direct consequence of that question.


What Clean Architecture actually solves (and what it doesn’t)

Clean Architecture is not about being clean.

It is about controlling change.

Specifically, it helps when:

  • business rules evolve over time
  • infrastructure choices are not stable
  • multiple developers touch the same codebase
  • testability matters

It does not help when:

  • the domain is trivial
  • the app is short‑lived
  • speed matters more than correctness

This is why the template is intentionally minimal.


When you should NOT use Clean Architecture

Do not use this template if:

  • you are building a CRUD-only admin panel
  • you are prototyping something disposable
  • you are not writing automated tests

Clean Architecture introduces friction.

This template exists to make that friction worth it — not invisible.


Project structure (real, not theoretical)

From the repository:

src/
  BoricuaCoder.CleanTemplate.Api            -> Presentation (Controllers, DTOs)
  BoricuaCoder.CleanTemplate.Application    -> Use cases (Commands, Queries, Handlers, Validation)
  BoricuaCoder.CleanTemplate.Domain         -> Entities, Value Objects, Domain rules
  BoricuaCoder.CleanTemplate.Infrastructure -> Dapper repositories, DB access, External services
tests/
  BoricuaCoder.CleanTemplate.UnitTests      -> NUnit + Moq

Dependency flow:

Api → Application → Domain
              ↑
       Infrastructure

This single rule explains 90% of the design.


Why CQRS (lightweight, without MediatR)

CQRS in this project is not about scale.

It exists to:

  • make intent explicit
  • separate reads from writes
  • keep use cases small and testable

A command represents something the system wants to do. A query represents something the system wants to know.

We intentionally avoided MediatR.

Why?

  • explicit dependencies are easier to reason about
  • debugging is straightforward
  • no hidden pipelines

CQRS is a pattern — not a framework requirement.


Layer-by-layer explanation (with real examples)

1. API — Presentation layer

Located in:

BoricuaCoder.CleanTemplate.Api

Responsibilities:

  • define HTTP endpoints
  • accept requests
  • translate input → commands / queries
  • return HTTP responses

Controllers are intentionally thin.

Example flow:

[HttpPost]
public async Task<IActionResult> Create(
    CreateMovieRequest request,
    ICreateMovieHandler handler)
{
    var result = await handler.Handle(request.ToCommand());
    return result.ToHttpResult();
}

No business rules live here.


2. Application — Use cases

Located in:

BoricuaCoder.CleanTemplate.Application

This is the center of gravity.

It contains:

  • commands & queries
  • handlers
  • validation
  • repository interfaces (ports)

Example command:

public record CreateMovieCommand(
    string Title,
    int Year,
    string? ImdbId
);

Example handler:

public sealed class CreateMovieHandler : ICommandHandler<CreateMovieCommand, Result<Guid>>
{
    private readonly IMovieRepository _repository;

    public async Task<Result<Guid>> Handle(CreateMovieCommand command)
    {
        var movie = Movie.Create(command.Title, command.Year, command.ImdbId);
        await _repository.Add(movie);
        return Result.Success(movie.Id);
    }
}

The handler coordinates.

It does not know how persistence works.


3. Domain — Business rules

Located in:

BoricuaCoder.CleanTemplate.Domain

The domain has no dependencies.

It defines what is valid, what is allowed, and what is not.

Example entity:

public sealed class Movie
{
    public Guid Id { get; }
    public string Title { get; }
    public int Year { get; }

    public static Movie Create(string title, int year, string? imdbId)
    {
        if (string.IsNullOrWhiteSpace(title))
            throw new DomainException("Title is required");

        if (year < 1888)
            throw new DomainException("Invalid movie year");

        return new Movie(Guid.NewGuid(), title, year, imdbId);
    }
}

No infrastructure concerns exist here.


4. Infrastructure — Details

Located in:

BoricuaCoder.CleanTemplate.Infrastructure

Infrastructure implements ports defined by Application.

Example repository:

public sealed class MovieRepository : IMovieRepository
{
    private readonly IDbConnectionFactory _factory;

    public async Task Add(Movie movie)
    {
        using var conn = _factory.Create();
        await conn.ExecuteAsync(Sql.InsertMovie, movie);
    }
}

Dapper and SQL stay here.

The domain never sees them.


Adding a new feature (real step-by-step)

Let’s add a new feature to this project:

Get Movie by Id

Step 1 — Domain

Nothing changes.

This feature does not introduce new rules.


Step 2 — Application

Create the query:

public record GetMovieByIdQuery(Guid Id);

Define the port:

public interface IMovieReadRepository
{
    Task<MovieResponse?> GetById(Guid id);
}

Implement the handler.


Step 3 — Infrastructure

Implement the repository using Dapper.

SQL stays in:

Infrastructure/Database

Step 4 — API

Add the controller endpoint:

GET /movies/{id}

Map request → query → response.


Step 5 — Tests

Add unit tests for the handler.

No database required.


What we intentionally avoided (Without Dogma)

This project avoids:

  • MediatR
  • generic repositories
  • EF Core
  • extra abstraction layers
  • premature DDD patterns

Not because they are bad.

But because they were not needed yet.


What comes next: DDD

Clean Architecture defines where code lives.

DDD defines how the business is modeled.

Next in this series:

  • aggregates
  • invariants
  • value objects
  • ubiquitous language

Built on this same template.

No rewrites.

Just evolution.


Final thoughts

Clean Architecture is a tool.

Used blindly, it slows teams down.

Used intentionally, it creates leverage.

This template lives on that boundary — deliberately.

Project

👉🏻 GitHub Repo ⭐️