Software Architecture - Clean Architecture
1. What is Layered Architecture?
Layered Architecture divides an application into layers. Each layer has a specific responsibility.
The common three-layer structure is:
| Layer | Responsibility | Example |
|---|---|---|
| Presentation / API | Accept requests and return responses | OrdersController |
| Business / BLL | Validate business rules and perform calculations | OrderService |
| Data Access / DAL | Read and write database data | OrderRepository |
Typical flow:
Controller → Service → Repository → Database
Example: Create an order
-
Controller: Receives product and quantity.
-
Service: Validates quantity and calculates the total.
-
Repository: Saves the order.
-
Controller: Returns
201 Created.
2. Why do we need layers?
-
Separate API, business, and database responsibilities.
-
Avoid putting everything inside controllers.
-
Make code easier to understand and maintain.
-
Reuse business logic across different entry points.
-
Make testing and changes easier.
Important: Creating separate folders or projects is useful only when their responsibilities are respected.
3. Common types of layering
| Type | Meaning |
|---|---|
| Two-layer | Presentation and combined business/data code |
| Three-layer | Presentation, Business, and Data Access |
| N-layer | Additional layers when needed |
| Strict layering | A layer calls only the next lower layer |
| Relaxed layering | A layer may skip an intermediate layer |
Layer versus tier:
-
Layer: Logical separation of code.
-
Tier: Physical separation of deployment.
-
Three projects can run inside one application process.
4. Traditional Layered Architecture: dependency
Typically:
API depends on Business → Business depends on Data Access
public class OrderService
{
private readonly SqlOrderRepository _repository;
public OrderService(SqlOrderRepository repository)
{
_repository = repository;
}
}
-
OrderServiceknows the concreteSqlOrderRepository. -
Business code is therefore coupled to a data-access implementation.
-
Traditional layered applications can also use interfaces to reduce this coupling.
5. What is Clean Architecture?
Clean Architecture separates responsibilities while protecting business logic from database, UI, and external-service details.
Its main rule:
Code dependencies point inward toward the business core.
A common .NET structure uses four layers:
| Layer | Responsibility | Typical contents |
|---|---|---|
| Domain | Business objects and rules | Entities, value objects |
| Application | Coordinate business operations | Services, DTOs, interfaces |
| Infrastructure | Implement technical operations | EF Core, repositories, email |
| Presentation / API | Handle HTTP requests and responses | Controllers, middleware |
Clean Architecture is also layered. Its defining feature is the dependency rule.
6. Domain Layer
Purpose: Define the business and protect its rules.
Contains:
-
Entities: Objects with identity, such as
Order. -
Value objects: Values such as
MoneyorAddress. -
Business rules: Quantity must be positive.
-
Optional domain services and events for more complex applications.
public class Order
{
public int Quantity { get; private set; }
public decimal UnitPrice { get; private set; }
public decimal Total => Quantity * UnitPrice;
public Order(int quantity, decimal unitPrice)
{
if (quantity <= 0)
throw new ArgumentException(
"Quantity must be positive.");
if (unitPrice <= 0)
throw new ArgumentException(
"Price must be positive.");
Quantity = quantity;
UnitPrice = unitPrice;
}
}
Key points:
-
No controller or HTTP code.
-
No SQL or EF Core implementation code.
-
No reference to Application or Infrastructure.
-
Rules work consistently from an API, background job, or other entry point.
7. Application Layer
Purpose: Coordinate a use case.
A use case is an operation such as:
-
Create an order.
-
Cancel an order.
-
Transfer money.
-
Publish an article.
Contains:
-
Application services or handlers.
-
DTOs.
-
Interfaces for required operations.
-
Use-case validation and authorization.
Repository interface — defined in Application:
public interface IOrderRepository
{
Task SaveAsync(Order order);
}
Application service:
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public async Task CreateAsync(
int quantity,
decimal unitPrice)
{
var order = new Order(quantity, unitPrice);
await _repository.SaveAsync(order);
}
}
What happens here?
-
Create an order using Domain rules.
-
Ask the repository to save it.
-
Remain unaware of the database implementation.
Why does Application reference Domain?
Because it uses Domain entities and business rules, such as:
var order = new Order(quantity, unitPrice);
8. Infrastructure Layer
Purpose: Implement database and external-system operations.
Contains:
-
EF Core
DbContext. -
Repository implementations.
-
Database mappings and migrations.
-
Email, file-storage, and external API integrations.
public class SqlOrderRepository : IOrderRepository
{
private readonly AppDbContext _db;
public SqlOrderRepository(AppDbContext db)
{
_db = db;
}
public async Task SaveAsync(Order order)
{
_db.Orders.Add(order);
await _db.SaveChangesAsync();
}
}
Key points:
-
Implements interfaces defined in the inner layers.
-
Knows how to communicate with SQL Server.
-
Depends on Application and Domain.
-
Application does not depend on this implementation.
Code snippets illustrate responsibilities; EF Core context and entity mapping are omitted.
9. Presentation / API Layer
Purpose: Convert HTTP requests into application operations.
Contains:
-
Controllers or endpoints.
-
Request models.
-
Routing and model binding.
-
Authentication integration.
-
HTTP response and error handling.
public record CreateOrderRequest(
int Quantity,
decimal UnitPrice);
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly OrderService _service;
public OrdersController(OrderService service)
{
_service = service;
}
[HttpPost]
public async Task<IActionResult> Create(
CreateOrderRequest request)
{
await _service.CreateAsync(
request.Quantity,
request.UnitPrice);
return StatusCode(201);
}
}
Key points:
-
Keep controllers small.
-
Delegate business operations to Application.
-
Map errors to suitable HTTP responses through an exception handler.
-
Keep
IActionResultandHttpContextout of Domain and Application.
Example simplification: A real purchase API should obtain the product price from server-side data rather than trusting the client.
10. Project dependencies
An arrow means “references / depends on.”
flowchart TD
API["API"] --> APP["Application"]
INF["Infrastructure"] --> APP
APP --> DOM["Domain"]
INF --> DOM
API -.->|"DI registration only"| INF
| Project | References |
|---|---|
| Domain | No other solution project |
| Application | Domain |
| Infrastructure | Application and Domain |
| API | Application; Infrastructure for startup registration |
Mandatory rule: Domain and Application must not depend on Infrastructure implementations.
11. How does Application call Infrastructure without referencing it?
Through an interface and Dependency Injection.
Register the implementation in Program.cs:
builder.Services.AddScoped<
IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<OrderService>();
At runtime:
-
The API calls
OrderService. -
OrderServicecallsIOrderRepository. -
DI has supplied a
SqlOrderRepository. -
SqlOrderRepositorysaves the data.
Application knows the contract; DI supplies the implementation.
12. Dependency Injection versus Dependency Inversion
| Concept | Meaning |
|---|---|
| Dependency Injection — DI | Dependencies are supplied from outside a class |
| Dependency Inversion — DIP | Business code depends on abstractions, and technical implementations implement them |
// Injection, but coupled to a concrete implementation.
OrderService(SqlOrderRepository repository)
// Injection using an Application-owned abstraction.
OrderService(IOrderRepository repository)
Interface ownership matters: Putting an interface in Infrastructure and referencing it from Application still creates an outward dependency.
13. Where should validation go?
| Validation | Layer |
|---|---|
| Invalid JSON or request format | API |
| Customer must exist before placing an order | Application |
| Quantity must be positive | Domain |
| Unique key and foreign-key integrity | Database |
Domain versus Application:
-
Domain: “What business rules must always hold?”
-
Application: “What steps complete this operation?”
14. Layered versus Clean Architecture
| Point | Traditional Layered | Clean |
|---|---|---|
| Main focus | Separate responsibilities | Separate responsibilities and protect the business core |
| Business dependency | Often depends on Data Access | Depends on inner-owned interfaces |
| Database implementation | Lower layer | Outer infrastructure |
| Setup | Usually simpler | More boundaries and contracts |
| Testing | Depends on coupling | Core can be tested without real infrastructure |
| Suitable example | Straightforward CRUD | Complex, long-lived business workflows |
Both can run as one monolithic application. Neither automatically improves performance or scalability.
15. Testing and transactions
-
Domain tests: Verify business rules and calculations.
-
Application tests: Verify workflows using fake dependencies.
-
Infrastructure tests: Verify actual database queries and mappings.
-
API tests: Verify HTTP behavior, validation, and authorization.
For transactions:
-
Operations that must succeed together need one clear transaction boundary.
-
Avoid independently committing each repository call in a multi-step operation.
-
A SQL transaction cannot generally roll back an external payment or email.
16. What is optional?
Clean Architecture does not require:
-
MediatR.
-
CQRS.
-
AutoMapper.
-
A generic repository.
-
A separate Unit of Work wrapper.
-
Microservices.
-
Exactly four projects.
Use these only when they solve a real problem.
17. Key points
-
Layered Architecture separates responsibilities.
-
Clean Architecture protects business rules through inward dependencies.
-
Domain contains business entities and rules.
-
Application coordinates use cases and defines required contracts.
-
Infrastructure implements database and external-system operations.
-
API handles HTTP requests and responses.
-
Application references Domain because it uses its business model.
-
Infrastructure implements Application interfaces.
-
DI connects interfaces to implementations at startup.
-
Runtime execution flow differs from project dependency direction.
-
Keep business logic out of controllers.
-
Keep database implementation details out of the business core.
-
Choose the simplest structure that supports the application’s complexity.