Skip to content

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.

┌─────────────────────────────┐
│ 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.

LayerResponsibilityExample Technologies
PresentationUI, user interactionReact, Angular, Razor Pages
ApplicationUse cases, orchestrationASP.NET Controllers, MediatR
DomainBusiness rules, entitiesC# domain classes, services
InfrastructureDB, file system, external APIsEF Core, HttpClient, Redis
// Domain layer — pure business logic, no dependencies
public 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 cases
public 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 interfaces
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();
}
}

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>();

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

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.

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();
}