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.
๐๏ธ What is Clean Architecture?
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
๐ก Why Use Clean Architecture?
โ The Problem with Traditional Layered Architecture
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
โ The Clean Architecture Solution
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ ๐ฏ 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
๐ Core Principles
1. ๐ The Dependency Rule
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 โก๏ธ2. ๐๏ธ SOLID Principles
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
3. ๐ Domain-Driven Design (DDD)
- 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
๐๏ธ Architecture Layers Explained
๐ฏ Layer 1: Domain (Core)
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.
๐ Layer 2: Application (Use Cases)
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.”
๐ง Layer 3: Infrastructure (Implementation)
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!
๐ Layer 4: Presentation (UI/API)
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!
๐ The Dependency Rule
๐ How Dependencies Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ 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!โ๏ธ Pros and Cons
โ Advantages
| Benefit | Description |
|---|---|
| Testability | Business logic can be tested without UI, DB, or external dependencies |
| Maintainability | Clear boundaries make code easier to understand and modify |
| Flexibility | Easy to swap out infrastructure (change database, UI framework, etc.) |
| Independence | Business rules are framework-agnostic |
| Scalability | Clear structure makes it easier to scale and add features |
| Team Collaboration | Different teams can work on different layers independently |
| Long-term Stability | Business logic remains stable even as technology changes |
โ ๏ธ Disadvantages
| Challenge | Description |
|---|---|
| Initial Complexity | More setup and boilerplate code upfront |
| Learning Curve | Requires understanding of SOLID, DDD, and design patterns |
| Over-engineering | Can be overkill for simple CRUD applications |
| More Files | More layers = more files to navigate |
| Development Time | Takes longer to set up initially |
| Abstractions | Multiple layers of abstraction can make debugging harder |
๐ Continue Learning
Now that you understand the fundamentals of Clean Architecture, explore these related articles:
- ๐ Clean Architecture in .NET Real-World Example - Complete implementation with Product Management System
- โ ๏ธ Common Clean Architecture Mistakes in .NET - Learn what NOT to do and how to fix common antipatterns
Questions? Leave a comment below. Happy coding!!!





