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 ⭐️
