Day 053: Layered Backend Architecture
A backend becomes easier to change when transport, business rules, and persistence stop leaking into each other.
Today's "Aha!" Moment
Layered architecture is often introduced as a diagram: controllers at the top, services in the middle, repositories at the bottom. That picture is easy to memorize and easy to misuse. If you copy it mechanically, you get many files and not much clarity. The real value of layers is not the shape of the folders. It is the control of dependencies and change.
Think about one backend feature on the learning platform: enrolling a learner in a course. The HTTP handler should understand the request and response. The business layer should decide whether enrollment is allowed. The persistence layer should know how enrollment records are stored. If those responsibilities mix together, every small change becomes cross-cutting. A validation tweak touches SQL. A schema change leaks into handlers. A new background job must reimplement rules that were buried inside a controller.
That is the core insight: layers are boundaries around kinds of knowledge. The transport layer knows HTTP. The application or domain layer knows business decisions. The data-access layer knows storage mechanics. Each boundary exists to keep one kind of change from dragging the others around with it.
Once you see layered architecture that way, it stops being ceremony. It becomes a way to ask concrete questions. Does this code need to know about HTTP here? Does this business rule really depend on SQL? If I had to run the same logic from a queue worker instead of a route handler, would I have to copy it? Those questions are the real architecture.
Why This Matters
The problem: Many backends start simple and then accumulate handlers that parse requests, enforce business rules, open transactions, execute queries, and format responses all in one place.
Before:
- Route handlers become the easiest place to dump “just one more rule.”
- Database queries spread through controllers and jobs.
- Business decisions cannot be reused without dragging framework and persistence concerns along with them.
After:
- Transport logic stays near the transport boundary.
- Business rules live in a layer that can be reused from HTTP, CLI, jobs, or tests.
- Persistence mechanics are localized so storage changes have smaller blast radius.
Real-world impact: Better testability, cleaner refactors, safer feature changes, and a much easier time explaining where a piece of backend logic should belong.
Learning Objectives
By the end of this session, you will be able to:
- Explain layered architecture as dependency control - Describe what each layer should know and what it should not need to know.
- Spot boundary violations - Identify when HTTP, business, and persistence concerns are bleeding into the wrong place.
- Use layers pragmatically - Apply the pattern where it reduces coupling instead of turning it into empty pass-through ceremony.
Core Concepts Explained
Concept 1: Controllers Should Translate the Outside World, Not Own the Business Policy
Suppose the API receives POST /courses/42/enrollments. The controller has an important job: parse input, authenticate the caller, validate the request shape, invoke the right application behavior, and map the result to an HTTP response. That is already enough responsibility.
The controller becomes problematic when it starts making deeper business decisions such as:
- whether the course is open
- whether the learner is already enrolled
- whether payment status permits access
- whether an audit event should be emitted
Those rules are not about HTTP. They are about the domain. If they live in the controller, they become hard to reuse anywhere else.
One good boundary test is:
If I moved this use case from HTTP to a queue worker,
would I want to keep this code?
If the answer is yes, the logic probably does not belong in the controller.
The trade-off is speed versus reuse. Putting logic in the controller is fast in the moment, but it quickly produces duplicated rules and brittle endpoints once the same behavior is needed elsewhere.
Concept 2: Services or Use Cases Hold the Business Decisions and Workflow
The service layer is where the backend should answer domain questions in domain terms. For enrollment, the service can ask:
- is the course open?
- is the learner already enrolled?
- should a waitlist path be used?
- what follow-up actions should happen if enrollment succeeds?
Notice that none of those questions inherently depend on HTTP, a web framework, or a specific SQL query. That is the reason this layer is valuable: it carries the behavior the product actually cares about.
class EnrollmentService:
def __init__(self, course_repo, enrollment_repo):
self.course_repo = course_repo
self.enrollment_repo = enrollment_repo
def enroll(self, learner_id, course_id):
course = self.course_repo.get(course_id)
if not course["is_open"]:
raise ValueError("course_closed")
if self.enrollment_repo.exists(learner_id, course_id):
raise ValueError("already_enrolled")
return self.enrollment_repo.create(learner_id, course_id)
The example is intentionally small. The important point is that the service speaks in domain language. It is easier to test, easier to call from different entry points, and easier to change without touching transport concerns.
The trade-off is indirection versus stability. A service layer adds one more hop in the codebase, but it is often the hop that keeps the core behavior from getting entangled with frameworks and storage details.
Concept 3: Repositories and Data Access Boundaries Keep Storage Knowledge Localized
The service still needs data, so where should query and persistence details live? In a layered backend, that knowledge belongs behind a persistence boundary such as a repository or gateway. The point is not to hide every SQL statement behind ceremony. The point is to stop storage detail from leaking everywhere.
For the enrollment use case, the service should not care whether data comes from PostgreSQL, an ORM, or a test double. It should ask for domain-relevant operations such as:
- get course by ID
- check whether enrollment exists
- create enrollment
The persistence layer then owns:
- query shape
- connection/session mechanics
- transaction boundaries where appropriate
- row-to-domain mapping details
controller
-> service/use case
-> repository / persistence boundary
-> database
This is valuable because storage is one of the noisiest change sources in a backend. Queries evolve. ORM details leak. Transactions become complicated. If those concerns are spread across handlers and services, the whole codebase starts depending on them accidentally.
The trade-off is explicit abstraction versus convenience. Repositories help when they remove storage coupling. They become wasteful only when they are so generic and empty that they hide nothing and clarify nothing.
Troubleshooting
Issue: The project has layers, but each one only forwards calls without owning real responsibility.
Why it happens / is confusing: Teams often copy the folder structure of layered architecture before deciding what each layer is supposed to protect.
Clarification / Fix: Keep only boundaries that remove a meaningful dependency. If a “service” or “repository” adds no separation of concerns, it is probably ceremony rather than architecture.
Issue: Controllers turn into “small monoliths.”
Why it happens / is confusing: Controllers are the most visible place to make something work quickly, so business rules and storage logic accumulate there by default.
Clarification / Fix: Move any rule that could matter outside one route handler into the service layer. Move any repeated storage detail behind a persistence boundary. Thin controllers are a result of protecting those two decisions.
Advanced Connections
Connection 1: Layered Architecture ↔ API Design
The parallel: Stable API contracts are easier to maintain when HTTP concerns stay near the edge and domain behavior stays in a reusable core.
Real-world case: Versioning, status mapping, and validation get clearer when the controller is translating an application result rather than implementing the business workflow itself.
Connection 2: Layered Architecture ↔ Test Design
The parallel: Clear layers make it much easier to test business behavior without booting the entire transport and persistence stack.
Real-world case: Enrollment rules can be tested with fake repositories, while controller tests can focus on request/response translation instead of business policy.
Resources
Optional Deepening Resources
- These resources are optional and are not required for the core 30-minute path.
- [ARTICLE] Service Layer
- Link: https://martinfowler.com/eaaCatalog/serviceLayer.html
- Focus: See why application services help organize business workflows above transport and persistence details.
- [ARTICLE] Presentation Domain Data Layering
- Link: https://martinfowler.com/bliki/PresentationDomainDataLayering.html
- Focus: Revisit the purpose of separating presentation, domain, and data concerns.
- [BOOK] Clean Architecture
- Link: https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/
- Focus: Compare layered dependency control with broader architecture rules about keeping business logic independent from frameworks.
Key Insights
- Layers are about controlling dependencies - Their value comes from separating kinds of knowledge, not from matching a diagram mechanically.
- Controllers should translate, not decide - Transport logic belongs at the edge; business rules belong in reusable application code.
- Persistence boundaries keep storage detail from spreading - Repositories are useful when they localize the noise of data access and transactions.
Knowledge Check (Test Questions)
-
What is the strongest reason to keep controllers thin?
- A) So transport handling stays separate from business rules that should be reusable elsewhere.
- B) So no validation ever happens there.
- C) So repositories can return HTTP responses directly.
-
Why is a service or use-case layer valuable in a layered backend?
- A) Because it keeps domain decisions independent from HTTP and persistence details.
- B) Because it should contain all SQL so controllers remain short.
- C) Because it removes the need for tests.
-
What problem do repositories mainly solve when they are used well?
- A) They localize storage mechanics so query and transaction details do not leak through the whole codebase.
- B) They turn every query into a separate microservice.
- C) They make the domain model depend more directly on the ORM.
Answers
1. A: Thin controllers protect the boundary between transport and reusable business behavior.
2. A: The service layer keeps product rules in a place that can survive framework, entry-point, and storage changes.
3. A: Repositories are useful when they reduce persistence coupling by keeping storage mechanics localized.