Layered (N-Tier) Architecture
Layered architecture divides an application into horizontal layers, each with a distinct responsibility. It is the most widely used architectural pattern and forms the basis of most enterprise applications.
The Classic Three-Tier Model
Section titled “The Classic Three-Tier Model”┌─────────────────────────────┐│ Presentation Layer │ UI, controllers, API endpoints├─────────────────────────────┤│ Business Logic Layer │ Services, domain rules, workflows├─────────────────────────────┤│ Data Access Layer │ Repositories, ORM, database calls└─────────────────────────────┘Each layer only communicates with the layer directly below it. No layer skips a layer.
Four-Tier Model (Common in Enterprise)
Section titled “Four-Tier Model (Common in Enterprise)”| Layer | Responsibility | Example Technologies |
|---|---|---|
| Presentation | UI, user interaction | React, Angular, Razor Pages |
| Application | Use cases, orchestration | ASP.NET Controllers, MediatR |
| Domain | Business rules, entities | C# domain classes, services |
| Infrastructure | DB, file system, external APIs | EF Core, HttpClient, Redis |
C# Example
Section titled “C# Example”// Domain layer — pure business logic, no dependenciespublic class Order{ public Guid Id { get; private set; } public List<OrderItem> Items { get; private set; } = new(); public decimal Total => Items.Sum(i => i.Price * i.Quantity);
public void AddItem(Product product, int quantity) { if (quantity <= 0) throw new ArgumentException("Quantity must be positive"); Items.Add(new OrderItem(product.Id, product.Price, quantity)); }}
// Application layer — orchestrates use casespublic class PlaceOrderHandler{ private readonly IOrderRepository _orders; private readonly IProductRepository _products;
public PlaceOrderHandler(IOrderRepository orders, IProductRepository products) { _orders = orders; _products = products; }
public async Task<Guid> HandleAsync(PlaceOrderCommand cmd) { var order = new Order(); foreach (var line in cmd.Lines) { var product = await _products.GetByIdAsync(line.ProductId); order.AddItem(product, line.Quantity); } await _orders.SaveAsync(order); return order.Id; }}
// Infrastructure layer — implements interfacespublic 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(); }}Dependency Rule
Section titled “Dependency Rule”Inner layers must not depend on outer layers. Use interfaces to invert dependencies:
// Domain defines the interface (inner layer)public interface IOrderRepository{ Task SaveAsync(Order order); Task<Order?> GetByIdAsync(Guid id);}
// Infrastructure implements it (outer layer)public class EfOrderRepository : IOrderRepository { ... }Register in DI container:
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();When to Use Layered Architecture
Section titled “When to Use Layered Architecture”Good fit for:
- CRUD-heavy enterprise applications
- Teams familiar with MVC/repository pattern
- Applications with a single database
- Medium-complexity business logic
Consider alternatives when:
- Business logic is very complex → use Domain-Driven Design
- Need independent scaling of components → use Microservices
- High read/write ratio difference → use CQRS
Common Pitfalls
Section titled “Common Pitfalls”Fat service layer — Services grow to thousands of lines because all logic lives there. Fix: apply Single Responsibility, split into smaller focused services.
Anemic domain model — Domain classes are just data bags with no behavior. Fix: move business rules into entity methods.
Skipping layers — A controller calls a repository directly. Fix: enforce the rule in code reviews and architecture tests.
Architecture Tests (ArchUnitNET)
Section titled “Architecture Tests (ArchUnitNET)”Enforce layer dependencies automatically:
[Test]public void Domain_should_not_depend_on_Infrastructure(){ var result = Types.InAssembly(DomainAssembly) .ShouldNot() .HaveDependencyOn("MyApp.Infrastructure") .GetResult();
result.IsSuccessful.Should().BeTrue();}