La arquitectura adecuada es el cimiento sobre el que se construyen aplicaciones web escalables y mantenibles. Un error temprano en el diseño puede convertirse en deuda técnica difícil de corregir meses después. Con la llegada de .NET 10 y Visual Studio 2026, la plataforma ofrece nuevas capacidades de rendimiento, AOT más estable, mejoras en Minimal APIs y tooling avanzado para diagnósticos. Sin embargo, ninguna herramienta reemplaza los principios arquitectónicos correctos.

Este artículo explica cómo aplicar separación de capas, principios SOLID y Clean Architecture en proyectos web modernos utilizando .NET 10, con ejemplos de código paso a paso y explicaciones detalladas orientadas a ingeniería de software profesional.

Objetivo del artículo Link to heading

El objetivo es que el lector pueda:

  • Diseñar aplicaciones web limpias, seguras y fáciles de evolucionar.
  • Aplicar SOLID en módulos web, APIs, casos de uso y servicios.
  • Implementar Clean Architecture sin sobreingeniería.
  • Comprender qué va en cada capa y por qué.
  • Aprovechar mejoras de .NET 10 y Visual Studio 2026 para arquitectura moderna.

1. Separación de capas: base de una arquitectura web sólida Link to heading

Separar capas no significa distribuir carpetas sin criterio. Significa aislar responsabilidades y minimizar dependencias accidentales. Clean Architecture propone un flujo unidireccional donde las capas internas nunca dependen de las externas.

Estructura recomendada:

src/
  Web/
  Application/
  Domain/
  Infrastructure/

Domain Link to heading

Contiene las reglas del negocio: entidades, agregados y objetos de valor. No conoce bases de datos, HTTP ni JSON.

namespace Payroll.Domain.Entities;

public class Employee
{
    public Guid Id { get; }
    public string FullName { get; private set; }
    public decimal BaseSalary { get; private set; }

    public Employee(Guid id, string fullName, decimal baseSalary)
    {
        Id = id;
        FullName = fullName;
        BaseSalary = baseSalary;
    }

    public void ChangeSalary(decimal newSalary)
    {
        if (newSalary <= 0)
            throw new DomainException("Salary must be greater than zero.");

        BaseSalary = newSalary;
    }
}

public class DomainException : Exception
{
    public DomainException(string message) : base(message) { }
}

Application Link to heading

Contiene los casos de uso del negocio.

public record ChangeSalaryCommand(Guid EmployeeId, decimal NewSalary);

public class ChangeSalaryHandler
{
    private readonly IEmployeeRepository _repository;

    public ChangeSalaryHandler(IEmployeeRepository repository)
    {
        _repository = repository;
    }

    public async Task Handle(ChangeSalaryCommand command)
    {
        var employee = await _repository.GetAsync(command.EmployeeId);
        if (employee is null)
            throw new ApplicationException("Employee not found");

        employee.ChangeSalary(command.NewSalary);

        await _repository.UpdateAsync(employee);
    }
}

Interfaces:

public interface IEmployeeRepository
{
    Task<Employee?> GetAsync(Guid id);
    Task UpdateAsync(Employee employee);
}

Infrastructure Link to heading

Implementa detalles técnicos: SQL, Dapper, EF Core, Redis, colas, etc.

public class EmployeeRepository : IEmployeeRepository
{
    private readonly NpgsqlConnection _connection;

    public EmployeeRepository(NpgsqlConnection connection)
    {
        _connection = connection;
    }

    public async Task<Employee?> GetAsync(Guid id)
    {
        const string sql = "SELECT id, fullname, basesalary FROM employee WHERE id = @id";

        var dto = await _connection.QueryFirstOrDefaultAsync<EmployeeDto>(sql, new { id });

        return dto is null
            ? null
            : new Employee(dto.Id, dto.FullName, dto.BaseSalary);
    }

    public async Task UpdateAsync(Employee employee)
    {
        const string sql =
            "UPDATE employee SET fullname=@FullName, basesalary=@BaseSalary WHERE id=@Id";

        await _connection.ExecuteAsync(sql, employee);
    }
}

internal record EmployeeDto(Guid Id, string FullName, decimal BaseSalary);

Web Link to heading

La capa expuesta al usuario (API).

app.MapGroup("/employees")
    .MapPut("/{id:guid}/salary", async (Guid id, SalaryRequest req, ChangeSalaryHandler handler) =>
    {
        await handler.Handle(new ChangeSalaryCommand(id, req.NewSalary));
        return Results.NoContent();
    });

2. Aplicación correcta de los principios SOLID Link to heading

S — Single Responsibility Principle Link to heading

Cada clase debe tener una responsabilidad.

public class PayrollCalculator
{
    public decimal Calculate(Employee employee, int hours)
        => employee.BaseSalary + (hours * 30000);
}

O — Open/Closed Principle Link to heading

El software debe poder extenderse sin modificarse.

public interface IPayrollStrategy
{
    decimal Calculate(Employee employee, int factor);
}

L — Liskov Substitution Principle Link to heading

Las clases derivadas deben funcionar como las base sin alterar comportamiento esperado.

I — Interface Segregation Principle Link to heading

Evita interfaces demasiado amplias.

public interface IUserCreator { Task Create(); }
public interface IUserUpdater { Task Update(); }

D — Dependency Inversion Principle Link to heading

Las capas superiores dependen de abstracciones, no de implementaciones.

private readonly IEmployeeRepository repository;

3. Clean Architecture aplicada a .NET 10 paso a paso Link to heading

Proyecto Link to heading

dotnet new webapi -n Web
dotnet new classlib -n Domain
dotnet new classlib -n Application
dotnet new classlib -n Infrastructure

Dependencias Link to heading

Web → Application  
Application → Domain  
Infrastructure → Application + Domain

Registro de dependencias Link to heading

builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();
builder.Services.AddScoped<ChangeSalaryHandler>();

Endpoint Link to heading

app.MapPut("/employees/{id}/salary", async (
    Guid id,
    SalaryRequest req,
    ChangeSalaryHandler handler) =>
{
    await handler.Handle(new ChangeSalaryCommand(id, req.NewSalary));
    return Results.NoContent();
});

Test sin infraestructura Link to heading

public class FakeEmployeeRepository : IEmployeeRepository
{
    public Employee Employee = new(Guid.NewGuid(), "Test", 5000000);

    public Task<Employee?> GetAsync(Guid _) => Task.FromResult<Employee?>(Employee);
    public Task UpdateAsync(Employee e) { Employee = e; return Task.CompletedTask; }
}

[Fact]
public async Task Should_Change_Salary()
{
    var repo = new FakeEmployeeRepository();
    var handler = new ChangeSalaryHandler(repo);

    await handler.Handle(new ChangeSalaryCommand(repo.Employee.Id, 7000000));

    Assert.Equal(7000000, repo.Employee.BaseSalary);
}

4. Beneficios de .NET 10 y Visual Studio 2026 Link to heading

  • AOT más rápido y predecible.
  • Diagnósticos de dependencias entre capas.
  • Análisis de arquitectura en tiempo real.
  • Refactorización avanzada guiada.
  • Herramientas para verificar cumplimiento de SOLID.

5. Checklist arquitectónico Link to heading

  • ¿El dominio contiene toda la lógica del negocio?
  • ¿Application usa solo abstracciones?
  • ¿Cada clase tiene una sola responsabilidad?
  • ¿La API es solo una capa de orquestación?
  • ¿Es posible testear sin base de datos?
  • ¿Los cambios son fáciles de extender sin modificar?

Conclusión Link to heading

Clean Architecture no es una moda: es una forma de pensar el software para resistir el paso del tiempo. Combinada con .NET 10 y las herramientas avanzadas de Visual Studio 2026, permite desarrollar APIs y aplicaciones web mantenibles, probadas y escalables.

La separación de capas, SOLID y una correcta aplicación de abstracciones convierten un proyecto en una base sólida lista para crecer sin dolor.