In the previous post, we took an overall look at Clean Architecture. In this article, letโs explore a real-world example of how to use Clean Architecture.
๐ Clean Architecture Series:
- Clean Architecture in .NET Overall - Fundamentals and principles
- Common Clean Architecture Mistakes - Learn what to avoid
๐ป GitHub Repository
The complete source code for this implementation is available on GitHub:
๐ Repository: DotNetCleanArchitecture
๐ฆ What’s Included:
- โ Complete Product Management System implementation
- โ All layers: Domain, Application, Infrastructure, WebAPI
- โ CQRS pattern with Commands and Queries
- โ Repository Pattern and Unit of Work
- โ Entity Framework Core with InMemory database
- โ Domain Events implementation
- โ Value Objects (Money)
- โ REST API with Swagger documentation
- โ Unit tests and integration tests
- โ Docker support
Feel free to clone, explore, and experiment with the code!
๐๏ธ Real-World Implementation: Product Management System
Let’s walk through a complete implementation of a Product entity following Clean Architecture principles.
๐ System Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Product Management System โ
โ โ
โ Features: โ
โ โข Create Product with validation โ
โ โข Update Product details โ
โ โข Manage Stock (Add/Remove) โ
โ โข Activate/Deactivate Products โ
โ โข Query Products โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ๐จ Architecture Diagram
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ WebAPI Layer โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ ProductsController โ โ
โ โ โโโ POST /api/products (Create) โ โ
โ โ โโโ GET /api/products (GetAll) โ โ
โ โ โโโ GET /api/products/{id} (GetById) โ โ
โ โ โโโ PUT /api/products/{id} (Update) โ โ
โ โ โโโ DELETE /api/products/{id} (Delete) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โ HTTP Requests โ
โ โผ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Application Layer โ
โ โโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Commands (Write) โ โ Queries (Read) โ โ
โ โ โโโโโโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ CreateProduct โ โ โ โ GetAllProducts โ โ โ
โ โ โ UpdateProduct โ โ โ โ GetProductById โ โ โ
โ โ โ DeleteProduct โ โ โ โโโโโโโโโโโโโโโโโโโ โ โ
โ โ โโโโโโโโโโโโโโโโโโโ โ โโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โ Uses Domain โ Uses Repository โ
โ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Interfaces (Contracts) โ โ
โ โ โข IProductRepository โ โ
โ โ โข IUnitOfWork โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โฒ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ โ
โ Domain Layer โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Product (Aggregate Root) โ โ
โ โ โโโ Properties: Id, Name, SKU, Price, Stock โ โ
โ โ โโโ Methods: โ โ
โ โ โ โข Create() โ Factory Method โ โ
โ โ โ โข UpdateDetails() โ โ
โ โ โ โข AddStock() / RemoveStock() โ โ
โ โ โ โข Activate() / Deactivate() โ โ
โ โ โโโ Domain Events: โ โ
โ โ โข ProductCreated โ โ
โ โ โข ProductUpdated โ โ
โ โ โข ProductStockChanged โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Money (Value Object) โ โ
โ โ โข Amount + Currency โ โ
โ โ โข Immutable โ โ
โ โ โข Equality by value โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Infrastructure Layer โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ ProductRepository (implements IProductRepository) โ โ
โ โ โข GetByIdAsync() โ โ
โ โ โข GetAllAsync() โ โ
โ โ โข AddAsync() โ โ
โ โ โข UpdateAsync() โ โ
โ โ โข DeleteAsync() โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ ApplicationDbContext (EF Core) โ โ
โ โ โข DbSet<Product> โ โ
โ โ โข Entity Configurations โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Database (SQL Server / InMemory) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโCode Walkthrough
1. ๐ฏ Domain Layer: The Heart of the Application
Product Entity (Aggregate Root)
The Product entity encapsulates all business logic and rules:
// src/DotNetCleanArchitecture.Domain/Entities/Product.cs
public sealed class Product : BaseEntity
{
// Properties with private setters (Encapsulation)
public string Name { get; private set; }
public string Sku { get; private set; }
public Money Price { get; private set; } // Value Object
public int StockQuantity { get; private set; }
public bool IsActive { get; private set; }
// Private constructor for EF Core
private Product() { }
// Factory Method - Only way to create a Product
public static Product Create(
string name,
string sku,
Money price,
int stockQuantity)
{
// โ
Business Rules Enforced Here
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Product name cannot be empty");
if (name.Length > 200)
throw new ArgumentException("Name cannot exceed 200 characters");
if (string.IsNullOrWhiteSpace(sku))
throw new ArgumentException("SKU cannot be empty");
var product = new Product
{
Name = name,
Sku = sku,
Price = price,
StockQuantity = stockQuantity,
IsActive = true
};
// โ
Domain Event Published
product.AddDomainEvent(
new ProductCreatedDomainEvent(product.Id, product.Name, product.Sku)
);
return product;
}
// Business Operations
public void AddStock(int quantity)
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive");
StockQuantity += quantity;
SetUpdatedAt();
// Publish domain event
AddDomainEvent(new ProductStockChangedDomainEvent(Id, StockQuantity));
}
public void RemoveStock(int quantity)
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive");
if (StockQuantity < quantity)
throw new InvalidOperationException("Insufficient stock");
StockQuantity -= quantity;
SetUpdatedAt();
AddDomainEvent(new ProductStockChangedDomainEvent(Id, StockQuantity));
}
public void Activate() => IsActive = true;
public void Deactivate() => IsActive = false;
public bool IsInStock() => StockQuantity > 0;
public bool IsAvailable() => IsActive && IsInStock();
}Key Points:
- โ Encapsulation: Private setters prevent invalid state
- โ
Factory Method:
Create()ensures valid object creation - โ Business Rules: Validation inside the entity
- โ Domain Events: Track important business events
- โ Rich Behavior: Methods that make sense in business context
Money Value Object
Value objects represent concepts that are defined by their attributes:
// src/DotNetCleanArchitecture.Domain/ValueObjects/Money.cs
public sealed class Money : ValueObject
{
public decimal Amount { get; private set; }
public string Currency { get; private set; }
private Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
// Factory method with validation
public static Money Create(decimal amount, string currency = "USD")
{
if (amount < 0)
throw new ArgumentException("Amount cannot be negative");
if (string.IsNullOrWhiteSpace(currency))
throw new ArgumentException("Currency cannot be empty");
return new Money(amount, currency.ToUpperInvariant());
}
// Business operations
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException(
"Cannot add money with different currencies"
);
return new Money(Amount + other.Amount, Currency);
}
// Value objects are compared by value, not reference
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
public override string ToString() => $"{Amount:N2} {Currency}";
}Why Value Objects?
- โ Immutability: Cannot be changed after creation
- โ Self-Validation: Business rules enforced
- โ Equality by Value: Two Money objects with same amount/currency are equal
- โ Type Safety: Can’t accidentally mix up price with quantity
2. Application Layer: Use Cases (CQRS)
Create Product Command
Commands handle write operations:
// src/DotNetCleanArchitecture.Application/UseCases/Products/Commands/CreateProductCommand.cs
public sealed class CreateProductCommand
{
private readonly IProductRepository _productRepository;
private readonly IUnitOfWork _unitOfWork;
public CreateProductCommand(
IProductRepository productRepository,
IUnitOfWork unitOfWork)
{
_productRepository = productRepository;
_unitOfWork = unitOfWork;
}
public async Task<ProductDto> ExecuteAsync(
CreateProductDto dto,
CancellationToken cancellationToken = default)
{
// โ
Business validation
if (await _productRepository.SkuExistsAsync(dto.Sku, cancellationToken))
{
throw new InvalidOperationException(
$"Product with SKU '{dto.Sku}' already exists"
);
}
// โ
Use domain factory method
var price = Money.Create(dto.Price, dto.Currency);
var product = Product.Create(
dto.Name,
dto.Sku,
price,
dto.StockQuantity
);
// โ
Persist through repository
await _productRepository.AddAsync(product, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
// โ
Return DTO (not domain entity)
return MapToDto(product);
}
private static ProductDto MapToDto(Product product)
{
return new ProductDto
{
Id = product.Id,
Name = product.Name,
Sku = product.Sku,
Price = product.Price.Amount,
Currency = product.Price.Currency,
StockQuantity = product.StockQuantity,
IsActive = product.IsActive,
CreatedAt = product.CreatedAt
};
}
}Key Points:
- โ Single Responsibility: Only creates products
- โ
Uses Domain Logic: Calls
Product.Create() - โ Repository Pattern: Abstracts data access
- โ Unit of Work: Manages transactions
- โ DTOs: External representation separate from domain
Get Products Query
Queries handle read operations:
// src/DotNetCleanArchitecture.Application/UseCases/Products/Queries/GetAllProductsQuery.cs
public sealed class GetAllProductsQuery
{
private readonly IProductRepository _productRepository;
public GetAllProductsQuery(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<IReadOnlyList<ProductDto>> ExecuteAsync(
CancellationToken cancellationToken = default)
{
var products = await _productRepository.GetAllAsync(cancellationToken);
return products.Select(MapToDto).ToList();
}
private static ProductDto MapToDto(Product product)
{
return new ProductDto
{
Id = product.Id,
Name = product.Name,
Sku = product.Sku,
Price = product.Price.Amount,
Currency = product.Price.Currency,
StockQuantity = product.StockQuantity,
IsActive = product.IsActive,
CreatedAt = product.CreatedAt,
UpdatedAt = product.UpdatedAt
};
}
}CQRS Benefits:
- โ Separation: Read and write models are separate
- โ Optimization: Can optimize queries differently than commands
- โ Scalability: Can scale reads and writes independently
- โ Clarity: Clear intent (command vs query)
3. Infrastructure Layer: Implementation Details
Repository Implementation
// src/DotNetCleanArchitecture.Infrastructure/Persistence/Repositories/ProductRepository.cs
public class ProductRepository : IProductRepository
{
private readonly ApplicationDbContext _context;
public ProductRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<Product?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default)
{
return await _context.Products
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
}
public async Task<IReadOnlyList<Product>> GetAllAsync(
CancellationToken cancellationToken = default)
{
return await _context.Products
.OrderBy(p => p.Name)
.ToListAsync(cancellationToken);
}
public async Task<Product> AddAsync(
Product product,
CancellationToken cancellationToken = default)
{
await _context.Products.AddAsync(product, cancellationToken);
return product;
}
public async Task<bool> SkuExistsAsync(
string sku,
CancellationToken cancellationToken = default)
{
return await _context.Products
.AnyAsync(p => p.Sku == sku, cancellationToken);
}
// ... other methods
}Entity Framework Configuration
// src/DotNetCleanArchitecture.Infrastructure/Persistence/Configurations/ProductConfiguration.cs
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products");
builder.HasKey(p => p.Id);
builder.Property(p => p.Name)
.IsRequired()
.HasMaxLength(200);
builder.Property(p => p.Sku)
.IsRequired()
.HasMaxLength(50);
builder.HasIndex(p => p.Sku)
.IsUnique();
// โ
Value Object as Owned Entity
builder.OwnsOne(p => p.Price, priceBuilder =>
{
priceBuilder.Property(m => m.Amount)
.HasColumnName("Price")
.HasPrecision(18, 2)
.IsRequired();
priceBuilder.Property(m => m.Currency)
.HasColumnName("Currency")
.HasMaxLength(3)
.IsRequired();
});
// โ
Ignore domain events (not persisted)
builder.Ignore(p => p.DomainEvents);
}
}Key Points:
- โ Fluent API: Configuration separate from domain
- โ Owned Entities: Value objects mapped correctly
- โ Constraints: Database constraints mirror business rules
4. Presentation Layer: REST API
Products Controller
// src/DotNetCleanArchitecture.WebAPI/Controllers/ProductsController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly CreateProductCommand _createProductCommand;
private readonly GetAllProductsQuery _getAllProductsQuery;
private readonly GetProductByIdQuery _getProductByIdQuery;
private readonly ILogger<ProductsController> _logger;
public ProductsController(
CreateProductCommand createProductCommand,
GetAllProductsQuery getAllProductsQuery,
GetProductByIdQuery getProductByIdQuery,
ILogger<ProductsController> logger)
{
_createProductCommand = createProductCommand;
_getAllProductsQuery = getAllProductsQuery;
_getProductByIdQuery = getProductByIdQuery;
_logger = logger;
}
/// <summary>
/// Get all products
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(IReadOnlyList<ProductDto>), 200)]
public async Task<ActionResult<IReadOnlyList<ProductDto>>> GetAll(
CancellationToken cancellationToken)
{
try
{
var products = await _getAllProductsQuery.ExecuteAsync(cancellationToken);
return Ok(products);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting all products");
return StatusCode(500, "An error occurred");
}
}
/// <summary>
/// Create a new product
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(ProductDto), 201)]
[ProducesResponseType(400)]
public async Task<ActionResult<ProductDto>> Create(
[FromBody] CreateProductDto dto,
CancellationToken cancellationToken)
{
try
{
var product = await _createProductCommand.ExecuteAsync(
dto,
cancellationToken
);
return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
product
);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Validation error");
return BadRequest(ex.Message);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "Business rule violation");
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating product");
return StatusCode(500, "An error occurred");
}
}
// ... other endpoints
}Controller Responsibilities:
- โ HTTP Concerns: Handle requests/responses
- โ Validation: Input validation (can add FluentValidation)
- โ Orchestration: Call appropriate use cases
- โ Error Handling: Convert exceptions to HTTP status codes
- โ Logging: Log important events
Dependency Injection Setup
// src/DotNetCleanArchitecture.WebAPI/Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
// โ
Register Application Layer
builder.Services.AddApplication();
// โ
Register Infrastructure Layer
builder.Services.AddInfrastructure(builder.Configuration);
var app = builder.Build();
// Configure middleware
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();๐ Request Flow Example
Let’s trace a complete request through all layers:
Creating a Product: Step-by-Step
1๏ธโฃ HTTP Request
โ
โ POST /api/products
โ {
โ "name": "Wireless Mouse",
โ "sku": "MOUSE-001",
โ "price": 29.99,
โ "currency": "USD",
โ "stockQuantity": 50
โ }
โ
โผ
2๏ธโฃ ProductsController (WebAPI Layer)
โ โข Receives HTTP request
โ โข Validates input
โ โข Calls CreateProductCommand
โ
โผ
3๏ธโฃ CreateProductCommand (Application Layer)
โ โข Checks if SKU already exists (business rule)
โ โข Creates Money value object
โ โข Calls Product.Create() factory method
โ
โผ
4๏ธโฃ Product.Create() (Domain Layer)
โ โข Validates business rules
โ โข Creates Product entity
โ โข Publishes ProductCreatedDomainEvent
โ โข Returns valid Product
โ
โผ
5๏ธโฃ ProductRepository.AddAsync() (Infrastructure Layer)
โ โข Adds product to DbContext
โ
โผ
6๏ธโฃ UnitOfWork.SaveChangesAsync() (Infrastructure Layer)
โ โข Commits transaction to database
โ โข Dispatches domain events
โ
โผ
7๏ธโฃ Response Back to Client
โ โข Maps Product to ProductDto
โ โข Returns 201 Created with product data
โ
โโโโบ HTTP Response
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Wireless Mouse",
"sku": "MOUSE-001",
"price": 29.99,
"currency": "USD",
"stockQuantity": 50,
"isActive": true,
"createdAt": "2025-12-21T10:30:00Z"
}โจ Best Practices
โ DO
Keep Domain Pure
// โ Good: Pure domain logic public void AddStock(int quantity) { if (quantity <= 0) throw new ArgumentException("Quantity must be positive"); StockQuantity += quantity; }Use Factory Methods
// โ Good: Factory method enforces rules public static Product Create(string name, string sku, Money price) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name required"); return new Product { Name = name, Sku = sku, Price = price }; }Encapsulate with Private Setters
// โ Good: Cannot be modified from outside public string Name { get; private set; }Use Value Objects
// โ Good: Money is a value object public Money Price { get; private set; } // โ Bad: Primitives can be mixed up public decimal Price { get; set; } public string Currency { get; set; }Define Interfaces in Application Layer
// Application/Interfaces/IProductRepository.cs public interface IProductRepository { Task<Product?> GetByIdAsync(Guid id); }
โ DON’T
Don’t Reference Outer Layers from Inner Layers
// โ BAD: Domain referencing Infrastructure public class Product { private readonly IProductRepository _repository; // WRONG! }Don’t Put Business Logic in Controllers
// โ BAD: Business logic in controller [HttpPost] public async Task<IActionResult> Create(CreateProductDto dto) { if (string.IsNullOrEmpty(dto.Name)) // Business rule in wrong place return BadRequest(); var product = new Product { Name = dto.Name }; // Direct instantiation await _context.Products.AddAsync(product); // Direct DB access await _context.SaveChangesAsync(); return Ok(); }Don’t Expose Domain Entities Directly
// โ BAD: Returning domain entity [HttpGet] public async Task<Product> Get(Guid id) { return await _repository.GetByIdAsync(id); } // โ GOOD: Return DTO [HttpGet] public async Task<ProductDto> Get(Guid id) { var product = await _repository.GetByIdAsync(id); return MapToDto(product); }
๐ค When to Use Clean Architecture
โ Use Clean Architecture When:
- โ Building complex business applications with lots of business rules
- โ Project has a long lifespan (5+ years)
- โ Multiple teams working on different parts
- โ Requirements are likely to change frequently
- โ Need to support multiple platforms (Web, Mobile, Desktop)
- โ Testability is critical
- โ Technology stack might change (database, framework, etc.)
โ ๏ธ Consider Alternatives When:
- โ ๏ธ Building a simple CRUD application with minimal business logic
- โ ๏ธ Rapid prototyping or MVP development
- โ ๏ธ Very small team (1-2 developers)
- โ ๏ธ Short-lived project (< 6 months)
- โ ๏ธ Team is unfamiliar with design patterns and DDD
- โ ๏ธ Time to market is more important than architecture
๐ Summary of Our Implementation
๐ข What We Built
A Product Management System demonstrating:
| Layer | What We Created | Purpose |
|---|---|---|
| Domain | Product entity, Money value object, Domain events | Pure business logic |
| Application | Create/Update/Delete commands, Get queries, DTOs | Use cases and orchestration |
| Infrastructure | EF Core repository, DbContext, configurations | Data access implementation |
| WebAPI | REST API controller, 5 endpoints | HTTP interface |
๐ Key Patterns Used
โ
Clean Architecture โ Layered structure with dependency rule
โ
Domain-Driven Design โ Entities, Value Objects, Events
โ
CQRS โ Commands and Queries separated
โ
Repository Pattern โ Abstract data access
โ
Unit of Work Pattern โ Transaction management
โ
Factory Pattern โ Product.Create() method
โ
Dependency Injection โ Loose coupling throughout
โ
SOLID Principles โ All five principles applied๐ Project Statistics
- 26 C# Files across 4 layers
- 1 Aggregate Root (Product)
- 1 Value Object (Money)
- 3 Domain Events
- 3 Commands (Write operations)
- 2 Queries (Read operations)
- 5 REST Endpoints
- 0 Framework Dependencies in Domain layer
โ๏ธ Business Rules Enforced
- โ Product name is required and max 200 characters
- โ SKU must be unique
- โ Price cannot be negative
- โ Stock quantity cannot be negative
- โ Cannot remove more stock than available
- โ Currency must be valid (3-letter code)
- โ Domain events published for important actions
๐ฏ Conclusion
Clean Architecture provides a robust foundation for building maintainable, testable, and scalable applications. While it requires more upfront investment, the long-term benefits are substantial:
๐ก Key Takeaways
Independence is Key: Business logic should be independent of frameworks, UI, and databases
The Dependency Rule: Dependencies always point inwardโinner layers know nothing about outer layers
Testability: With proper separation, you can test business logic without any infrastructure
Flexibility: Swap out databases, frameworks, or UI without touching core business logic
Maintainability: Clear boundaries make code easier to understand and modify
Team Collaboration: Different teams can work on different layers independently
๐ฐ Is It Worth It?
For complex business applications with long lifespans: Absolutely YES โ
For simple CRUD apps or rapid prototypes: Probably NOT โ
๏ฟฝ Final Thoughts
Clean Architecture is not a silver bullet, but when applied appropriately, it creates systems that are:
- Easy to test
- Easy to understand
- Easy to maintain
- Easy to extend
- Hard to break
The key is knowing when to use it and how much complexity to introduce based on your project’s needs.
๐ Resources
๐ Official Documentation
- Clean Architecture by Robert C. Martin (Uncle Bob)
- Domain-Driven Design by Eric Evans
- Microsoft’s eShopOnContainers (reference implementation)
๐พ Implementation Repository
GitHub Repository: DotNetCleanArchitecture
Project structure:
- Domain:
src/DotNetCleanArchitecture.Domain/ - Application:
src/DotNetCleanArchitecture.Application/ - Infrastructure:
src/DotNetCleanArchitecture.Infrastructure/ - WebAPI:
src/DotNetCleanArchitecture.WebAPI/
Clone and run:
git clone https://github.com/yourusername/DotNetCleanArchitecture.git
cd DotNetCleanArchitecture
dotnet restore
dotnet run --project src/DotNetCleanArchitecture.WebAPI๐ Further Reading
Related Articles:
- Clean Architecture in .NET Overall - Fundamentals and principles
- Common Clean Architecture Mistakes in .NET - Avoid these antipatterns!
Patterns & Principles:
- SOLID Principles
- CQRS Pattern
- Event Sourcing
- Repository Pattern
- Unit of Work Pattern
- Dependency Injection
Questions? Leave a comment below. Happy coding!!!






