On this page:
10.1 Learning Objectives
10.2 Motivation:   You’ve Been Building This All Semester
10.2.1 The pattern you already live in
10.2.2 Why architecture patterns exist at all
10.2.3 Intuition:   a restaurant
10.3 Running Example:   Terp  Tasks Grows House Rules
10.4 The Pattern
10.4.1 The request flow, traced
10.5 The Four Layers, One at a Time
10.5.1 Presentation layer
10.5.2 Business logic layer (service layer)
10.5.3 Data access layer (repository)
10.5.4 Database layer
10.6 The Payoff:   Testing Each Layer in Isolation
10.7 The Honest Trade-offs
10.7.1 Performance overhead
10.7.2 Rigidity:   one feature, four files
10.7.3 Pass-through code (the real disease)
10.7.4 When to use it (and not)
10.8 The Neighborhood:   Comparison with Other Architectures
10.9 Git  Hub-Ready Mini-Project:   terptasks-layered
10.10 Practice Exercises
10.10.1 Basic
10.10.2 Intermediate
10.10.3 Advanced
10.11 Summary
10.11.1 Key takeaways
10.11.2 Terminology
10.11.3 Common mistakes
10.11.4 Connections
10.12 Instructor Notes
9.1

10 Layered Architecture🔗

    10.1 Learning Objectives

    10.2 Motivation: You’ve Been Building This All Semester

      10.2.1 The pattern you already live in

      10.2.2 Why architecture patterns exist at all

      10.2.3 Intuition: a restaurant

    10.3 Running Example: TerpTasks Grows House Rules

    10.4 The Pattern

      10.4.1 The request flow, traced

    10.5 The Four Layers, One at a Time

      10.5.1 Presentation layer

      10.5.2 Business logic layer (service layer)

      10.5.3 Data access layer (repository)

      10.5.4 Database layer

    10.6 The Payoff: Testing Each Layer in Isolation

    10.7 The Honest Trade-offs

      10.7.1 Performance overhead

      10.7.2 Rigidity: one feature, four files

      10.7.3 Pass-through code (the real disease)

      10.7.4 When to use it (and not)

    10.8 The Neighborhood: Comparison with Other Architectures

    10.9 GitHub-Ready Mini-Project: terptasks-layered

    10.10 Practice Exercises

      10.10.1 Basic

      10.10.2 Intermediate

      10.10.3 Advanced

    10.11 Summary

      10.11.1 Key takeaways

      10.11.2 Terminology

      10.11.3 Common mistakes

      10.11.4 Connections

    10.12 Instructor Notes

10.1 Learning Objectives🔗

By the end of this lecture you should be able to:

  1. Define layered (n-tier) architecture and name the four canonical layers—presentation, business logic (service), data access, database—with each layer’s single responsibility.

  2. Trace a request down and back up the stack, and state the dependency rule: each layer talks only to the layer directly below it.

  3. Assign any given piece of code to its correct layer—and recognize misplaced code (SQL in a route handler, HTTP status codes in a service) on sight.

  4. Demonstrate the testing payoff: unit-test business rules with a fake repository (no database, no HTTP), and explain why the mud-ball version cannot be tested this way.

  5. Weigh the trade-offs—separation of concerns and maintainability against performance overhead, rigidity, and pass-through code—and identify when the pattern is (and isn’t) the right choice.

  6. Position layered architecture among its alternatives (MVC, microservices, event-driven, hexagonal, clean) and articulate what each changes.

  7. Refactor a tangled single-file application into layers without changing its externally observable behavior.

10.2 Motivation: You’ve Been Building This All Semester🔗

10.2.1 The pattern you already live in🔗

Look at what the last three lectures produced, stacked in order:

  • FastAPI lectureroute handlers that parse HTTP, validate input with Pydantic, and return JSON.

  • REST lecturethe contract those handlers expose: resources, methods, status codes.

  • Database lecturecrud.py over SQLAlchemy models over PostgreSQL, and exercise E8, which wired the API to real storage without changing the contract tests.

That separation—HTTP concerns here, data concerns there, rules in between—has a name: layered architecture (also called n-tier architecture), one of the most common software architecture patterns in industry. Today we make the pattern explicit, name its rules, and—most importantly—see what happens when you don’t follow it.

10.2.2 Why architecture patterns exist at all🔗

An architecture pattern is a pre-made decision about where code goes. Its value is boring and enormous: when every feature asks “where do I put this?”, a team without an answer produces the big ball of mudeverything touching everything, where no change is safe. A pattern answers the question once, for the whole codebase. (This is CLAUDE.md-thinking at the largest scale: conventions written down so nobody—human or AI agent—has to re-decide them per feature. Indeed, “which layer does this belong in?” is precisely the kind of instruction your agents need in a design doc.)

10.2.3 Intuition: a restaurant🔗

A layered application is a restaurant. The waiter (presentation) talks to customers, writes down orders in a fixed format, and never cooks. The chef (business logic) decides whether the order is possible, applies the house rules, and never talks to customers. The pantry clerk (data access) is the only person who goes into the storeroom (database). The customer never wanders into the pantry; the chef never argues with a diner. Each role is replaceable—new waiter, same kitchen—precisely because the handoffs are standardized.

10.3 Running Example: TerpTasks Grows House Rules🔗

TerpTasks (multi-user since the database lecture) now has actual campus policies:

  1. A user may have at most 20 open tasks (the procrastination cap—our version of the reference outline’s 18-credit limit).

  2. A task’s due date may not be in the past at creation.

  3. Only the task’s owner may complete or delete it.

And here is how the intern (of REST-lecture fame) implemented rule 1, in the single file that is the whole application:

# mudball.py -- the whole app in one file: presentation + rules + SQL
@app.post("/tasks")
def create_task(body: dict):
    conn = sqlite3.connect("terptasks.db")
    # HTTP parsing, business rule, and SQL -- all in one place
    n = conn.execute(
        "SELECT COUNT(*) FROM tasks WHERE owner=? AND done=0",
        (body["owner"],),
    ).fetchone()[0]
    if n >= 20:
        return JSONResponse(status_code=422,
                            content={"error": "too many open tasks"})
    if body["due"] < str(date.today()):
        return JSONResponse(status_code=422,
                            content={"error": "due date in past"})
    cur = conn.execute(
        "INSERT INTO tasks (owner, title, due, done) VALUES (?, ?, ?, 0)",
        (body["owner"], body["title"], body["due"]),
    )
    conn.commit()
    return {"id": cur.lastrowid}

It works. Now run three thought experiments, and keep the answers in mind through the whole lecture:

  • Change storage (SQLite → PostgreSQL): edit every handler.

  • Test the 20-task rule: you must boot HTTP and a real database just to check an if.

  • Add a CLI or a Discord bot that creates tasks: copy-paste the rules, then keep two copies in sync forever.

Every one of these pains is a coupling pain—the three concerns are welded together. The rest of the lecture is the unwelding.

10.4 The Pattern🔗

Formal definition. Layered architecture organizes a system into horizontal layers, where each layer has one specific responsibility and communicates only with the layer directly adjacent to it (in the strict form, only downward).

+----------------------+

|  Presentation Layer  |   HTTP, validation, serialization       (api.py)

+----------------------+

           |  calls

+----------------------+

|  Business Logic      |   the rules: policies, workflows,       (services.py)

|  (Service Layer)     |   authorization, transactions

+----------------------+

           |  calls

+----------------------+

|  Data Access Layer   |   SQL / ORM operations, persistence     (repositories.py)

+----------------------+

           |  reads/writes

+----------------------+

|  Database            |   PostgreSQL / SQLite / MySQL / ...

+----------------------+

In plain English: slice the app by technical job, not by feature. Everything that speaks HTTP lives at the top; everything that speaks SQL lives near the bottom; everything that decides lives in the middle.

The dependency rule is what makes it a discipline rather than a diagram: dependencies point down, one layer at a time. Presentation imports the service; the service imports the repository; nothing imports upward. The service must not know what a status code is; the repository must not know what a “procrastination cap” is.

Intuition. Each boundary is a replaceability contract, and you’ve already seen each one pay off: the REST lecture swapped storage without touching the contract (repository boundary); FastAPI’s dependency_overrides swapped a fresh store into tests (the same boundary, exploited by tests). The layer boundary is to code what the API contract was to clients—a line across which the other side can change without your knowledge.

Common misconceptions.

  • “Layers = folders.” Making api/, services/, repositories/ directories does nothing if api.py still writes SQL. The pattern is the dependency rule, not the file tree.

  • “Layers = tiers.” Layers are logical separations; tiers are physical (separate machines). Our four layers run happily in one process. (The names blur in practice—“n-tier” is used for both—but the distinction matters when someone proposes “splitting into services” to fix a code-organization problem.)

  • “More layers = better architecture.” Each layer is real overhead—in latency, in ceremony, in files-touched-per-change. Four is canonical because it matches four genuinely different reasons to change.

  • “MVC is the same thing.” MVC organizes the presentation layer itself (how UI code separates state, display, and input). A Spring or FastAPI “controller” is the presentation layer of an n-tier app; the service and repository below it are outside MVC’s story.

10.4.1 The request flow, traced🔗

GET /tasks/123the reference outline’s flow, on our stack:

Browser/client                  "GET /tasks/123"

   |

   v

Presentation   api.py:      parse path, authenticate  --> service.get_task(user, 123)

   v

Service        services.py: check task 123 is visible to user (rule 3)

   v

Repository     repositories.py:  SELECT ... FROM tasks WHERE id = ?

   v

Database       returns the row

   ^

Repository     row -> Task object

   ^

Service        object passes policy -> returned as-is

   ^

Presentation   Task -> JSON, status 200 (or domain error -> 403/404)

   ^

Browser/client receives the representation

Notice the translation duty at each boundary going up: the repository turns rows into domain objects; the presentation turns domain results into HTTP. The service in the middle speaks only domain language—Task, User, TooManyOpenTasksnever Request, never SELECT. When you review code (or an agent’s diff), vocabulary is the fastest layer-violation detector: a word from the wrong layer’s language is a misplaced responsibility.

10.5 The Four Layers, One at a Time🔗

10.5.1 Presentation layer🔗

Responsibility: everything about talking to the outsideREST endpoints, request parsing, input validation, serializing results, mapping outcomes to status codes. React frontends, mobile UIs, and CLI interfaces are presentation too: alternative front doors to the same building.

# app/api.py (excerpt) -- speaks HTTP, delegates everything else
@router.post("/tasks", status_code=201)
def create_task(body: TaskCreate,
                svc: TaskService = Depends(get_service)) -> TaskOut:
    try:
        task = svc.create_task(owner=body.owner, title=body.title, due=body.due)
    except DomainError as exc:
        raise HTTPException(status_code=422, detail=str(exc))  # translate!
    return TaskOut.from_task(task)

The handler is thin on purpose: parse, delegate, translate, serialize. The try/except is the layer boundary made visible—the service raises domain exceptions (TooManyOpenTasks); the presentation layer alone knows that this maps to 422 (REST lecture vocabulary). If you ever see HTTPException raised inside a service, a layer has leaked.

10.5.2 Business logic layer (service layer)🔗

Responsibility: the application’s rules and workflowspolicies, calculations, authorization checks, transaction boundaries. This layer is the reason the software exists; the other three are plumbing it needs.

# app/services.py (excerpt) -- speaks pure domain language
MAX_OPEN_TASKS = 20

class TaskService:
    def __init__(self, repo: TaskRepository):
        self.repo = repo                      # depends on the layer below

    def create_task(self, owner: str, title: str, due: date) -> Task:
        if self.repo.count_open(owner) >= MAX_OPEN_TASKS:
            raise TooManyOpenTasks(f"{owner} already has {MAX_OPEN_TASKS} open tasks")
        if due < date.today():
            raise DueDateInPast(f"{due} is in the past")
        return self.repo.add(owner=owner, title=title, due=due)

Compare this to the mud-ball: the same if statements, but now they live in a class with no HTTP and no SQL in sight. That has two consequences worth dwelling on. First, every front door gets the rules for freethe web API, a future CLI, the Discord bot all call svc.create_task(...); the 20-task cap cannot be bypassed by coming in through a different entrance. Second—the payoff the reference outline calls “easier testing”—the rules are now testable in microseconds (The Payoff: Testing Each Layer in Isolation).

10.5.3 Data access layer (repository)🔗

Responsibility: all communication with the database—SQL queries, ORM operations, mapping rows to domain objects. Nothing else: no policies, no HTTP.

# app/repositories.py (excerpt) -- speaks SQL, hides it from everyone above
class SqliteTaskRepository:
    def count_open(self, owner: str) -> int:
        row = self.conn.execute(
            "SELECT COUNT(*) FROM tasks WHERE owner = ? AND done = 0",
            (owner,),                      # parameterized: injection-safe
        ).fetchone()
        return row[0]

The interface the service sees (count_open, add, get, complete) is written in domain vocabulary; the SQL is an implementation detail behind it. Swap in PostgresTaskRepository or the database lecture’s SQLAlchemy crud.py, and the service cannot tell the difference—that is the maintainability claim (“database changes usually affect only the data layer”) made concrete. In the mini-project the interface is a Protocol, so the swap is checked by types, not hope.

10.5.4 Database layer🔗

Responsibility: durable storage and integrity—PostgreSQL, MySQL, SQLite, MongoDB. Everything from the Relational Database Design & MySQL lecture is this layer: schema, constraints, transactions. One clarification the stack diagram invites: constraints living “down here” is not a violation of “rules live in the service.” The service enforces policies (a cap of 20 is a choice); the database enforces invariants (a task must have an owner—true in every possible application). When in doubt: if the rule could plausibly change next semester, it’s service; if violating it means the data is corrupt, it’s schema.

10.6 The Payoff: Testing Each Layer in Isolation🔗

The reference outline lists “easier testing” as an advantage; here is that advantage in runnable form—the heart of this lecture.

The fake repository. Because the service depends on an interface, tests hand it an in-memory stand-in:

# tests/test_service_unit.py (excerpt)
class FakeTaskRepository:
    """In-memory repo: no SQL, no files, no server. Milliseconds."""
    def __init__(self):
        self.tasks, self.next_id = {}, 1
    def count_open(self, owner):
        return sum(1 for t in self.tasks.values()
                   if t.owner == owner and not t.done)
    def add(self, owner, title, due):
        t = Task(self.next_id, owner, title, due, False)
        self.tasks[t.id] = t; self.next_id += 1
        return t
    # ... get / complete, ~15 lines total


def test_twenty_first_task_is_rejected():
    svc = TaskService(FakeTaskRepository())
    for i in range(20):
        svc.create_task("alice", f"task {i}", date(2099, 1, 1))
    with pytest.raises(TooManyOpenTasks):
        svc.create_task("alice", "one too many", date(2099, 1, 1))

Step through what just happened. We tested the flagship business rule—boundary condition and all—with no HTTP server, no database, no fixtures on disk. The test constructs a service, feeds it a fake, and asserts on domain exceptions. It runs in under a millisecond, so you’ll run it after every save; it cannot flake on a port conflict or a stale schema. Now try to write this test against mudball.py: you can’t call the rule without going through HTTP and SQLite, because the rule has no name of its own—it’s three lines in the middle of a handler. Testability is not something you add; it’s a property of where the boundaries are.

Each layer gets the kind of test it deserves:

Layer

Test style

Uses

Speed

service

unit, with FakeTaskRepository

pure Python

microseconds

repository

integration, real (temp) SQLite

SQL actually runs

milliseconds

presentation

contract, TestClient over full stack

REST lecture's tests

milliseconds

database

schema tests (database lecture)

constraints fire

milliseconds

The pyramid shape is deliberate: many fast rule tests, fewer stack tests. The REST lecture’s contract tests don’t disappear—they become the outermost check that the layers still compose.

10.7 The Honest Trade-offs🔗

10.7.1 Performance overhead🔗

Every request traverses every layer—UI → Service → Repository → Databaseeven trivial ones. A health-check endpoint pays four function calls and two translations to return "ok". In-process, this is nanoseconds and almost never matters; what does bite is each layer naively adding its own query (the N+1 problem from the database lecture can hide between layers, where no single layer sees the whole flow). Measure before blaming the pattern—but know the cost is real.

10.7.2 Rigidity: one feature, four files🔗

Add a “priority” field end-to-end and you will touch the schema, the repository, the service, and the API schema—four edits for one concept. That’s the price of separation, and on a small CRUD app it can feel like ceremony. It is also, note, exactly the shape the The Elephant-Goldfish Model design doc’s “every single file” list captures—layered systems make that list predictable, which is why agents work well in them.

10.7.3 Pass-through code (the real disease)🔗

The most common degeneration: layers that merely forward.

# services.py -- adding nothing
def get_task(self, task_id):
    return self.repo.get(task_id)      # no rule, no translation, no check

When most of a service looks like this, the middle layer is dead weight—every change costs a stop at a station where nothing happens. Two honest responses: (a) accept modest pass-through as the reserved seat for rules that are coming (our get_task will soon check rule 3—visibility); (b) if a system is pure CRUD with genuinely no rules, admit it and collapse a layer. The unforgivable option is ritual: layers maintained because the diagram says so. Architecture is a tool, not a liturgy.

10.7.4 When to use it (and not)🔗

Use layered architecture when: building CRUD-plus-rules applications (the vast majority of business software—student information systems, banking, e-commerce backends, hospital systems); the team is small; requirements are understood; you’re teaching or learning fundamentals. Reach elsewhere when: components must scale/deploy independently (→ microservices), the domain is deeply event-shaped (→ event-driven), or the business core must be fanatically isolated from frameworks (→ hexagonal/clean, below).

10.8 The Neighborhood: Comparison with Other Architectures🔗

Architecture

Main idea

Relation to layered

Layered

organize by technical responsibility (UI, business, data)

---

MVC

separate Model/View/Controller

organizes the presentation layer's interior

Microservices

split into independently deployable services

each service is often layered inside

Event-driven

components communicate via async events

replaces the call-down chain with messages

Hexagonal

core defines interfaces; HTTP and DB are adapters

rotates the diagram: DB becomes a plugin beside HTTP

Clean

domain at center; dependencies point inward

layered with the dependency rule radicalized

The hexagonal row deserves one more sentence, because our mini-project quietly took a step toward it: the moment TaskService depended on a TaskRepository interface (with SQLite and the fake as interchangeable implementations), the database stopped being “the foundation” and became “a replaceable adapter.” Hexagonal and clean architecture take that idea to its logical end. Layered is where everyone starts; knowing what the others change is how you’ll recognize when to move.

10.9 GitHub-Ready Mini-Project: terptasks-layered🔗

See GitHub

10.10 Practice Exercises🔗

10.10.1 Basic🔗
  1. E1. Take mudball.py and highlight each line with P (presentation), S (service), R (repository), or D (database). Which lines carry two letters at once? Those are the coupling points.

  2. E2. For each snippet, name the layer it belongs in—and the layer it’s probably in when found in the wild: (a) raise HTTPException(404); (b) SELECT COUNT(*) ...; (c) if user.role != "admin": raise NotAllowed; (d) CHECK (length(title) > 0); (e) converting a Task to JSON.

  3. E3. Draw the request trace for PATCH /v1/tasks/7/completion sent by bob for a task owned by alice, showing exactly which layer stops the request and what crosses each boundary on the way back up.

  4. E4. State the dependency rule. Then explain which rule is violated (and why it matters) if: (a) services.py imports HTTPException; (b) repositories.py imports MAX_OPEN_TASKS; (c) api.py opens a sqlite3.connect(...).

  5. E5. Why does test_twenty_first_open_task_is_rejected need no database, and what specifically about mudball.py makes the equivalent test there require one?

10.10.2 Intermediate🔗
  1. E6. Finish the refactor: mudball.py also had a GET /tasks?owner=... listing endpoint (write it if missing). Move it through all four layers—repository method, service method (rule 3: only your own tasks), route—with one unit test and one contract test.

  2. E7. Add rule 4: a task may not be completed before its due date minus 30 days (no completing December tasks in July—this is a procrastination app with standards). Implement it touching exactly one file, and write down which file. Then add the test. What does the one-file answer tell you about where the boundaries are?

  3. E8. Write PostgresTaskRepository (or reuse the database lecture’s SQLAlchemy crud.py behind an adapter) satisfying the TaskRepository protocol. The graded criterion: both test files pass unchanged with the new repository wired into main.py.

  4. E9. Add a second front door: cli.py, a command-line interface (python -m app.cli add "study" 2099-01-01 user alice) that calls TaskService directly. Demonstrate that the 20-task cap holds from the CLI without writing any new rule codethen write one paragraph on what this proves about the service layer.

  5. E10. Introduce a deliberate pass-through: add services.get_open_count(owner) that just calls the repo. Now argue both sides in a short design memo: keep it (reserved seat) or let the API call repo.count_open directly (skip a layer)? Cite the trade-offs section, pick a side, and state the rule your team would adopt.

10.10.3 Advanced🔗
  1. E11. Rotate to hexagonal. Invert the remaining arrow: move TaskRepository (the Protocol) into domain.py, make repositories.py import from the domain, and add a second adapter—JsonFileTaskRepository. Draw before/after dependency diagrams and explain, in terms of the comparison table, what just became possible that wasn’t before.

  2. E12. Layer-violation linter. Write a script (or Claude Code slash command + CI job, per the (part "Claude Code Files") lecture) that fails the build if forbidden vocabulary crosses layers: HTTPException|status_code in services.py/repositories.py, sqlite3|SELECT|INSERT outside repositories.py, import app.api anywhere below the presentation layer. Run it on mudball.py and count the violations.

  3. E13. The architecture review, EGM-style. Write the four-section design doc (The Elephant-Goldfish Model lecture) for migrating terptasks-layered to hexagonal architecture, including the Alternatives section arguing why you did not choose microservices or event-driven for a campus task app. Goldfish-test the doc; deliver the doc plus the critic’s findings.

10.11 Summary🔗

10.11.1 Key takeaways🔗
  1. Layered architecture = one responsibility per layer + the dependency rule (talk only downward, one layer at a time). The folders are cosmetic; the rule is the pattern.

  2. The four layers answer “where does this code go?”HTTP at the top, rules in the middle, SQL below, storage at the bottom. Vocabulary is the violation detector: a word from another layer’s language is misplaced code.

  3. Boundaries are replaceability contracts. Swap SQLite for PostgreSQL, add a CLI beside the API, hand tests a fake—each swap exercises a boundary, and each is impossible in the mud-ball.

  4. The testing pyramid falls out of the layers: many microsecond unit tests against fakes for the rules; fewer full-stack contract tests for the translation. Testability is a property of where the boundaries are.

  5. The costs are real: per-request overhead, four-files-per-feature rigidity, and the pass-through disease. Layers are a tool, not a liturgy—collapse one when it provably adds nothing.

  6. Policies vs. invariants: rules that could change live in the service; truths that make data valid live in the schema. Both are “business logic”; they have different homes.

  7. Know the neighborhood: MVC organizes presentation’s interior; microservices distribute; hexagonal/clean invert the data-layer arrow. A repository interface is your first step on that road.

10.11.2 Terminology🔗

Term

Meaning

layered / n-tier architecture

system organized into single-responsibility layers

presentation layer

HTTP/UI: parse, validate, delegate, translate, serialize

service layer

policies, workflows, authorization, transactions

data access / repository

all SQL/ORM; rows and domain objects translated

dependency rule

layers depend only downward, one layer at a time

domain exception

rule violation in domain vocabulary (TooManyOpenTasks)

translation

boundary duty: rows to objects, exceptions to status codes

composition root

the one module that wires all layers together

fake / test double

in-memory stand-in satisfying a layer's interface

pass-through code

a layer method that only forwards, adding nothing

big ball of mud

the pattern-less alternative: everything couples to everything

layers vs. tiers

logical separation vs. physical (machine) separation

10.11.3 Common mistakes🔗
  • SQL in route handlers; HTTPException in services (vocabulary leaks).

  • Mistaking folders for architecture while dependencies still tangle.

  • Testing business rules only through HTTP + database (slow, flaky, and it means the rules have no callable name).

  • Letting every service method become pass-through and defending the layer as ritual.

  • Duplicating rules in each front door instead of sharing the service.

  • Putting changeable policy in schema constraints, or data invariants only in Python.

  • “Fixing” a code-organization problem with microservices (tiers ≠ layers).

10.11.4 Connections🔗
  • Backward: this lecture names what the FastAPI (presentation), (part "REST APIs") (the contract), and Relational Database Design & MySQL (data access + storage) lectures built; the E8 wiring exercise was the boundary payoff in advance; fakes-over-interfaces is the testing lecture’s dependency-injection theme grown up.

  • Forward: the OWASP notes’ access-control flaws are usually misplaced-layer bugs (authorization in the UI instead of the service); hexagonal/clean architecture radicalize today’s dependency rule; and for AI-assisted work, layered systems are what make the The Elephant-Goldfish Model “every single file” implementation lists short, predictable, and safe to hand to an agent.

10.12 Instructor Notes🔗

Where students struggle.

  • “It’s just folders.” The E4 violations drill and the E12 linter make the dependency rule mechanical instead of aesthetic.

  • Which layer does validation go in? The honest answer is “each layer validates its own concern” (Pydantic shape-checks in presentation, policy in service, invariants in schema)—teach it with E2(d) vs. rule 2.

  • Where do transactions live? Service owns the boundary (“this workflow is atomic”), repository owns the mechanics. Mention it before the lab or it derails E6.

  • Pass-through guilt or pass-through blindness. Students either add ceremony everywhere or collapse everything. The E10 memo forces an actual position.

  • Layers vs. tiers vs. MVC. Ten minutes of terminology now saves a semester of confusion; the misconceptions in The Pattern are quiz gold.

Live demonstrations.

  1. The one-file test race. Time pytest tests/test_service_unit.py (milliseconds), then write the equivalent cap test against mudball.py live—watch the setup ceremony grow. The visceral speed difference sells the whole lecture.

  2. The E7 rule: add it, show one file changed, run both suites green.

  3. Vocabulary grep as architecture review: grep -rn "HTTPException" app/services.py on a seeded violation.

  4. Course tie-in: ask Claude Code to classify each mudball.py line by layer (E1), then compare with the class’s answers—and note how much better the agent does with the layer names in context than without.

Quiz seeds. (1) Name the four layers and each one’s single responsibility; state the dependency rule. (2) raise HTTPException(403) appears in services.py. What’s wrong, what breaks (name a concrete consumer), and where does that logic belong? (3) Why can the 20-task cap be tested without a database in the layered version but not in the mud-ball? What property of the design makes it so? (4) Give one honest disadvantage of layering and the degenerate form it can take; when is collapsing a layer the right call? (5) In one sentence each: what do hexagonal architecture and microservices change relative to layered? (6) The team adds a Discord bot front door. What must be written, and what must not be duplicated? Why?

Homework ideas. E6+E7+E9 as the standard set (rounding out the refactor, one-file rule change, second front door—together they prove all three boundaries). E8 connects to the database lecture for a two-lecture arc. E12’s linter makes an excellent pair-programming lab with Claude Code; E13 is the capstone bridging architecture to the EGM design-doc discipline.

This lecture expands Layered_architecture.md (the course’s original outline, with Java/Spring examples) into the TerpTasks stack. The outline’s university-registration example survives as the house rules of the running example; its Spring controller is our FastAPI router. For more: Fowler’s Patterns of Enterprise Application Architecture (the repository and service layer patterns are chapters 10 and 9), and the C4 model for drawing these diagrams at every zoom level.