Clean Architecture is a software design philosophy introduced by Robert C. Martin (Uncle Bob). Its goal is to create systems that are:
- Independent of frameworks
- Testable
- Independent of UI or database
- Flexible and maintainable
Key Principles
- Separation of Concerns: Each layer of the application has a specific responsibility and doesn't mix with others.
- Dependency Rule: Code dependencies must point inward, from outer layers (UI, DB) to inner layers (Use Cases, Entities). Inner layers know nothing about the outer layers.
- Inversion of Control: High-level policies (business rules) shouldn't depend on low-level details (DB, APIs). Instead, interfaces should be used to invert control.
-
Independence:
- Framework Independent: Can change from ASP.NET to another without affecting core logic.
- Database Independent: Can switch from SQL Server to MongoDB.
- UI Independent: Can change from Web to Mobile easily.
The Layers
-
Domain Layer (Entities)
- Contains core business rules
- Framework and UI agnostic
- Pure business objects (e.g., User, Order)
-
Application Layer (Use Cases)
- Contains application-specific logic
- Coordinates the flow of data
- Interfaces with repositories
- E.g., CreateUser, GetOrders
-
Interface Adapters Layer
- Adapts data between use cases and frameworks
- Includes Controllers, Gateways, ViewModels
- Implements interfaces from Application Layer
-
Frameworks & Drivers
- External tools and frameworks (e.g., ASP.NET Core, EF Core)
- Dependency injection and infrastructure
- Least stable and most replaceable
Flow of Control
UI (Controller) → Use Case → Domain Logic → Output/Result
- Controllers receive the input (e.g., HTTP request)
- Pass data to the Use Case
- Use Case performs business logic using the Domain
- Returns results (e.g., DTOs or ViewModels)
⚖️ Advantages
- High Testability – Business logic can be tested without UI or DB.
- Easy to Maintain – Changes in UI/DB won’t affect core logic.
- Scalability– Modular design helps teams work independently.
- Reusability– Domain and Use Cases are reusable in different apps.
⚠️ Common Mistakes
- Violating the dependency rule (e.g., use case calling Entity Framework directly)
- Mixing business logic with framework code
- Skipping interfaces (tight coupling)
🧱 Project Structure
Solution: CleanArchitectureDemo.sln
Projects:
├── CleanArchitectureDemo.Domain // Entities
├── CleanArchitectureDemo.Application // Use Cases
├── CleanArchitectureDemo.Infrastructure // DB / Repositories
├── CleanArchitectureDemo.API // Web API (Controllers)
🔧 Create Projects
dotnet new sln -n CleanArchitectureDemo
dotnet new classlib -n CleanArchitectureDemo.Domain
dotnet new classlib -n CleanArchitectureDemo.Application
dotnet new classlib -n CleanArchitectureDemo.Infrastructure
dotnet new webapi -n CleanArchitectureDemo.API
Add them to the solution:
dotnet sln add CleanArchitectureDemo.Domain
dotnet sln add CleanArchitectureDemo.Application
dotnet sln add CleanArchitectureDemo.Infrastructure
dotnet sln add CleanArchitectureDemo.API
Add project references:
dotnet add CleanArchitectureDemo.Application reference CleanArchitectureDemo.Domain
dotnet add CleanArchitectureDemo.Infrastructure reference CleanArchitectureDemo.Application
dotnet add CleanArchitectureDemo.API reference CleanArchitectureDemo.Application
dotnet add CleanArchitectureDemo.API reference CleanArchitectureDemo.Infrastructure
📁 Layer Implementations
1. Domain/Entities/User.cs
namespace CleanArchitectureDemo.Domain.Entities
{
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
}
2. Application/Interfaces/IUserRepository.cs
using CleanArchitectureDemo.Domain.Entities;
using System.Collections.Generic;
namespace CleanArchitectureDemo.Application.Interfaces
{
public interface IUserRepository
{
IEnumerable<User> GetAll();
User GetById(int id);
}
}
3. Application/UseCases/UserService.cs
using CleanArchitectureDemo.Application.Interfaces;
using CleanArchitectureDemo.Domain.Entities;
using System.Collections.Generic;
namespace CleanArchitectureDemo.Application.UseCases
{
public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public IEnumerable<User> GetUsers() => _userRepository.GetAll();
public User GetUser(int id) => _userRepository.GetById(id);
}
}
4. Infrastructure/Repositories/InMemoryUserRepository.cs
using CleanArchitectureDemo.Application.Interfaces;
using CleanArchitectureDemo.Domain.Entities;
using System.Collections.Generic;
using System.Linq;
namespace CleanArchitectureDemo.Infrastructure.Repositories
{
public class InMemoryUserRepository : IUserRepository
{
private readonly List<User> _users = new()
{
new User { Id = 1, Name = "Alice", Email = "alice@example.com" },
new User { Id = 2, Name = "Bob", Email = "bob@example.com" }
};
public IEnumerable<User> GetAll() => _users;
public User GetById(int id) => _users.FirstOrDefault(u => u.Id == id);
}
}
5. API/Controllers/UserController.cs
using Microsoft.AspNetCore.Mvc;
using CleanArchitectureDemo.Application.UseCases;
using CleanArchitectureDemo.Domain.Entities;
using System.Collections.Generic;
namespace CleanArchitectureDemo.API.Controllers
{
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
private readonly UserService _userService;
public UserController(UserService userService)
{
_userService = userService;
}
[HttpGet]
public IEnumerable<User> Get() => _userService.GetUsers();
[HttpGet("{id}")]
public ActionResult<User> Get(int id)
{
var user = _userService.GetUser(id);
if (user == null)
return NotFound();
return user;
}
}
}
6. API/Program.cs – Register Dependencies
using CleanArchitectureDemo.Application.Interfaces;
using CleanArchitectureDemo.Application.UseCases;
using CleanArchitectureDemo.Infrastructure.Repositories;
var builder = WebApplication.CreateBuilder(args);
// Dependency Injection
builder.Services.AddScoped<IUserRepository, InMemoryUserRepository>();
builder.Services.AddScoped<UserService>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.MapControllers();
app.Run();
Thanks
Keep Coding.
Top comments (1)
Clean Architecture has a single rule: dependencies must always point toward the domain.
Many NestJS projects violate this rule, even those that claim to use Clean Architecture.
NestJS has a key feature that is often misused and ends up breaking the dependency rule.
That same feature, when used correctly (as shown in the documentation), allows the rule to be respected without hacks.
The key is to properly understand the rule and know what to do and what not to do. The documentation explains how to apply the correct option.
There are four strategies to implement transaction management in a NestJS project that uses Clean Architecture.
One of these strategies relies on a feature that has existed since Node.js 12.
Each strategy can be analyzed in terms of: