When applications grow, traditional 3-tier architectures (UI → BLL → DAL) often collapse into spaghetti code where the business rules become hopelessly tangled with UI frameworks and database technologies. Clean Architecture (aka Onion or Hexagonal Architecture) flips dependencies inward, ensuring your core business logic knows absolutely nothing about databases, frameworks, or APIs.
The single most important principle of Clean Architecture is The Dependency Rule: Dependencies can only flow inward. Inner layers must have zero knowledge of outer layers. You achieve this by heavily utilizing Interfaces and Dependency Injection.
┌────────────────────────────────────────────────────────┐
│ Presentation (ASP.NET Core MVC) │ <-- Outermost layer
│ ┌────────────────────────────────────────────────────┐ │
│ │ Infrastructure │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ Application │ │ │
│ │ │ ┌────────────────────────────────────┐ │ │ │
│ │ │ │ Domain (Core) │ │ │ │
│ │ │ └────────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
This is the center. It has zero dependencies on any other layer or framework (no EF Core, no ASP.NET, no NuGets if possible). It contains absolute business truth.
Order, Customer).Money, Address).IProductRepository, but does not implement it.This layer implements "Use Cases". It orchestrates business workflows using the Domain entities. It depends only on the Domain layer.
OrderProcessingService.IEmailService, but doesn't write the SMTP code.This is where the actual "dirty work" happens. It depends on the Application and Domain layers. This is where frameworks live.
DbContext, Migrations.IProductRepository.IEmailService (SendGrid API), IPaymentGateway (Stripe API).The entry point. It depends on Application and Infrastructure (for DI registration). It translates HTTP requests into Application use cases.
Program.cs, Dependency Injection setup, Middleware.MyApp.Domain)public interface IOrderRepository
{
// The core defines WHAT it needs, not HOW it happens
Task<Order> GetByIdAsync(int id);
Task SaveAsync(Order order);
}
MyApp.Application)public class CheckoutService
{
private readonly IOrderRepository _orderRepo;
private readonly IPaymentGateway _paymentGateway; // Interface defined in Application
// Uses Dependency Injection to heavily decouple code
public CheckoutService(IOrderRepository orderRepo, IPaymentGateway paymentGateway)
{
_orderRepo = orderRepo;
_paymentGateway = paymentGateway;
}
public async Task ProcessOrderAsync(int orderId)
{
// 1. Retrieves Domain Entity utilizing the abstracted interface
var order = await _orderRepo.GetByIdAsync(orderId);
// 2. Execute core domain logic
if (!order.CanBeProcessed()) throw new DomainException("Order is invalid.");
// 3. Utilize abstracted external infrastructure
await _paymentGateway.ChargeAsync(order.TotalAmount);
// 4. Save state
order.MarkAsPaid();
await _orderRepo.SaveAsync(order);
}
}
MyApp.Infrastructure)// The ONLY project that references Microsoft.EntityFrameworkCore
public class SqlOrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _context;
public SqlOrderRepository(ApplicationDbContext context) => _context = context;
public async Task<Order> GetByIdAsync(int id)
=> await _context.Orders.FindAsync(id);
public async Task SaveAsync(Order order)
{
_context.Update(order);
await _context.SaveChangesAsync();
}
}
// Payment implementation utilizing an external NuGet package (e.g. Stripe)
public class StripePaymentGateway : IPaymentGateway
{
public async Task ChargeAsync(decimal amount) { /* ... */ }
}
MyApp.Web)// Program.cs orchestrates the Dependency Injection wire-up
// This is where we bind the Interfaces to their Infrastructure implementations
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IPaymentGateway, StripePaymentGateway>();
builder.Services.AddScoped<CheckoutService>();
Why do we do this? Suppose 2 years from now, SQL Server is too expensive, and the CTO demands a switch to MongoDB. Because IOrderRepository shields your Application and Domain logic, you do not change a single line of business logic. You simply create a MongoOrderRepository in the Infrastructure, update one line in Program.cs, and the entire app seamlessly transitions databases.
Q: "In a standard N-Tier architecture, the UI depends on the BLL (Business Logic Layer), and the BLL depends on the DAL (Data Access Layer). How is Clean Architecture different?"
Architect Answer: "Traditional N-Tier has a fatal flaw: the Business Logic Layer physically depends on the Data Access Layer (e.g. Entity Framework). This means database technicalities inevitably leak into business rules, making the business layer untestable without a database connection. Clean Architecture uses the Dependency Inversion Principle. The Domain/Application layers don't reference the Infrastructure; instead, they declare Interfaces. The Infrastructure layer references the Application layer to *implement* those interfaces. By pointing the dependency arrows inward, the core business logic remains pristine, framework-agnostic, and 100% unit-testable in isolation using mock infrastructures."