Todos los posts

· 5 min de lectura

Clean Architecture en .NET (sin dogmas): recorrido por un proyecto real

Una explicación profunda y real de Clean Architecture con el BoricuaCoder Clean Template. Decisiones, trade-offs y caminos de extensión, explicados con código real.

En esta página
  1. Por qué existe este proyecto
  2. Qué resuelve realmente Clean Architecture (y qué no)
  3. Cuándo NO deberías usar Clean Architecture
  4. Estructura del proyecto (real, no teórica)
  5. Por qué CQRS (ligero, sin MediatR)
  6. Explicación capa por capa (con ejemplos reales)
  7. 1. API: capa de presentación
  8. 2. Application: casos de uso
  9. 3. Domain: reglas de negocio
  10. 4. Infrastructure: los detalles
  11. Agregar una funcionalidad nueva (paso a paso, con el proyecto real)
  12. Paso 1: Domain
  13. Paso 2: Application
  14. Paso 3: Infrastructure
  15. Paso 4: API
  16. Paso 5: Pruebas
  17. Lo que evitamos a propósito (Without Dogma)
  18. Lo que viene: DDD
  19. Reflexión final
  20. Proyecto

Este artículo se basa por completo en el BoricuaCoder Clean Template (Without Dogma), un proyecto .NET real y listo para producción. Sin pseudocódigo. Sin capas imaginarias. Sin teoría sin contexto.


Por qué existe este proyecto

Clean Architecture suele presentarse como un ideal.

En la práctica, muchos equipos la viven así:

  • demasiadas capas
  • demasiada abstracción
  • frameworks que deciden tu diseño
  • código que se siente académico en lugar de útil

Este template existe para responder una pregunta simple:

¿Cuál es la mínima cantidad de estructura necesaria para construir de forma segura un sistema .NET de larga vida?

Todo lo que hay en este repositorio, y en este artículo, es consecuencia directa de esa pregunta.


Qué resuelve realmente Clean Architecture (y qué no)

Clean Architecture no trata de ser limpia.

Trata de controlar el cambio.

En concreto, ayuda cuando:

  • las reglas de negocio evolucionan con el tiempo
  • las decisiones de infraestructura no son estables
  • varios desarrolladores tocan el mismo código
  • la capacidad de probar el código importa

No ayuda cuando:

  • el dominio es trivial
  • la aplicación va a durar poco
  • la velocidad importa más que la corrección

Por eso el template es mínimo a propósito.


Cuándo NO deberías usar Clean Architecture

No uses este template si:

  • estás construyendo un panel de administración que es puro CRUD
  • estás prototipando algo desechable
  • no vas a escribir pruebas automatizadas

Clean Architecture introduce fricción.

Este template existe para que esa fricción valga la pena, no para esconderla.


Estructura del proyecto (real, no teórica)

Tomado del repositorio:

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

Flujo de dependencias:

Api → Application → Domain
              ↑
       Infrastructure

Esta única regla explica el 90% del diseño.


Por qué CQRS (ligero, sin MediatR)

CQRS en este proyecto no tiene que ver con escala.

Existe para:

  • hacer explícita la intención
  • separar lecturas de escrituras
  • mantener los casos de uso pequeños y fáciles de probar

Un command representa algo que el sistema quiere hacer. Un query representa algo que el sistema quiere saber.

Evitamos MediatR a propósito.

¿Por qué?

  • las dependencias explícitas son más fáciles de razonar
  • depurar es directo
  • no hay pipelines ocultos

CQRS es un patrón, no un requisito de ningún framework.


Explicación capa por capa (con ejemplos reales)

1. API: capa de presentación

Ubicada en:

BoricuaCoder.CleanTemplate.Api

Responsabilidades:

  • definir los endpoints HTTP
  • recibir las peticiones
  • traducir la entrada a commands / queries
  • devolver respuestas HTTP

Los controllers son delgados a propósito.

Flujo de ejemplo:

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

Aquí no vive ninguna regla de negocio.


2. Application: casos de uso

Ubicada en:

BoricuaCoder.CleanTemplate.Application

Este es el centro de gravedad.

Contiene:

  • commands y queries
  • handlers
  • validación
  • interfaces de repositorio (ports)

Command de ejemplo:

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

Handler de ejemplo:

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);
    }
}

El handler coordina.

No sabe cómo funciona la persistencia.


3. Domain: reglas de negocio

Ubicada en:

BoricuaCoder.CleanTemplate.Domain

El dominio no tiene dependencias.

Define qué es válido, qué está permitido y qué no.

Entidad de ejemplo:

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);
    }
}

Aquí no existe ninguna preocupación de infraestructura.


4. Infrastructure: los detalles

Ubicada en:

BoricuaCoder.CleanTemplate.Infrastructure

Infrastructure implementa los ports que define Application.

Repositorio de ejemplo:

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 y SQL se quedan aquí.

El dominio nunca los ve.


Agregar una funcionalidad nueva (paso a paso, con el proyecto real)

Vamos a agregar una funcionalidad nueva a este proyecto:

Get Movie by Id

Paso 1: Domain

No cambia nada.

Esta funcionalidad no introduce reglas nuevas.


Paso 2: Application

Crea el query:

public record GetMovieByIdQuery(Guid Id);

Define el port:

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

Implementa el handler.


Paso 3: Infrastructure

Implementa el repositorio con Dapper.

El SQL se queda en:

Infrastructure/Database

Paso 4: API

Agrega el endpoint al controller:

GET /movies/{id}

Mapea request → query → response.


Paso 5: Pruebas

Agrega pruebas unitarias para el handler.

No hace falta base de datos.


Lo que evitamos a propósito (Without Dogma)

Este proyecto evita:

  • MediatR
  • repositorios genéricos
  • EF Core
  • capas de abstracción extra
  • patrones DDD prematuros

No porque sean malos.

Sino porque todavía no hacían falta.


Lo que viene: DDD

Clean Architecture define dónde vive el código.

DDD define cómo se modela el negocio.

Lo siguiente en esta serie:

  • aggregates
  • invariantes
  • value objects
  • lenguaje ubicuo

Sobre este mismo template.

Sin reescrituras.

Solo evolución.


Reflexión final

Clean Architecture es una herramienta.

Usada a ciegas, frena a los equipos.

Usada con intención, genera palanca.

Este template vive justo en esa frontera, a propósito.

Proyecto

👉🏻 GitHub Repo ⭐️