Layered Architecture — N-Tier Explained
A practical guide to Layered (N-Tier) Architecture: separating presentation, business logic, and data layers with clear responsibilities and dependency rules.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Layered Architecture (also called N-Tier) is the most common architectural pattern in enterprise applications. It divides the application into horizontal layers, each with a specific responsibility. The classic three-tier model separates Presentation, Business Logic, and Data Access. This separation makes the system easier to understand, test, and maintain — though it can also introduce unnecessary abstraction if over-applied.
When to Use
-
For alternatives, see Clean Architecture.
-
Building traditional enterprise web or desktop applications
-
Team structure mirrors technical specialization (frontend, backend, DB)
-
Business logic is moderately complex but not rapidly changing
-
You need a well-understood, proven architecture with abundant examples
-
The application is not expected to change its delivery mechanism (web vs mobile vs API)
The Classic Three Layers
| Layer | Responsibility | Example Components |
|---|---|---|
| Presentation | UI rendering, input validation, routing | Controllers, Views, ViewModels, DTOs |
| Business Logic | Domain rules, calculations, workflows | Services, Entities, Validators |
| Data Access | Persistence, querying, transactions | Repositories, ORM mappings, SQL |
Dependency Direction
In strict layered architecture, a layer can only depend on the layer directly below it.
Presentation Layer
↓ (depends on)
Business Logic Layer
↓ (depends on)
Data Access Layer
↓ (depends on)
Database
Example Implementation
// Presentation Layer — Controller
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
public OrderController(IOrderService orderService) =>
_orderService = orderService;
[HttpPost]
public async Task<ActionResult<OrderDto>> Create(CreateOrderRequest request)
{
var dto = await _orderService.CreateOrderAsync(request.ProductId, request.Quantity);
return CreatedAtAction(nameof(Get), new { id = dto.Id }, dto);
}
}
// Business Logic Layer — Service
public class OrderService : IOrderService
{
private readonly IOrderRepository _repository;
private readonly IProductRepository _productRepository;
public OrderService(IOrderRepository repository, IProductRepository productRepository)
{
_repository = repository;
_productRepository = productRepository;
}
public async Task<OrderDto> CreateOrderAsync(int productId, int quantity)
{
var product = await _productRepository.GetByIdAsync(productId);
if (product.Stock < quantity)
throw new BusinessException("Insufficient stock");
var order = new Order { ProductId = productId, Quantity = quantity, Total = product.Price * quantity };
await _repository.AddAsync(order);
return new OrderDto(order);
}
}
// Data Access Layer — Repository
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public OrderRepository(AppDbContext context) => _context = context;
public async Task AddAsync(Order order)
{
_context.Orders.Add(order);
await _context.SaveChangesAsync();
}
public async Task<Order> GetByIdAsync(int id) =>
await _context.Orders.FindAsync(id);
}
Relaxed vs Strict Layering
| Style | Rule | Trade-off |
|---|---|---|
| Strict | Layer N only calls Layer N-1 | Cleaner but more abstraction layers |
| Relaxed | Layer N can call any layer below | Less code, but harder to trace dependencies |
Common Variations
- Two-tier: Client directly accesses database (legacy desktop apps)
- Three-tier: Presentation → Business → Data (most common)
- Four-tier: Presentation → Application → Domain → Infrastructure (Onion/Clean influence)
Common Mistakes
- Business logic leaking into controllers — thin controllers, fat services
- Direct database access from presentation — breaks encapsulation and testability
- Circular dependencies between layers — use dependency injection to prevent
- Anemic domain model — entities with only getters/setters and all logic in services
- DTO explosion — creating separate DTOs for every layer transition without need
Troubleshooting
- High latency between services: trace the request path. Look for synchronous chains, missing caching, and oversized payloads that cross network boundaries.
- Single point of failure: identify components without redundancy. Add replicas, failover, or circuit breakers before scaling traffic.
- Unexpected coupling between services: review shared databases, libraries, and schemas. Bound contexts should own their data and expose stable interfaces.
- Cost spikes after scaling: right-size instances and use autoscaling with limits. Reserved capacity or spot instances can reduce steady-state spend.
- Difficult to reason about the system: maintain architecture decision records and service dependency maps. Use observability to validate the diagrams.
FAQ
Is Layered Architecture outdated? No, it remains valid for many applications. However, for highly complex domains or systems needing frequent delivery mechanism changes, Onion/Clean/Hexagonal architectures provide better isolation.
How do I test a layered application? Unit test each layer in isolation by mocking the layer below. Integration tests verify the wiring between layers. End-to-end tests validate the full stack.
Can microservices use layered architecture? Yes. Each microservice can internally use layered architecture while communicating via APIs. The layering is an internal organization pattern, not an inter-service pattern.
How do I get started with this in an existing project?
Start with a small, isolated part of your codebase. Apply the concepts from this guide to one module or service. Measure the impact, then expand to other areas.
What tools do I need?
The tools mentioned throughout this guide are listed in each section. Most are open-source and widely adopted. Check the related resources for setup instructions.
How do I measure success after implementing this?
Define clear metrics before starting: performance benchmarks, error rates, or maintainability indicators. Compare before and after. Iterate based on the data, not on assumptions.
Advanced Topics
Detailed Scenario: E-commerce App with Four Layers
Project: E-commerce .NET 8
Layer structure:
src/
ECommerce.Web/ # Presentation (Controllers, Views)
ECommerce.Application/ # Application (Services, DTOs, Validators)
ECommerce.Domain/ # Domain (Entities, Value Objects, Rules)
ECommerce.Infrastructure/ # Infrastructure (EF Core, Email, Cache)
Flow: Create order
1. Web: OrderController.Create(CreateOrderRequest)
- Validate input with DataAnnotations
- Map to CreateOrderCommand
- Call _orderService.CreateOrderAsync(cmd)
2. Application: OrderService.CreateOrderAsync(cmd)
- Check stock via _productRepository
- Calculate total, discounts, taxes
- Create Order entity (logic in domain)
- Persist via _orderRepository
- Publish OrderCreated event
- Return OrderDto
3. Domain: Order.Create(customerId, items)
- Apply business rules: min 1 item,
max 100 items, total > 0
- Set status = Pending
- Set created date
4. Infrastructure: OrderRepository.AddAsync(order)
- EF Core maps entity to Orders table
- SaveChangesAsync persists
- Publish event to RabbitMQ via outbox
Dependency rules:
Web -> Application -> Domain
Infrastructure -> Domain (implements domain interfaces)
Domain depends on nothing (pure, no external references)
Testing per layer:
| Layer | Test type | Tool |
|-------|-----------|------|
| Domain | Pure unit | xUnit, no mocks |
| Application | Unit with repo mocks | xUnit + Moq |
| Infrastructure | Integration with Testcontainers | xUnit + Testcontainers |
| Web | Integration with TestServer | xUnit + Web.ApplicationFactory |
How do I avoid the anemic domain model?
Move business logic into domain entities. An Order entity should have methods like AddItem(), CalculateTotal(), Cancel(). If all logic lives in OrderService and Order only has getters/setters, you have an anemic model. The service should coordinate, not contain rules. Rules belong to the entity that governs them. This makes rules testable without mocks and reusable across services.
Should I use dependency injection in every layer?
Yes, but with different purposes. In the application layer, DI coordinates services and repositories. In the domain layer, avoid DI: the domain should be pure and constructible with new. In infrastructure, DI configures concrete implementations (EF Core, Redis, SMTP). Use dependency composition at the entry point (Program.cs) to wire everything together.
End of document. Review and update quarterly.
Common Production Pitfalls
- Treating the guide as a checklist to complete once rather than a practice to evolve.
- Adopting every recommendation at once instead of starting with one measured change.
- Skipping the maturity assessment and forcing advanced practices on an unprepared team.
- Not updating runbooks and on-call expectations as new practices are introduced.
- Ignoring real incident data when prioritizing which parts of the guide to apply first.
- Failing to assign an owner who reviews decisions quarterly.
- Copying examples without adapting them to the team’s actual tooling and constraints.
- Forgetting to measure outcomes before adding the next improvement.
Related Resources
Onion Architecture — Dependency Inversion in Practice
A practical guide to Onion Architecture: organizing code around the domain model, enforcing dependency direction inward, and isolating infrastructure from business logic.
GuideVertical Slice Architecture: Feature-First Organization
A practical guide to Vertical Slice Architecture: organizing code by feature instead of technical concern, reducing cross-layer navigation and improving cohesion.
PatternRepository Pattern
Abstract data access logic behind a clean interface. An architectural design pattern for testable, maintainable data layers.