N-tier architecture is not about counting layers.
It's not about following a rigid blueprint or checking boxes on a design document.
It's about asking one question repeatedly: what responsibility belongs where, and why?
When you're building a system that needs to grow, change, and adapt without breaking under its own weight, the answer to that question becomes everything.
The layers you build are walls—boundaries that isolate the kinds of changes that happen at different speeds, with different frequency, and with different consequences.
A layer should exist because it provides a boundary.
HTTP changes.
Business logic changes.
Database schema changes.
Domain rules change.
Each of these changes at a different cadence.
When you know what varies, you know where to build the wall.
The Core Idea: Boundaries, Not Just Layers The exact number of layers is not important.
Three layers, four layers, five—the number is less significant than the principle: separate your system based on responsibility.
Think of it this way: if the part of your system that talks to PostgreSQL changes, how much of the rest of your application needs to change?
The answer should be: only the part that knows about PostgreSQL.
If the part of your system that handles HTTP requests changes, how much else is affected?
The answer should be: only the part that knows about HTTP.
If your business rules change, those changes should ripple through your application in a way you can predict and control.
You should know exactly which layers are impacted and which layers remain untouched.
This is what a good n-tier architecture gives you: predictable change, isolated concerns, and clearer boundaries.
The Four-Layer Foundation Most systems benefit from separating into four core layers.
Each layer has a distinct responsibility.
Each layer knows about the layers below it but is unknown to the layers above it.
Layer 1: Presentation Layer (Request Receiver) "Does this code deal with transport?" HTTP, JSON, gRPC, WebSocket.
The presentation layer is where the outside world knocks on your door.
Its job is narrowly defined: Parse incoming requests (JSON, form data, etc.) Validate basic request format (is this JSON well-formed?) Call the application layer to handle the actual logic Convert the result back into an HTTP response Handle serialization, status codes, and error formatting What it should NOT know: How a promotion is stored in the database How the video generation service works internally How subscription billing is validated The business rules that govern which operations are allowed Example: The controller translates HTTP into application logic.
That's it.
Layer 2: Application/Service Layer (Orchestration) "Does this code coordinate a user or system action?" Create promotion.
Publish promotion.
Generate video.
Cancel subscription.
This is where the use case orchestration happens.
The service layer decides the sequence of steps required to fulfill a request.
The application layer: Brings together all the collaborators needed to complete a use case Controls the sequence: step 1, then step 2, then step 3 Coordinates between repositories, domain objects, external services, and event publishers Does NOT care how those collaborators work internally Example: Notice: the service doesn't know whether the repository uses PostgreSQL or MongoDB.
It doesn't know whether the video service calls OpenAI or Runway.
It just orchestrates the workflow.
Layer 3: Domain/Business Layer (Rules) "Does this code define a business rule?" A promotion cannot be published before video generation completes.
A restaurant can only create promotions if it has an active subscription.
The domain layer is where the rules of your business live.
These rules should: Define what is valid and what is not Enforce invariants (things that must always be true) Be independent of HTTP, databases, caches, and external APIs Remain stable even as infrastructure changes The domain layer should NOT know about: HTTP requests or responses Database queries or ORM frameworks Redis or caching layers AWS, Stripe, or any external services Instead of directly updating data, the domain layer enforces a rule that dictates how the data should be updated.
This keeps related business logic in one place.
Layer 4: Data Access & Infrastructure Layer (Implementation) "Does this code talk to an external system?" Database, Redis, S3, video AI provider, Stripe, Kafka.
This is where the implementation details live.
The infrastructure layer: Provides concrete implementations of interfaces defined by domain and application layers Handles persistence, caching, and external API calls Can be swapped out without changing business logic Example: The application layer depends on an interface: The infrastructure layer provides the concrete implementation: Tomorrow, if you want to switch from Evator to Runway, you only change the infrastructure implementation.
The application layer, domain layer, and presentation layer don't change at all.
Why This Matters: The Promotion Use Case Let's walk through a real example to see how the layers work together.
Restaurant Owner wants to create a promotion with AI-generated video At each step, the layer only knows what it needs to know: The controller doesn't know about subscriptions or video APIs The service doesn't know about HTTP or database queries The domain rules don't know about external services The infrastructure doesn't know about business logic If the database changes: only infrastructure is affected.
If the video provider changes: only infrastructure is affected.
If the business rule changes (e.g., "subscription validation now requires an API call"): you modify the service orchestration, not the controller or domain.
The Dependency Rule The most important rule in n-tier architecture: code dependencies flow inward.
The presentation layer depends on the application layer The application layer depends on the domain and infrastructure layers The domain layer depends on nothing (it's the core) The infrastructure layer depends on abstractions defined by the domain Bad: A controller directly calls a database query function.
Good: A controller calls a service, which uses a repository interface.
The repository is implemented by the infrastructure layer.
This inversion of control is what allows you to: Test business logic without a database Swap implementations without rewriting logic Keep the core of your system stable while the edges change Practical Takeaways: Building Layers That Earn Their Keep
1.
Ask "What Varies?" Before You Layer Don't add a layer because the architecture diagram shows four boxes.
Add a layer because you have change pressure that justifies isolation.
Start with three layers: HTTP, logic, persistence.
Add more if you find yourself writing the same rule-checking code in multiple places.
2.
Depend on Interfaces, Not Implementations The service layer should not import .
It should import interface.
Let the infrastructure layer wire the concrete implementation.
3.
Test Each Layer in Isolation Domain logic: No database.
No HTTP.
No external APIs.
Just unit tests.
Application logic: Use mock repositories and services.
Test the orchestration.
Presentation logic: Call the service, verify the response format.
No database needed.
When you can test a layer without spinning up other infrastructure, you know the boundary is real.
4.
Make Layers Visible in Your Code The directory structure should make the layers obvious: When someone new joins your team, they should be able to guess which layer a file belongs to just by looking at the path and imports.
5.
Move Slowly Toward Abstraction You don't need a repository interface on day one.
You don't need five layers.
Build what you need, test what you built, and refactor when you feel the pain of change.
The best architecture is the one that makes the next change easiest.
Common Mistakes to Avoid Mistake 1: Leaky Abstractions The presentation layer knows too much about the database.