Layered Architecture
Layered Architecture stacks the application in horizontal layers — presentation, application, domain, infrastructure — each depending only on layers below.
Introduction
Layered Architecture stacks the application in horizontal layers — presentation, application, domain, infrastructure — each depending only on layers below. It's the lingua franca of enterprise software: Controller → Service → Repository → DB. The goal isn't ceremony; it's predictable placement so teams ship without stepping on each other.
The story
A startup MVP had controllers calling SQL directly. After 18 months every feature touched HTTP + SQL + business rules in one file. Introducing layers (Controller → Service → Repository) took 4 weeks; new developers shipped features in 2 days instead of 2 weeks.
The business problem
Monolithic handlers mixing transport, logic, and persistence create team friction:
- No test seam: every test needs HTTP server + real database.
- Ownership blur: newcomers can't find where business logic lives.
- Merge conflicts: all teams edit the same controller files.
- Change ripple: swapping DB touches every endpoint.
The problem teams faced
Layered Architecture fits when:
- CRUD-heavy enterprise app with multiple contributors.
- Team size exceeds ~5 and needs shared vocabulary.
- Long-lived system with separate presentation and persistence churn.
- Tests must run without full infrastructure stack.
Understanding the topic
Intent: Separate concerns into layers with downward-only dependencies.
- Presentation — HTTP controllers, CLI, GraphQL resolvers.
- Application — use cases, orchestration, transaction boundaries.
- Domain — entities, value objects, business rules (optional explicit layer).
- Infrastructure — DB, messaging, external APIs.
Internal architecture
Layered checkout flow:
// Presentationapp.post("/orders", (req, res) => orderController.place(req, res))// Applicationclass OrderService {constructor(private orders: OrderRepository) {}async place(cart: Cart) { /* business rules */ return this.orders.save(order) }}// Infrastructureclass PostgresOrderRepository implements OrderRepository { /* SQL */ }
Visual explanation
Three diagrams cover layer stack, dependencies, and leak detection:
Informative example
Before / after — controller with SQL to layered:
// ❌ Everything in controller — untestable, coupledapp.post("/orders", async (req, res) => {const client = await pool.connect()const total = req.body.items.reduce((s, i) => s + i.price, 0)await client.query("INSERT INTO orders ...", [total])res.json({ ok: true })})// ✅ Layers — test service with fake repoclass OrderService {constructor(private repo: OrderRepository) {}async place(cart: Cart) {const order = Order.fromCart(cart)return this.repo.save(order)}}
Execution workflow
Extract service
Move business logic out of controller.
Real-world use
Spring Boot (@Controller/@Service/@Repository), ASP.NET layers, Rails MVC + service objects, most enterprise CRUD apps.
Production case study
Startup MVP layer extraction:
- Before: controllers with SQL; 2-week feature cycles for new hires.
- After: Controller → Service → Repository; onboarding 2 days to first PR.
- Metric: test suite ran without DB using in-memory repo fakes.
Trade-offs
- Pro: predictable structure; testable seams; team vocabulary.
- Con: more files; wrong layer placement hard to fix later.
- Con: anemic domain if all logic lives in service layer.
Decision framework
- Use for CRUD enterprise apps with team > 5.
- Use when test isolation from HTTP/DB is required.
- Avoid for tiny apps — layers add ceremony without payoff.
- Don't skip domain layer when rules are complex — avoid anemic services.
Best practices
- Controllers thin — validate input, call service, map response.
- Repositories return domain types — not raw ORM rows to controllers.
- Enforce dependency direction in CI.
Anti-patterns to avoid
- Smart controller with SQL and business rules.
- Repository called directly from controller — skips application layer.
- Layers as folders only — no dependency rules enforced.
Common mistakes
- Anemic domain — all logic in services, entities are data bags.
- Layer leak — ORM entities in presentation layer.
- Premature layers before team pain exists.
Debugging tips
- Trace bug layer-by-layer — which layer owns the wrong assumption?
- Import graph visualization finds layer violations.
Optimization strategies
- Don't add domain layer until rules exceed simple CRUD.
Common misconceptions
- Layers ≠ microservices. Layers are in-process; services are deployment boundaries.
- Hexagonal/Clean refine layers with explicit ports — not replace them entirely.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionLayered vs Hexagonal?+
Answer
Follow-up
2IntermediateQuestionWhere do business rules go?+
Answer
Follow-up
3AdvancedQuestionTest layers how?+
Answer
Follow-up
Summary
You can structure CRUD apps in layers, enforce dependency direction, and know when layers earn their cost. Tell the controller-with-SQL startup story — if you can do that, you own Layered Architecture.