Skip to main content

What is Clean Architecture?

Clean Architecture, introduced by Robert C. Martin (Uncle Bob), is a software design philosophy that emphasizes:
  • Separation of Concerns: Each layer has a distinct responsibility
  • Dependency Inversion: Dependencies point inward toward business logic
  • Testability: Business logic can be tested without UI, database, or frameworks
  • Framework Independence: Core logic isn’t tied to any specific framework
Clean Architecture Diagram

The Dependency Rule

The fundamental principle of Clean Architecture is the Dependency Rule:
Source code dependencies must point only inward, toward higher-level policies. Nothing in an inner circle can know anything about an outer circle.

How SAPFIAI Implements This

Domain Layer (Core)

   | depends on
   |
Application Layer

   | depends on
   |
Infrastructure Layer

   | depends on
   |
Web Layer (Presentation)

Layer Responsibilities

Domain Layer (Enterprise Business Rules)

The Domain layer is the heart of the application, containing:
  • Entities: Core business objects
  • Value Objects: Immutable domain concepts
  • Domain Events: Business events that occurred
  • Domain Exceptions: Business rule violations
// src/Domain/Entities/Permission.cs
namespace SAPFIAI.Domain.Entities;

public class Permission : BaseEntity
{
    public string Name { get; set; } = string.Empty;
    public string? Description { get; set; }
    public string Module { get; set; } = string.Empty;
    public bool IsActive { get; set; } = true;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    
    // Navigation properties
    public ICollection<RolePermission> RolePermissions { get; set; } 
        = new List<RolePermission>();
}
The Domain layer has zero dependencies on external packages. It’s pure C# code representing your business rules.

Application Layer (Application Business Rules)

The Application layer orchestrates business workflows through:
  • Use Cases: Commands and Queries (CQRS)
  • Interfaces: Abstract contracts for external services
  • DTOs: Data Transfer Objects for layer communication
  • Behaviors: Cross-cutting concerns (validation, logging)
  • Exceptions: Application-specific errors
// src/Application/Common/Interfaces/IApplicationDbContext.cs
namespace SAPFIAI.Application.Common.Interfaces;

public interface IApplicationDbContext
{
    DbSet<Permission> Permissions { get; }
    DbSet<RolePermission> RolePermissions { get; }
    DbSet<AuditLog> AuditLogs { get; }
    
    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
The Application layer defines the interface, but the Infrastructure layer provides the implementation.

Infrastructure Layer (Frameworks & Drivers)

The Infrastructure layer provides concrete implementations:
  • DbContext: Entity Framework database access
  • Repositories: Data access patterns
  • Services: External API integrations (email, SMS)
  • File Storage: Cloud or local file systems
  • Identity: Authentication and authorization
// src/Infrastructure/Data/ApplicationDbContext.cs
namespace SAPFIAI.Infrastructure.Data;

public class ApplicationDbContext 
    : IdentityDbContext<ApplicationUser>, IApplicationDbContext
{
    public ApplicationDbContext(
        DbContextOptions<ApplicationDbContext> options) 
        : base(options) { }

    public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
    public DbSet<Permission> Permissions => Set<Permission>();
    public DbSet<RolePermission> RolePermissions => Set<RolePermission>();
    public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.ApplyConfigurationsFromAssembly(
            Assembly.GetExecutingAssembly());
        base.OnModelCreating(builder);
    }
}
ApplicationDbContext implements IApplicationDbContext from the Application layer.

Web Layer (Interface Adapters)

The Web layer handles HTTP concerns:
  • Endpoints: API route definitions
  • Middleware: Request/response pipeline
  • Filters: Cross-cutting HTTP concerns
  • DTOs: Request/Response models
  • Configuration: Dependency injection setup
// src/Web/Endpoints/Permissions.cs (lines 65-68)
private static async Task<IResult> GetPermissions(
    IMediator mediator, 
    [FromQuery] bool activeOnly = false)
{
    var permissions = await mediator.Send(
        new GetPermissionsQuery { ActiveOnly = activeOnly });
    return Results.Ok(permissions);
}

Dependency Inversion in Action

Let’s see how dependency inversion works:
1

Application defines interface

// src/Application/Common/Interfaces/IEmailService.cs
public interface IEmailService
{
    Task SendEmailAsync(string to, string subject, string body);
}
2

Application uses interface

// Application handler depends on IEmailService
public class RegisterCommandHandler
{
    private readonly IEmailService _emailService;
    
    public RegisterCommandHandler(IEmailService emailService)
    {
        _emailService = emailService;
    }
    
    public async Task Handle(...)
    {
        await _emailService.SendEmailAsync(...);
    }
}
3

Infrastructure implements interface

// src/Infrastructure/Services/BrevoEmailService.cs
public class BrevoEmailService : IEmailService
{
    public async Task SendEmailAsync(string to, string subject, string body)
    {
        // Actual implementation using Brevo API
    }
}
4

Infrastructure registers implementation

// src/Infrastructure/DependencyInjection.cs
services.AddHttpClient<IEmailService, BrevoEmailService>();
The Application layer doesn’t know about Brevo. You could swap to SendGrid, AWS SES, or any other provider without changing application code!

Benefits Realized

1. Testability

// Test the handler with a mock email service
[Test]
public async Task Handle_ValidRequest_SendsEmail()
{
    var mockEmailService = new Mock<IEmailService>();
    var handler = new RegisterCommandHandler(mockEmailService.Object);
    
    await handler.Handle(new RegisterCommand { ... });
    
    mockEmailService.Verify(
        x => x.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), 
        Times.Once);
}

2. Framework Independence

The Domain and Application layers don’t depend on:
  • Entity Framework
  • ASP.NET Core
  • Any specific database
  • Any external library
You can swap these out without touching your business logic.

3. Parallel Development

Teams can work on different layers simultaneously:
  • Frontend team uses the Application interfaces
  • Backend team implements Infrastructure
  • Domain experts work on business logic

4. Migration-Friendly

  • Migrate from SQL Server to PostgreSQL → Change Infrastructure only
  • Migrate from REST API to gRPC → Change Web layer only
  • Add GraphQL alongside REST → Add new Web endpoints

Common Pitfalls

Don’t let the Domain depend on anything!❌ Wrong:
// Domain/Entities/User.cs
using Microsoft.EntityFrameworkCore; // BAD!

public class User : BaseEntity { }
✅ Correct:
// Domain/Entities/User.cs
namespace SAPFIAI.Domain.Entities;

public class User : BaseEntity { }
Don’t bypass the Application layer❌ Wrong:
// Web calling Infrastructure directly
public class PermissionsController
{
    private readonly ApplicationDbContext _context; // BAD!
}
✅ Correct:
// Web calling Application via MediatR
public class Permissions
{
    private static async Task<IResult> GetPermissions(
        IMediator mediator) // GOOD!
    {
        return await mediator.Send(new GetPermissionsQuery());
    }
}

Next Steps

Project Structure

Explore the folder organization

CQRS Pattern

Learn about Commands and Queries

Domain Entities

Deep dive into Domain entities

Dependency Injection

Service registration patterns

Build docs developers (and LLMs) love