Layered Architecture
Stack code into horizontal layers — Presentation, Business, and Data.
Layered Architecture: A Complete Guide with Examples in 11 Programming Languages
Layered Architecture — also known as N-Tier Architecture — is the most widely used structural pattern in software development. It organizes an application into horizontal layers, where each layer has a clearly defined responsibility and can only communicate with the layer directly below it. The classic form has four layers: Presentation (UI or API), Application (use cases and orchestration), Domain (business rules and entities), and Infrastructure (databases, external services, file systems). Each layer depends only on what is beneath it, never on what is above it.
In this comprehensive guide, we'll explore Layered Architecture with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.
Table of Contents
- What is Layered Architecture?
- Why Use Layered Architecture?
- Layered Architecture Comparison
- Layered Architecture Explained
- Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is Layered Architecture?
Layered Architecture solves the most fundamental challenge in application design: how do you organize growing complexity so that each concern is handled in exactly one place, changes in one area do not cascade unexpectedly into others, and developers can reason about one part of the system without holding the entire codebase in their head?
The naive approach — writing all logic in one place — works for small scripts. The moment a project grows past a few hundred lines, the entanglement begins: database queries appear inside button-click handlers, business rules are duplicated across API endpoints, and a change to the database schema breaks the UI rendering. The application becomes a single fragile mass where every change carries unknown risk.
Layered Architecture solves this by enforcing a simple structural rule: code is divided into horizontal layers, and each layer may only depend on the layer immediately below it. A Presentation layer calls the Application layer. The Application layer calls the Domain layer. The Domain layer calls the Infrastructure layer. No layer may skip over another or call upward. This single rule — enforced by team discipline or package boundaries — prevents the tangling that makes large codebases unmaintainable.
Think of it like a restaurant kitchen organized by station. The front-of-house (Presentation) takes orders from customers and passes them to the expediter. The expediter (Application) coordinates which station handles what. The line cooks (Domain) apply culinary expertise — recipes, timing, technique. The prep team (Infrastructure) handles raw ingredients, storage, and supplies. Each station does its own job; none reaches past the next station to grab ingredients from the walk-in cooler directly.
Why Use Layered Architecture?
Layered Architecture offers several compelling benefits:
- Separation of Concerns: Each layer owns exactly one category of responsibility — UI logic, use case orchestration, business rules, or data access. A developer reading one layer sees only that layer's concepts, not a mixture of HTTP handling and SQL queries
- Predictable Change Impact: A change to the database schema affects only the Infrastructure layer. A change to an API endpoint format affects only the Presentation layer. Because layers are isolated, the blast radius of any change is contained and predictable
- Replaceability: Each layer can be replaced independently. Switch the UI from a web interface to a mobile API without touching the Domain layer. Replace the database from MySQL to PostgreSQL by rewriting only the Infrastructure layer
- Team Parallelism: Frontend developers work in the Presentation layer; backend developers work in Domain and Application layers; database engineers work in the Infrastructure layer — with clear interfaces between layers, teams can work in parallel without constant coordination
- Testability: Each layer can be tested in isolation by mocking the layer below it. Application layer tests mock the Infrastructure layer. Domain layer tests require no external dependencies at all
- Familiarity and Onboarding: Layered Architecture is the most recognized and taught architectural pattern. New team members understand the structure immediately, navigate the codebase confidently from day one, and know exactly where to add new code
Layered Architecture Comparison
Let's compare Layered Architecture with related architectural styles:
| Architecture | Layer Direction | Coupling | Testability | Best For |
|---|---|---|---|---|
| Layered (N-Tier) | Top-down, strict | Medium — each layer knows the one below | Medium — mock layer below | Most applications, enterprise systems, teams new to architecture |
| Hexagonal (Ports & Adapters) | Inward toward domain | Low — domain knows nothing outside | High — fake adapters | Complex domain, multiple input channels, strict TDD |
| MVC | Controller → Model → View | High within one context | Medium | Web apps, simple CRUD |
| Microservices | Service-to-service | Low between services, high within | High per service | Large-scale distributed systems, independent deployability |
| Monolith (Big Ball of Mud) | None — everything calls everything | Very high | Very low | Prototypes only |
Key Distinctions:
- Layered vs. Hexagonal: Layered Architecture allows upper layers to depend on lower layers — the Application layer typically imports the Infrastructure layer directly (or through interfaces). Hexagonal Architecture inverts this: the domain defines interfaces (ports) and infrastructure implements them — the domain never imports infrastructure. Layered is simpler and more common; Hexagonal offers stronger isolation but requires more discipline and abstraction.
- Layered vs. Clean Architecture: Clean Architecture (a refinement of Hexagonal) uses concentric rings instead of horizontal layers and enforces that the innermost ring (domain entities) has zero outward dependencies. A strict Layered Architecture still allows the Infrastructure layer to be directly referenced by the Application layer — Clean Architecture forbids this entirely through Dependency Inversion.
- Layered vs. Microservices: Layered Architecture describes the internal structure of a single deployable application (monolith or service). Microservices describes how to decompose a system into multiple independent deployable services. These are orthogonal — each microservice typically uses a layered or hexagonal architecture internally.
Layered Architecture Explained
Layered Architecture involves four standard layers, each with a precisely defined role:
Core Layers
-
Presentation Layer: The outermost layer — the application's boundary with the outside world. Responsible for receiving input (HTTP requests, CLI arguments, UI events, message queue messages) and returning output (HTTP responses, rendered views, terminal output). Contains no business logic. Translates external data formats (JSON, form data, query parameters) into internal data structures, calls the Application layer, and translates results back into external formats. Examples: REST controllers, GraphQL resolvers, MVC views, CLI command handlers.
-
Application Layer: Orchestrates use cases. Receives calls from the Presentation layer, coordinates domain objects and infrastructure services to fulfill a business operation, and returns results. Does not contain business rules — it delegates all decisions to the Domain layer. Does contain workflow logic: "to place an order, first validate stock availability, then debit the account, then create the order record, then send a confirmation." Examples: service classes, use case handlers, application facades, command handlers.
-
Domain Layer: The heart of the application. Contains all business rules, domain entities, value objects, domain services, and domain events. Has no dependencies on any other layer — it is pure business logic expressed in the programming language's native constructs. The domain layer should be the most stable layer — it changes only when the business itself changes, not when the database or UI changes. Examples:
Order,Product,Customerentities;PricingService,InventoryServicedomain services;OrderPlaced,StockDepleteddomain events. -
Infrastructure Layer: The innermost layer for I/O concerns — the application's boundary with external systems. Responsible for all communication with databases, file systems, external APIs, message brokers, email services, and caches. Implements the interfaces defined by the Domain or Application layer. Contains all ORM code, SQL queries, HTTP client calls, and third-party SDK usage. Examples:
PostgresOrderRepository,StripePaymentGateway,SendGridEmailService,RedisCache.
Layered Architecture Variants
- Strict Layered: Each layer communicates only with the layer immediately below it — no layer skipping. Most enforced variant; maximizes isolation but requires more ceremony for simple pass-through operations
- Relaxed Layered: Upper layers may communicate with any lower layer — Presentation may call Infrastructure directly for simple queries. Less ceremony but increases coupling; appropriate when strict layering adds too much overhead for simple cases
- Three-Layer: Collapses Application and Domain into a single "Business Logic" layer — common in smaller applications and classic web frameworks (Django MTV, Rails MVC)
- Five-Layer: Adds a dedicated "Service Layer" between Application and Domain, and a "Data Access" layer between Domain and Infrastructure — common in large enterprise Java applications
- Onion Architecture: Concentric rings variant where the domain is the innermost ring; each outer ring depends only on inner rings. The domain ring has zero outward dependencies — similar to Hexagonal but visualized as rings rather than a hexagon with sides
Diagram
Here is the canonical Layered Architecture diagram:
┌─────────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ REST Controllers · GraphQL Resolvers · CLI Handlers │
│ Receives input · Returns output · No business logic │
└─────────────────────────────┬───────────────────────────────────────┘
│ calls
▼
┌─────────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ Use Case Services · Command Handlers · Facades │
│ Orchestrates use cases · No business rules │
└─────────────────────────────┬───────────────────────────────────────┘
│ calls
▼
┌─────────────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER │
│ Entities · Value Objects · Domain Services │
│ All business rules · Zero external dependencies │
└─────────────────────────────┬───────────────────────────────────────┘
│ calls
▼
┌─────────────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ Repositories · ORM · HTTP Clients · Message Brokers │
│ All I/O · Database queries · External API calls │
└─────────────────────────────────────────────────────────────────────┘
Beginner-Friendly Example
Let's build a blog post management system using strict four-layer architecture. The Domain layer defines Post and Author entities with validation rules. The Application layer provides PostService that orchestrates creating, publishing, and retrieving posts. The Infrastructure layer stores posts in memory. The Presentation layer exposes the operations through a simple interface.
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
from enum import Enum
# ═══════════════════════════════════════════════════════════════════════════════
# DOMAIN LAYER — entities, value objects, business rules
# ═══════════════════════════════════════════════════════════════════════════════
class PostStatus(Enum):
DRAFT = "draft"
PUBLISHED = "published"
ARCHIVED = "archived"
@dataclass
class Author:
id: str
name: str
email: str
def __post_init__(self) -> None:
if not self.name.strip():
raise ValueError("Author name cannot be blank")
if "@" not in self.email:
raise ValueError("Invalid email address")
@dataclass
class Post:
id: str
title: str
content: str
author: Author
status: PostStatus = PostStatus.DRAFT
created_at: datetime = field(default_factory=datetime.utcnow)
published_at: Optional[datetime] = None
def __post_init__(self) -> None:
if not self.title.strip():
raise ValueError("Post title cannot be blank")
if len(self.content) < 10:
raise ValueError("Post content is too short")
def publish(self) -> None:
if self.status == PostStatus.PUBLISHED:
raise ValueError("Post is already published")
if self.status == PostStatus.ARCHIVED:
raise ValueError("Cannot publish an archived post")
self.status = PostStatus.PUBLISHED
self.published_at = datetime.utcnow()
def archive(self) -> None:
if self.status == PostStatus.ARCHIVED:
raise ValueError("Post is already archived")
self.status = PostStatus.ARCHIVED
def is_published(self) -> bool:
return self.status == PostStatus.PUBLISHED
# Domain repository interface (defined in domain, implemented in infrastructure)
class PostRepository:
def find_by_id(self, post_id: str) -> Optional[Post]:
raise NotImplementedError
def find_all(self) -> list[Post]:
raise NotImplementedError
def save(self, post: Post) -> None:
raise NotImplementedError
def delete(self, post_id: str) -> None:
raise NotImplementedError
class AuthorRepository:
def find_by_id(self, author_id: str) -> Optional[Author]:
raise NotImplementedError
def save(self, author: Author) -> None:
raise NotImplementedError
# ═══════════════════════════════════════════════════════════════════════════════
# APPLICATION LAYER — use case orchestration
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class CreatePostCommand:
post_id: str
title: str
content: str
author_id: str
@dataclass
class PostDTO:
id: str
title: str
content: str
author_name: str
status: str
created_at: str
published_at: Optional[str]
@staticmethod
def from_post(post: Post) -> "PostDTO":
return PostDTO(
id=post.id,
title=post.title,
content=post.content,
author_name=post.author.name,
status=post.status.value,
created_at=post.created_at.isoformat(),
published_at=post.published_at.isoformat() if post.published_at else None,
)
class PostApplicationService:
def __init__(
self,
post_repository: PostRepository,
author_repository: AuthorRepository,
) -> None:
self._posts = post_repository
self._authors = author_repository
def create_post(self, command: CreatePostCommand) -> PostDTO:
author = self._authors.find_by_id(command.author_id)
if author is None:
raise ValueError(f"Author '{command.author_id}' not found")
post = Post(
id=command.post_id,
title=command.title,
content=command.content,
author=author,
)
self._posts.save(post)
return PostDTO.from_post(post)
def publish_post(self, post_id: str) -> PostDTO:
post = self._posts.find_by_id(post_id)
if post is None:
raise ValueError(f"Post '{post_id}' not found")
post.publish()
self._posts.save(post)
return PostDTO.from_post(post)
def get_post(self, post_id: str) -> PostDTO:
post = self._posts.find_by_id(post_id)
if post is None:
raise ValueError(f"Post '{post_id}' not found")
return PostDTO.from_post(post)
def list_published_posts(self) -> list[PostDTO]:
return [
PostDTO.from_post(p)
for p in self._posts.find_all()
if p.is_published()
]
# ═══════════════════════════════════════════════════════════════════════════════
# INFRASTRUCTURE LAYER — data persistence
# ═══════════════════════════════════════════════════════════════════════════════
class InMemoryPostRepository(PostRepository):
def __init__(self) -> None:
self._store: dict[str, Post] = {}
def find_by_id(self, post_id: str) -> Optional[Post]:
return self._store.get(post_id)
def find_all(self) -> list[Post]:
return list(self._store.values())
def save(self, post: Post) -> None:
self._store[post.id] = post
def delete(self, post_id: str) -> None:
self._store.pop(post_id, None)
class InMemoryAuthorRepository(AuthorRepository):
def __init__(self) -> None:
self._store: dict[str, Author] = {}
def find_by_id(self, author_id: str) -> Optional[Author]:
return self._store.get(author_id)
def save(self, author: Author) -> None:
self._store[author.id] = author
# ═══════════════════════════════════════════════════════════════════════════════
# PRESENTATION LAYER — CLI interface
# ═══════════════════════════════════════════════════════════════════════════════
class BlogCLI:
def __init__(self, service: PostApplicationService) -> None:
self._service = service
def run(self) -> None:
post_dto = self._service.create_post(CreatePostCommand(
post_id="post-1",
title="Introduction to Layered Architecture",
content="Layered Architecture organizes code into horizontal layers...",
author_id="author-1",
))
print(f"Created: [{post_dto.status}] {post_dto.title}")
published_dto = self._service.publish_post("post-1")
print(f"Published: [{published_dto.status}] {published_dto.title}")
posts = self._service.list_published_posts()
print(f"Published posts: {len(posts)}")
for p in posts:
print(f" - {p.title} by {p.author_name}")
# ═══════════════════════════════════════════════════════════════════════════════
# COMPOSITION ROOT
# ═══════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
# Set up infrastructure
post_repo = InMemoryPostRepository()
author_repo = InMemoryAuthorRepository()
# Seed test data
author_repo.save(Author(id="author-1", name="Jane Smith", email="jane@example.com"))
# Wire application layer
service = PostApplicationService(post_repo, author_repo)
# Run presentation layer
cli = BlogCLI(service)
cli.run()
Production-Ready Example
In a real-world application, the Infrastructure layer replaces in-memory stores with actual databases, message brokers, and external APIs. The following production concerns apply across all languages and frameworks when implementing a proper four-layer architecture.
Key Production Concerns
- Data Transfer Objects (DTOs): The Application layer must never expose Domain entities directly to the Presentation layer. DTOs are plain data structures without behavior — they define what the API contract looks like independently of the Domain model. This allows the domain to evolve without breaking the API, and the API to evolve without changing the domain
- Repository pattern in Infrastructure: Production repositories implement the domain-defined interfaces using an ORM (SQLAlchemy, Hibernate, Entity Framework, Eloquent, GORM). The domain defines what it needs (
findById,save); the repository implements how using the ORM's specific query API - Cross-layer error handling: Domain exceptions (
InsufficientStockError,PostAlreadyPublishedError) bubble up through the Application layer. The Presentation layer maps them to appropriate HTTP status codes (400, 404, 409) without the domain knowing anything about HTTP - Dependency injection: Each layer's dependencies are injected through constructors, never created internally. This makes testing straightforward — replace any layer's dependency with a test double
- Transaction boundaries: Transactions are managed in the Application layer, not the Domain or Infrastructure layer. The Application layer controls when a unit of work begins and commits — the domain makes changes; the infrastructure persists them; the application coordinates when
Production Folder Structure
src/
├── presentation/ # Presentation Layer — no business logic
│ ├── http/
│ │ ├── controllers/
│ │ │ └── post_controller.py
│ │ ├── middleware/
│ │ │ └── error_handler.py
│ │ └── schemas/ # Request/Response DTOs for API layer
│ │ └── post_schemas.py
│ └── cli/
│ └── post_cli.py
│
├── application/ # Application Layer — orchestration only
│ ├── services/
│ │ └── post_application_service.py
│ ├── commands/
│ │ └── create_post_command.py
│ └── dtos/
│ └── post_dto.py
│
├── domain/ # Domain Layer — zero external imports
│ ├── entities/
│ │ ├── post.py
│ │ └── author.py
│ ├── value_objects/
│ │ └── email.py
│ ├── repositories/ # Repository interfaces (not implementations)
│ │ ├── post_repository.py
│ │ └── author_repository.py
│ └── exceptions/
│ └── domain_exceptions.py
│
└── infrastructure/ # Infrastructure Layer — all I/O
├── persistence/
│ ├── postgres_post_repository.py
│ └── in_memory_post_repository.py
├── external/
│ └── sendgrid_email_service.py
└── config/
└── container.py # Composition root — wires all layers
Real-World Use Cases
Layered Architecture is the foundational pattern in virtually every commercial application built with a web framework. Its clarity and familiarity make it the correct default choice for the majority of projects.
1. Enterprise Web Applications
ERP systems, CRM platforms, and internal business tools are almost universally built with Layered Architecture. The complexity of these systems — dozens of domain entities, hundreds of use cases, multiple integration points — maps naturally to layers. Each department's workflows become use cases in the Application layer. The company's business rules live in the Domain layer. Database tables and third-party integrations live in the Infrastructure layer.
2. REST API Backends
Every well-structured REST API follows a layered pattern whether its authors name it explicitly or not. Controllers receive HTTP requests and delegate to service classes. Service classes implement use cases by calling repository classes. Repository classes issue database queries. The separation makes APIs maintainable as they grow from 5 endpoints to 500 endpoints.
3. E-Commerce Platforms
E-commerce applications have naturally layered concerns: the storefront UI (Presentation), checkout and order management workflows (Application), product catalog and pricing rules (Domain), and the database, payment gateway, and shipping API (Infrastructure). Each layer can be owned by a different team and evolved independently — the payment integration team works in Infrastructure; the pricing team works in Domain.
4. Mobile Application Backends
Mobile backends follow layered architecture with an additional concern: the Presentation layer must handle multiple API versions simultaneously (v1 and v2 clients may be in active use for years). By isolating the API contract in the Presentation layer and keeping the Domain stable, adding a v2 API means extending the Presentation layer without touching domain logic.
5. SaaS Multi-Tenant Applications
Multi-tenant SaaS applications add tenant isolation concerns to each layer. The Presentation layer extracts tenant identity from the request. The Application layer enforces tenant-scoped access. The Infrastructure layer applies tenant filters to all database queries. The Domain layer remains completely tenant-agnostic — the business rules don't change per tenant. Layered Architecture makes tenant concerns addable at the right layer without polluting the domain.
6. Background Job Processing
Batch jobs, data pipelines, and scheduled tasks use the same Domain and Infrastructure layers as the web application — they just have a different Presentation layer (a job scheduler instead of an HTTP server) and a different Application layer (batch processing orchestration instead of single-request use cases). Layered Architecture makes it natural to reuse domain logic across web and background contexts without duplication.
Language-Specific Mistakes and Anti-Patterns
Common Mistakes Across All Languages
- Fat controller: The Presentation layer contains business logic — validation of business rules, direct database queries, conditional logic that belongs in the Domain. The controller should only translate HTTP to application calls and back
- Anemic domain model: Domain entities are plain data bags with no methods. All behavior is in Application layer services. The Domain layer has no business rules — it is just a data schema with a fancy name
- Layer skipping: The Presentation layer imports and calls the Infrastructure layer (or Domain layer) directly, bypassing the Application layer. This makes it impossible to reuse the business operation through other channels (CLI, background job)
- Leaky abstraction: Domain entities expose database-specific types (
SqlAlchemy.Column,JPA @Entity) or API-specific types (Jackson @JsonProperty,Pydantic model). The Domain layer must be free of framework-specific annotations - Shared DTOs across layers: Using the same data class as both the API request model and the Domain entity couples the API contract to the domain model — a change in one forces a change in the other
Language-Specific Pitfalls
- Python: Using Django's
Modelclass (which is an ORM entity) directly in views or templates is the most common layering violation in Python web development — Django'sModelbelongs in Infrastructure, not Domain. Use separate plain Python classes for domain entities. Avoid putting business logic inserializers.py— serializers are a Presentation concern - TypeScript: Importing Prisma or TypeORM entities directly in NestJS controllers bypasses the Application layer entirely. Define separate domain classes and DTO interfaces; use mappers to convert between them. Avoid using
anyto sidestep type safety at layer boundaries — define explicit DTO types for every cross-layer call - Java: Using
@EntityJPA annotations on domain classes couples the domain to Hibernate. Use separate JPA entity classes in the Infrastructure layer and map them to domain objects. Avoid making Application layer services@Transactionalat the method level — transaction boundaries should be explicit and tied to the use case, not the Java method boundary - JavaScript: Without TypeScript, layer boundaries are enforced only by convention — use ESLint
import/no-restricted-pathsrules to fail the build when a layer imports from a layer it should not. Avoidrequire-ing Express types (req,res) in Application or Domain files. Use JSDoc to document expected object shapes at layer boundaries - C#: Using
DbContextdirectly in Controllers bypasses the Application and Domain layers entirely — always inject a service interface, never the DbContext. Avoid returningIQueryablefrom repositories — it leaks database query composition into the Application layer. Always returnIReadOnlyList<T>orTask<T>from repository methods - PHP: Laravel's Eloquent
Modelis designed to be used directly in controllers — resist this for anything beyond simple CRUD. As an application grows, introduce a Service layer. Avoid putting business logic in Eloquent model observers — observers are an Infrastructure concern that should call Application layer services, not replace them - Go: Go's flat package structure makes layering a naming convention rather than an enforced boundary — use subdirectories (
domain/,application/,infrastructure/,presentation/) and Go module paths to signal the intended layer. Avoid putting SQL query strings in handler functions — handler functions are Presentation layer code - Rust: Avoid using
sqlx::query!macro results (which contain database row types) as return types from functions in the Application layer — map them to domain structs first. Keepaxumoractix-webStatetypes out of domain and application structs — inject dependencies through trait objects or generic type parameters - Dart: In Flutter, avoid putting business logic in widget
build()methods orinitState()— these are Presentation layer hooks. Use a state management layer (BLoC, Riverpod, Provider) as the Application layer. Keep domain classes in a pure Dart package separate from any Flutter SDK imports - Swift: In iOS development with UIKit,
UIViewControlleris the Presentation layer — avoid putting network calls, database operations, or business rules there. Use a dedicated service or ViewModel (Application layer) as an intermediary. With SwiftUI,Viewstructs are the Presentation layer — bind them toObservableObjectViewModels, which call domain services - Kotlin: In Android,
ActivityandFragmentare the Presentation layer — avoid callingRoomDAO methods orRetrofitAPI calls directly from them. UseViewModel(Application layer) withRepositoryclasses (Infrastructure layer). Keep domain entities (data class) free of AndroidParcelableannotations — use separate Parcelable models in the Presentation layer for navigation arguments
Frequently Asked Questions
What is the difference between the Application layer and the Domain layer?
The Domain layer contains the what of the business — the rules, entities, and logic that define how the business operates. The Application layer contains the how of a specific use case — the sequence of steps to accomplish a goal using domain objects and infrastructure. The Domain layer defines that Order.cancel() requires the order to be in Pending status. The Application layer defines that cancelling an order involves loading it from the repository, calling cancel(), saving it, and sending a confirmation email. The Domain layer would be the same if you changed from a web API to a mobile app; the Application layer might differ.
Should I put repository interfaces in the Domain layer or the Application layer?
They belong in the Domain layer. Repository interfaces describe the Domain's persistence needs in domain terms — findById, findByStatus, save. The Domain layer defines what persistence operations it needs. The Infrastructure layer defines how those operations are fulfilled. If you put repository interfaces in the Application layer, the Domain layer has no way to express its own persistence needs without importing the Application layer, which creates an upward dependency.
When should I use Layered Architecture vs. Hexagonal Architecture?
Use Layered Architecture as the default. It is simpler, more familiar, and sufficient for the vast majority of applications. Move toward Hexagonal Architecture when: the domain is complex enough that business logic tests need to run without any infrastructure; the application must support many different entry points and output channels simultaneously; or the team is practicing strict TDD and needs the guarantee that no infrastructure code can ever be imported by a domain test. Hexagonal Architecture is more powerful but requires more discipline and abstraction — layered is often the right pragmatic choice.
How do I handle cross-cutting concerns like logging and authentication?
Cross-cutting concerns do not belong in any single layer — they belong in middleware or interceptors that wrap layers. In web frameworks, authentication middleware runs before controllers (Presentation layer) and rejects unauthorized requests before they reach the Application layer. Logging can appear at multiple layers: the Presentation layer logs incoming requests; the Application layer logs use case execution; the Infrastructure layer logs database query times. Each layer logs at the appropriate abstraction level — the domain should not log HTTP details; the presentation layer should not log SQL queries.
How many layers should I have?
Four layers (Presentation, Application, Domain, Infrastructure) is the standard and works for most applications. Add a fifth layer only when a specific concern becomes large enough to warrant it — a "Service Layer" between Application and Domain for complex business operations that span multiple aggregates, or a "Data Access Object (DAO)" layer between Domain and Infrastructure to separate ORM entity mapping from query construction. Resist adding layers for their own sake — every additional layer adds ceremony and indirection that must be justified by the complexity it manages.
Can I use Layered Architecture with microservices?
Yes — they operate at different scales. Microservices architecture describes how to split a system into independently deployable services. Layered Architecture describes how to structure the code inside each service. Each microservice is typically a small application with its own four-layer structure: a Presentation layer (its API or event consumer), an Application layer (its use cases), a Domain layer (its bounded context's business rules), and an Infrastructure layer (its database and external calls). Microservices decomposition and layered internal structure are complementary, not competing, choices.
Key Takeaways
Language-Specific Best Practices
- Python: Use plain Python
dataclassor PydanticBaseModel(withmodel_config = ConfigDict(frozen=True)) for domain entities — avoid Django ORMModelin the domain layer; useProtocolfor repository interfaces; organize packages aspresentation/,application/,domain/,infrastructure/to make layer membership obvious - TypeScript: Define strict DTO interfaces at every layer boundary —
CreatePostRequestin Presentation,CreatePostCommandin Application,Postentity in Domain; usereadonlymodifiers on domain entity properties; configuretsconfig.jsonpathsto enforce import direction; use NestJS modules to enforce layer boundaries - Java: Use Spring
@Servicefor Application layer classes,@Repositoryfor Infrastructure,@RestControllerfor Presentation — never put@Serviceon Domain classes; use Javarecordfor immutable DTOs and value objects; useOptional<T>for nullable repository returns rather than returningnull - JavaScript: Use ESLint
import/no-restricted-pathsto enforce layer boundaries at build time; organize assrc/presentation/,src/application/,src/domain/,src/infrastructure/; use JSDoc@typedefto document DTO shapes at layer boundaries; useObject.freeze()for domain value objects - C#: Use
recordfor immutable DTOs and value objects; register services inProgram.cswith the appropriate lifetime (Scopedfor application services,Singletonfor stateless infrastructure); useMediatRfor command/query handling in the Application layer; useFluentValidationin the Application layer, never in the Domain - PHP: Use PHP 8
readonlyclasses for domain value objects and DTOs; bind interfaces to implementations in a Service Provider (AppServiceProvider); use Laravel Form Requests for Presentation-layer input validation only; keep Eloquent in the Infrastructure layer — create plain PHP domain classes - Go: Organize with explicit package paths:
internal/presentation/http,internal/application,internal/domain,internal/infrastructure/postgres; use constructor functions for all structs across layers; define interfaces in the package that uses them (domain defines repository interfaces; infrastructure implements them) - Rust: Use separate Rust modules (
mod presentation,mod application,mod domain,mod infrastructure) withpub usere-exports to control visibility; usethiserrorfor domain-level errors and map them to HTTP status codes in the Presentation layer; useserdeonly in Presentation and Infrastructure, never in Domain structs - Dart: Separate domain into a pure Dart package with no Flutter imports; use BLoC or Riverpod as the Application layer in Flutter apps; define abstract classes for repository interfaces in the domain package; use
freezedfor immutable domain entities and value objects - Swift: Keep domain in a pure Swift framework target with no UIKit/SwiftUI imports; use
protocolfor all repository interfaces; useasync/awaitthroughout for clean async composition across layers; useactorfor Infrastructure implementations that manage mutable state - Kotlin: Use
data classfor domain entities and DTOs; use Spring Boot's component scan with package-level organization to enforce layer boundaries; use Kotlinsealed classfor domain-level error hierarchies; use coroutines (suspend fun) throughout for non-blocking I/O in Application and Infrastructure layers
📋 When to Use Layered Architecture:
- You are building a conventional web application, REST API, or enterprise system — this is the correct default choice for most applications
- Your team includes developers with varying experience levels — the pattern is widely understood and requires no explanation to onboard new members
- The application has clear separation between UI concerns, business logic, and data access
- You are building on a framework (Django, Spring, Laravel, Rails, ASP.NET) that already encourages a layered structure
- You want predictable change impact — modifications to the database schema should not cascade to the UI layer
- Speed of delivery is important — layered architecture is faster to implement than more complex architectures like Hexagonal or CQRS
⚠️ When NOT to Use Layered Architecture:
- The Domain layer is so complex that it must be testable with zero infrastructure — consider Hexagonal Architecture for stronger isolation
- The application must support many simultaneous entry points (REST, gRPC, CLI, message queue) with very different orchestration needs — consider Hexagonal with multiple driving adapters
- The application is a simple script or utility with no meaningful business rules — a single-file script needs no architecture
- The application has extreme write/read asymmetry and very high scale requirements — consider CQRS with separate read and write models
🛠️ Best Practices Across Languages:
- Enforce dependencies go downward only — Presentation calls Application; Application calls Domain; Domain calls Infrastructure. Any upward dependency is a violation that will cause future pain
- Define repository interfaces in the Domain layer — the Domain defines what it needs; Infrastructure provides it. Never import an Infrastructure class from the Domain
- DTOs at every layer boundary — never pass Domain entities to the Presentation layer directly; always map through DTOs that represent the API contract
- Application layer owns transactions — open and commit database transactions in the Application layer use case, not in repositories or domain methods
- Validate at the right layer — input format validation (required fields, data types) belongs in the Presentation layer; business rule validation (stock availability, order status) belongs in the Domain layer
- Keep the Domain layer free of framework annotations — any framework annotation on a domain entity (
@Entity,@JsonProperty,@Column) creates a dependency that will make the domain harder to test and evolve - Test each layer independently — unit test the Domain with no dependencies; unit test the Application with fake repositories; integration test the Infrastructure with a real database
💡 Common Pitfalls:
- The God Service: One Application layer service class with 30 methods handling every use case — split by domain concept (one service per aggregate or feature area)
- Downward layer skipping: Presentation imports Infrastructure directly for "simple" queries — once, always. Establish the pattern correctly from the first endpoint
- Domain importing Application: The Domain layer calls an Application service to perform a workflow — this creates an upward dependency that entangles the layers irreversibly
- Business logic in DTOs: DTOs with
isExpired(),calculateTotal(), or other business methods — DTOs are data containers; business logic belongs in Domain entities - Infrastructure exceptions leaking upward: Catching a
SqlExceptionin the Infrastructure layer and letting it bubble to the Presentation layer exposes infrastructure details — translate exceptions at layer boundaries - Premature optimization through layer skipping: Bypassing the Application layer to improve performance by "avoiding the overhead" — the overhead is negligible; the maintainability cost is not
🎁 Advanced Techniques:
- CQRS within layers: Split the Application layer into Command services (write operations with full domain model) and Query services (read operations with optimized DTOs from Infrastructure directly) — reduces overhead for read-heavy operations without abandoning the layered structure
- Domain events: Domain entities raise domain events (
PostPublished,OrderCancelled) when significant things happen; the Application layer collects them after a use case completes and dispatches them to event handlers — enables loose coupling between use cases without circular dependencies - Specification pattern: Encapsulate complex query criteria as Specification objects in the Domain layer that the Infrastructure layer translates to SQL — keeps query logic in the domain without introducing ORM dependencies
- Anti-corruption layer: When the Infrastructure layer integrates with a legacy system or third-party API with a different data model, add a translation layer inside the Infrastructure adapter that maps between the external model and the Domain model — prevents external model concepts from polluting the domain
- Layered architecture + Mediator: Use a Mediator (like MediatR in .NET or a similar pattern) in the Application layer to decouple the Presentation layer from specific service classes — the Presentation layer sends commands and queries through the Mediator; the Application layer handles them. This makes the Application layer extensible without modifying existing handlers
- Vertical Slice refactoring: For large applications where a strict horizontal layer separation creates too much ceremony, consider organizing within layers by feature (vertical slice) rather than technical type —
posts/containsPostController,PostApplicationService,Post, andInMemoryPostRepositoryall together. Start horizontal; move toward vertical slices as features grow large enough to warrant it
Layered Architecture is the bedrock of commercial software development. It is not glamorous and it does not solve every problem — but it solves the right problems for the right cost for most applications. Every developer who has worked on a Django, Spring, Laravel, or Rails application has worked with layered architecture, whether they named it or not. The pattern's staying power comes from its simplicity: one rule — dependencies flow downward — applied consistently across an entire codebase produces systems that are comprehensible, testable, and maintainable for years. Master the layers, enforce the boundaries, and the architecture will take care of itself.
This guide is part of the Design Patterns in Action series, which covers:
- Visitor Pattern — New operations on a stable hierarchy
- Memento Pattern — Capture and restore object state
- Observer Pattern — Event-driven notifications
- State Pattern — Behavior changes with lifecycle phase
- Strategy Pattern — Interchangeable algorithms
- Facade Pattern — Simplified interface to a subsystem
- MVC Pattern — Model-View-Controller architecture
- Unit of Work Pattern — Coordinate database changes atomically
- Hexagonal Architecture — Ports and Adapters for technology-independent domains
- Layered Architecture — Horizontal layers for separation of concerns
Each guide covers all 11 programming languages with production-ready examples, language-specific best practices, and comprehensive anti-pattern analysis.