LinhGo Labs
LinhGo Labs
Clean Architecture in .NET Overall

Clean Architecture in .NET Overall

The overall about Clean Architecture in .NET: Learn SOLID principles, dependency inversion, and domain-driven design to build maintainable, testable, and scalable applications with proper separation of concerns.

Clean Architecture is a software design philosophy introduced by Robert C. Martin (Uncle Bob) that emphasizes separation of concerns and independence from external frameworks, UI, and databases.

The main goal is to create systems that are:

  • Independent of Frameworks: The architecture doesn’t depend on the existence of some library of feature-laden software
  • Testable: Business rules can be tested without the UI, database, web server, or any external element
  • Independent of UI: The UI can change easily, without changing the rest of the system
  • Independent of Database: You can swap out SQL Server, Oracle, MongoDB, etc., without affecting business rules
  • Independent of any external agency: Business rules don’t know anything at all about the outside world

In traditional n-tier architecture, we often see:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Presentation  โ”‚  โ† Depends on Business Logic
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Business Logic  โ”‚  โ† Depends on Data Access
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚   Data Access   โ”‚  โ† Depends on Database
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚    Database     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Problems:

  • โŒ Business logic is tightly coupled to data access
  • โŒ Hard to test without a database
  • โŒ Difficult to change database or framework
  • โŒ Business rules get polluted with infrastructure concerns
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚                                 โ”‚
        โ”‚    ๐ŸŽฏ Domain (Entities)        โ”‚  โ† Core Business Logic
        โ”‚    - Pure Business Rules        โ”‚     No Dependencies!
        โ”‚    - Domain Events              โ”‚
        โ”‚                                 โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚                                 โ”‚
        โ”‚    ๐Ÿ“‹ Application (Use Cases)  โ”‚  โ† Application Logic
        โ”‚    - CQRS Commands & Queries    โ”‚     Depends only on Domain
        โ”‚    - Interfaces                 โ”‚
        โ”‚                                 โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚                                 โ”‚
        โ”‚  ๐Ÿ”ง Infrastructure             โ”‚  โ† Implementation Details
        โ”‚    - Repositories               โ”‚     Implements Application
        โ”‚    - Database (EF Core)         โ”‚     Interfaces
        โ”‚    - External Services          โ”‚
        โ”‚                                 โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚                                 โ”‚
        โ”‚  ๐ŸŒ Presentation (WebAPI)      โ”‚  โ† User Interface
        โ”‚    - Controllers                โ”‚     Orchestrates everything
        โ”‚    - HTTP/REST                  โ”‚
        โ”‚                                 โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Benefits:

  • โœ… Business logic is independent and testable
  • โœ… Can swap out database without touching business rules
  • โœ… Can swap out UI (Web API โ†’ gRPC โ†’ GraphQL) easily
  • โœ… Clear boundaries and responsibilities

Dependencies can only point INWARD. Nothing in an inner circle can know anything about something in an outer circle.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚         Frameworks & Drivers            โ”‚  โ† Outermost
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚   Interface Adapters              โ”‚  โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚  โ”‚
โ”‚  โ”‚  โ”‚   Application Business Rules โ”‚  โ”‚  โ”‚
โ”‚  โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚  โ”‚  โ”‚
โ”‚  โ”‚  โ”‚  โ”‚  Enterprise Business  โ”‚   โ”‚  โ”‚  โ”‚
โ”‚  โ”‚  โ”‚  โ”‚  Rules (Entities)     โ”‚   โ”‚  โ”‚  โ”‚  โ† Innermost
โ”‚  โ”‚  โ”‚  โ”‚      ๐ŸŽฏ CORE          โ”‚   โ”‚  โ”‚  โ”‚
โ”‚  โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚  โ”‚  โ”‚
โ”‚  โ”‚  โ”‚            โ–ฒ                  โ”‚  โ”‚  โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚  โ”‚
โ”‚  โ”‚               โ”‚                     โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                  โ”‚                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
            All dependencies
            point INWARD โžก๏ธ

Clean Architecture heavily relies on SOLID:

  • Single Responsibility Principle - One class, one responsibility
  • Open/Closed Principle - Open for extension, closed for modification
  • Liskov Substitution Principle - Subtypes must be substitutable for base types
  • Interface Segregation Principle - Many specific interfaces over one general
  • Dependency Inversion Principle โญ (Most Important!) - Depend on abstractions, not concretions

๐Ÿ“š Want to dive deeper? Check out our comprehensive guide: SOLID Principles and Domain-Driven Design Explained

  • Entities: Objects with identity and lifecycle
  • Value Objects: Immutable objects defined by their values
  • Aggregates: Cluster of entities and value objects
  • Domain Events: Something that happened in the domain
  • Repositories: Abstraction for data access

๐Ÿ“š Want to dive deeper? Check out our comprehensive guide: SOLID Principles and Domain-Driven Design Explained


Purpose: Contains enterprise business rules and domain models. The Domain layer is the heart of your application - it contains the core business logic and rules that define what your system does. Think of it as the rulebook of your business.

Characteristics:

  • โœ… No dependencies on other layers
  • โœ… No framework dependencies
  • โœ… Pure C# classes
  • โœ… Contains entities, value objects, domain events
  • โœ… Encapsulates business logic

What goes here:

  • Entities (Aggregate Roots)
  • Value Objects
  • Domain Events
  • Enums
  • Exceptions specific to domain

Example Structure:

Domain/
โ”œโ”€โ”€ Entities/
โ”‚   โ””โ”€โ”€ Product.cs          โ† Rich domain model
โ”œโ”€โ”€ ValueObjects/
โ”‚   โ””โ”€โ”€ Money.cs            โ† Immutable value object
โ”œโ”€โ”€ DomainEvents/
โ”‚   โ””โ”€โ”€ ProductCreatedEvent.cs
โ””โ”€โ”€ Common/
    โ””โ”€โ”€ BaseEntity.cs       โ† Base classes

๐ŸŒ Real-World Analogy: Imagine an e-commerce store. The Domain layer would contain rules like:

  • “Product price must be greater than zero”
  • “Cannot sell more items than available in stock”
  • “Discounted price cannot exceed original price”
  • “Product must have a unique SKU”

These rules exist regardless of whether customers shop through a mobile app, website, or in-store kiosk.

๐Ÿ“ฆ Key Components:

1. Entities - Objects with unique identity

public class Product
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public string SKU { get; private set; }
    public Money Price { get; private set; }
    public int StockQuantity { get; private set; }
    public ProductStatus Status { get; private set; }
    
    public void UpdatePrice(Money newPrice)
    {
        if (newPrice.Amount <= 0)
            throw new DomainException("Price must be greater than zero");
        
        Price = newPrice;
    }
    
    public void DecreaseStock(int quantity)
    {
        if (quantity > StockQuantity)
            throw new InsufficientStockException($"Only {StockQuantity} items available");
        
        StockQuantity -= quantity;
    }
    
    public void Discontinue()
    {
        Status = ProductStatus.Discontinued;
    }
}

๐ŸŒ Real-world: A specific iPhone 15 Pro (SKU: IPH15P-256-BLK) exists as a unique product even if its price or stock changes.

2. Value Objects - Immutable objects defined by their values

public class Money
{
    public decimal Amount { get; }
    public string Currency { get; }
    
    public Money(decimal amount, string currency)
    {
        if (amount < 0)
            throw new DomainException("Amount cannot be negative");
        
        Amount = amount;
        Currency = currency;
    }
    
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new DomainException("Cannot add different currencies");
        
        return new Money(Amount + other.Amount, Currency);
    }
}

๐ŸŒ Real-world: “$99.99 USD” is the same as another “$99.99 USD” - no identity needed, just the value matters.

3. Domain Events - Important things that happened

public class ProductPriceChangedEvent
{
    public Guid ProductId { get; set; }
    public Money OldPrice { get; set; }
    public Money NewPrice { get; set; }
    public DateTime ChangedAt { get; set; }
}

public class ProductOutOfStockEvent
{
    public Guid ProductId { get; set; }
    public string ProductName { get; set; }
    public DateTime OccurredAt { get; set; }
}

๐ŸŒ Real-world: “Product price changed from $99.99 to $79.99 at 3:45 PM” - used for price history, notifications, analytics, etc.


Purpose: Contains application-specific business rules and use cases. The Application layer is the orchestrator - it coordinates how domain objects work together to accomplish specific tasks. It defines WHAT your application can do, but not HOW it does it.

Characteristics:

  • โœ… Depends only on Domain layer
  • โœ… Defines interfaces for infrastructure
  • โœ… Implements CQRS pattern (Commands & Queries)
  • โœ… Contains DTOs for data transfer
  • โœ… Orchestrates domain logic

What goes here:

  • Use Cases (Commands & Queries)
  • Repository Interfaces
  • Service Interfaces
  • DTOs (Data Transfer Objects)
  • Validators
  • Mappers

Example Structure:

Application/
โ”œโ”€โ”€ UseCases/
โ”‚   โ””โ”€โ”€ Products/
โ”‚       โ”œโ”€โ”€ Commands/
โ”‚       โ”‚   โ””โ”€โ”€ CreateProductCommand.cs
โ”‚       โ””โ”€โ”€ Queries/
โ”‚           โ””โ”€โ”€ GetProductByIdQuery.cs
โ”œโ”€โ”€ Interfaces/
โ”‚   โ”œโ”€โ”€ IProductRepository.cs
โ”‚   โ””โ”€โ”€ IUnitOfWork.cs
โ””โ”€โ”€ DTOs/
    โ””โ”€โ”€ ProductDto.cs

๐ŸŒ Real-World Analogy: Think of a store manager:

  • Domain = product rules and inventory management (price validation, stock tracking)
  • Application = the manager coordinating operations (“Customer wants to buy 5 items, check stock, apply discount, update inventory, send confirmation”)

The manager doesn’t handle the actual database or send emails directly, but orchestrates all the steps.

๐Ÿ“ฆ Key Components:

1. Commands (Write Operations) - Actions that change data

public class CreateProductCommand
{
    public string Name { get; set; }
    public string SKU { get; set; }
    public decimal Price { get; set; }
    public string Currency { get; set; }
    public int InitialStock { get; set; }
}

public class CreateProductHandler
{
    private readonly IProductRepository _productRepo;
    private readonly IUnitOfWork _unitOfWork;
    
    public async Task<Result<Guid>> Handle(CreateProductCommand cmd)
    {
        // 1. Create domain objects with validation
        var money = new Money(cmd.Price, cmd.Currency);
        var product = new Product
        {
            Name = cmd.Name,
            SKU = cmd.SKU,
            Price = money,
            StockQuantity = cmd.InitialStock,
            Status = ProductStatus.Active
        };
        
        // 2. Save using repository
        await _productRepo.AddAsync(product);
        await _unitOfWork.SaveChangesAsync();
        
        // 3. Return result
        return Result<Guid>.Success(product.Id);
    }
}

๐ŸŒ Real-world: “Create new product: iPhone 15 Pro, $999, 100 units in stock”

2. Queries (Read Operations) - Retrieve data without changing it

public class GetProductByIdQuery
{
    public Guid ProductId { get; set; }
}

public class GetProductByIdHandler
{
    private readonly IProductRepository _productRepo;
    
    public async Task<ProductDto> Handle(GetProductByIdQuery query)
    {
        var product = await _productRepo.GetByIdAsync(query.ProductId);
        
        return new ProductDto
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price.Amount,
            Currency = product.Price.Currency,
            StockQuantity = product.StockQuantity
        };
    }
}

๐ŸŒ Real-world: “Show me details of product with ID xyz123”

3. Interfaces - Contracts for what infrastructure must provide

public interface IProductRepository
{
    Task<Product> GetByIdAsync(Guid id);
    Task<List<Product>> GetAllAsync();
    Task AddAsync(Product product);
    Task UpdateAsync(Product product);
    Task<bool> ExistsBySKU(string sku);
}

public interface IEmailService
{
    Task SendLowStockAlert(string productName, int quantity);
}

๐ŸŒ Real-world: “I need something that can save products and send stock alerts, but I don’t care if it uses SQL, MongoDB, Gmail, or SendGrid.”


Purpose: Contains implementation details for external concerns. The Infrastructure layer is where the rubber meets the road - it contains all the technical implementation details for actually storing data, sending emails, calling external APIs, etc. This is the “HOW” layer.

Characteristics:

  • โœ… Implements interfaces defined in Application layer
  • โœ… Contains framework-specific code (EF Core, etc.)
  • โœ… Database access, file system, external APIs
  • โœ… Can be replaced without affecting core logic

What goes here:

  • Repository Implementations
  • DbContext (Entity Framework)
  • External API clients
  • File system access
  • Email services
  • Caching implementations

Example Structure:

Infrastructure/
โ”œโ”€โ”€ Persistence/
โ”‚   โ”œโ”€โ”€ ApplicationDbContext.cs
โ”‚   โ”œโ”€โ”€ Repositories/
โ”‚   โ”‚   โ””โ”€โ”€ ProductRepository.cs
โ”‚   โ””โ”€โ”€ Configurations/
โ”‚       โ””โ”€โ”€ ProductConfiguration.cs
โ””โ”€โ”€ ExternalServices/
    โ””โ”€โ”€ EmailService.cs

๐ŸŒ Real-World Analogy: Back to our store:

  • Domain = product rules and business logic
  • Application = store manager coordinating operations
  • Infrastructure = the actual warehouse system, computer database, email server, and payment processor

You can replace your SQL database with MongoDB, or switch from Gmail to SendGrid, without changing how products work or what operations the store can perform.

๐Ÿ“ฆ Key Components:

1. Repository Implementations - How data is actually stored

public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;
    
    public async Task<Product> GetByIdAsync(Guid id)
    {
        // Using Entity Framework Core with SQL Server
        return await _context.Products
            .FirstOrDefaultAsync(p => p.Id == id);
    }
    
    public async Task<List<Product>> GetAllAsync()
    {
        return await _context.Products
            .Where(p => p.Status == ProductStatus.Active)
            .ToListAsync();
    }
    
    public async Task AddAsync(Product product)
    {
        await _context.Products.AddAsync(product);
    }
    
    public async Task UpdateAsync(Product product)
    {
        _context.Products.Update(product);
    }
}

๐ŸŒ Real-world: Using SQL Server with Entity Framework Core to store products. Tomorrow, you could swap to PostgreSQL, MongoDB, or even Azure Cosmos DB without changing your domain or application layers.

2. External Services - Connecting to third-party systems

public class EmailService : IEmailService
{
    private readonly IConfiguration _config;
    private readonly HttpClient _httpClient;
    
    public async Task SendLowStockAlert(string productName, int quantity)
    {
        // Using SendGrid API
        var message = new
        {
            personalizations = new[]
            {
                new { to = new[] { new { email = "admin@store.com" } } }
            },
            from = new { email = "alerts@store.com" },
            subject = "Low Stock Alert",
            content = new[]
            {
                new { type = "text/plain", value = $"Product '{productName}' has only {quantity} items left!" }
            }
        };
        
        await _httpClient.PostAsJsonAsync("https://api.sendgrid.com/v3/mail/send", message);
    }
}

๐ŸŒ Real-world: Using SendGrid to send low stock alerts. Could easily switch to Mailgun, AWS SES, or any other email provider without touching business logic.

3. Database Configuration - How entities map to tables

public class ApplicationDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    
    protected override void OnModelCreating(ModelBuilder builder)
    {
        // Configure Product table
        builder.Entity<Product>(entity =>
        {
            entity.ToTable("Products");
            entity.HasKey(p => p.Id);
            
            entity.Property(p => p.Name)
                .IsRequired()
                .HasMaxLength(200);
            
            entity.Property(p => p.SKU)
                .IsRequired()
                .HasMaxLength(50);
            
            entity.HasIndex(p => p.SKU)
                .IsUnique();
            
            // Complex type mapping for Money value object
            entity.OwnsOne(p => p.Price, price =>
            {
                price.Property(m => m.Amount).HasColumnName("Price");
                price.Property(m => m.Currency).HasColumnName("Currency");
            });
        });
    }
}

๐ŸŒ Real-world: Mapping Product C# objects to database tables with proper constraints and indexes.

โ“ Why Separate? If you need to:

  • Switch from SQL Server โ†’ PostgreSQL
  • Switch from SendGrid โ†’ Mailgun
  • Add Redis caching for product lookups
  • Change image storage from local disk โ†’ AWS S3
  • Add Elasticsearch for product search

You only modify Infrastructure layer, business logic stays untouched!


Purpose: User interface and entry points to the application. The Presentation layer is the front door of your application - it’s how users interact with your system. It handles user input, translates it into commands/queries, and formats responses back to users.

Characteristics:

  • โœ… Depends on Application and Infrastructure
  • โœ… Orchestrates use cases
  • โœ… Handles HTTP concerns
  • โœ… Maps requests to DTOs
  • โœ… Can be Web API, MVC, Console, etc.

What goes here:

  • Controllers
  • View Models
  • HTTP Request/Response handling
  • Dependency Injection setup
  • Middleware

Example Structure:

WebAPI/
โ”œโ”€โ”€ Controllers/
โ”‚   โ””โ”€โ”€ ProductsController.cs
โ”œโ”€โ”€ Program.cs
โ””โ”€โ”€ appsettings.json

๐ŸŒ Real-World Analogy: Back to our store:

  • Domain = product rules and business logic
  • Application = store manager coordinating operations
  • Infrastructure = warehouse, database, email system
  • Presentation = the store’s customer interface - website, mobile app, POS system, customer service phone line

The same store (business logic) can serve customers through multiple channels without changing how products work.

๐Ÿ“ฆ Key Components:

1. Controllers - Handle HTTP requests

[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;
    
    [HttpPost]
    public async Task<IActionResult> CreateProduct(
        [FromBody] CreateProductRequest request)
    {
        // Translate HTTP request to Command
        var command = new CreateProductCommand
        {
            Name = request.Name,
            SKU = request.SKU,
            Price = request.Price,
            Currency = request.Currency,
            InitialStock = request.InitialStock
        };
        
        // Execute use case
        var result = await _mediator.Send(command);
        
        // Return HTTP response
        if (result.IsSuccess)
            return CreatedAtAction(
                nameof(GetProduct),
                new { id = result.Value },
                new { id = result.Value, message = "Product created successfully" }
            );
        
        return BadRequest(new { error = result.Error });
    }
    
    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(Guid id)
    {
        var query = new GetProductByIdQuery { ProductId = id };
        var product = await _mediator.Send(query);
        
        if (product == null)
            return NotFound(new { error = "Product not found" });
        
        return Ok(product);
    }
    
    [HttpPut("{id}/price")]
    public async Task<IActionResult> UpdatePrice(
        Guid id,
        [FromBody] UpdatePriceRequest request)
    {
        var command = new UpdateProductPriceCommand
        {
            ProductId = id,
            NewPrice = request.Price,
            Currency = request.Currency
        };
        
        var result = await _mediator.Send(command);
        
        if (result.IsSuccess)
            return Ok(new { message = "Price updated successfully" });
        
        return BadRequest(new { error = result.Error });
    }
}

๐ŸŒ Real-world: Like a store cashier or website - accepts orders, processes them, returns confirmations.

2. Request/Response Models - HTTP-specific DTOs

public class CreateProductRequest
{
    [Required]
    [MaxLength(200)]
    public string Name { get; set; }
    
    [Required]
    [MaxLength(50)]
    public string SKU { get; set; }
    
    [Required]
    [Range(0.01, double.MaxValue)]
    public decimal Price { get; set; }
    
    [Required]
    [StringLength(3)]
    public string Currency { get; set; }
    
    [Range(0, int.MaxValue)]
    public int InitialStock { get; set; }
}

public class ProductResponse
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string SKU { get; set; }
    public decimal Price { get; set; }
    public string Currency { get; set; }
    public int StockQuantity { get; set; }
    public string Status { get; set; }
}

๐ŸŒ Real-world: Order forms that customers fill out online or at the counter.

3. Dependency Injection Setup - Wire everything together

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Register Application layer
builder.Services.AddMediatR(cfg => 
    cfg.RegisterServicesFromAssembly(typeof(CreateProductCommand).Assembly));

// Register Infrastructure layer
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

// Add controllers and API behavior
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

๐ŸŒ Real-world: Setting up how all parts of the store work together - connecting warehouse to POS system.

๐Ÿ–ฅ๏ธ Multiple UIs for Same Business Logic:

  • REST API โ†’ Mobile shopping app
  • GraphQL API โ†’ Web storefront
  • gRPC โ†’ Internal microservices
  • Console app โ†’ Inventory management tool
  • Blazor WebAssembly โ†’ Admin dashboard

All use the same Domain and Application layers - same product rules, same business logic!


โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                                                      โ”‚
โ”‚  WebAPI Layer (Presentation)                        โ”‚
โ”‚  โ”œโ”€โ”€ Controllers                                     โ”‚
โ”‚  โ””โ”€โ”€ Program.cs                                      โ”‚
โ”‚       โ”‚                                              โ”‚
โ”‚       โ”‚ references                                   โ”‚
โ”‚       โ–ผ                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”           โ”‚
โ”‚  โ”‚  Application Layer                   โ”‚           โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ Use Cases (Commands/Queries)    โ”‚           โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ Interfaces (IRepository)        โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€ Defines contracts
โ”‚  โ”‚  โ””โ”€โ”€ DTOs                             โ”‚           โ”‚
โ”‚  โ”‚       โ”‚                                โ”‚           โ”‚
โ”‚  โ”‚       โ”‚ references                     โ”‚           โ”‚
โ”‚  โ”‚       โ–ผ                                โ”‚           โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”‚           โ”‚
โ”‚  โ”‚  โ”‚  Domain Layer              โ”‚       โ”‚           โ”‚
โ”‚  โ”‚  โ”‚  โ”œโ”€โ”€ Entities              โ”‚       โ”‚           โ”‚
โ”‚  โ”‚  โ”‚  โ”œโ”€โ”€ Value Objects          โ”‚       โ”‚           โ”‚
โ”‚  โ”‚  โ”‚  โ””โ”€โ”€ Domain Events          โ”‚       โ”‚           โ”‚
โ”‚  โ”‚  โ”‚       NO DEPENDENCIES โญ    โ”‚       โ”‚           โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ”‚           โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜           โ”‚
โ”‚       โ–ฒ                                              โ”‚
โ”‚       โ”‚ implements                                   โ”‚
โ”‚       โ”‚                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”           โ”‚
โ”‚  โ”‚  Infrastructure Layer                โ”‚           โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ Repositories (implements IRepo) โ”‚           โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ DbContext                        โ”‚           โ”‚
โ”‚  โ”‚  โ””โ”€โ”€ External Services                โ”‚           โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜           โ”‚
โ”‚                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

KEY: Inner layers know NOTHING about outer layers!

BenefitDescription
TestabilityBusiness logic can be tested without UI, DB, or external dependencies
MaintainabilityClear boundaries make code easier to understand and modify
FlexibilityEasy to swap out infrastructure (change database, UI framework, etc.)
IndependenceBusiness rules are framework-agnostic
ScalabilityClear structure makes it easier to scale and add features
Team CollaborationDifferent teams can work on different layers independently
Long-term StabilityBusiness logic remains stable even as technology changes
ChallengeDescription
Initial ComplexityMore setup and boilerplate code upfront
Learning CurveRequires understanding of SOLID, DDD, and design patterns
Over-engineeringCan be overkill for simple CRUD applications
More FilesMore layers = more files to navigate
Development TimeTakes longer to set up initially
AbstractionsMultiple layers of abstraction can make debugging harder

Now that you understand the fundamentals of Clean Architecture, explore these related articles:


Questions? Leave a comment below. Happy coding!!!