The Technical Specification
What are you building on?
Chapter 6 — The Technical Specification
“Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious.” — Frederick P. Brooks, Jr., The Mythical Man-Month (1975)
“Everyone then who hears these words of mine and does them will be like a wise man who built his house on the rock.” — Matthew 7:24 (ESV)
Why This Matters
You are still wearing the architect’s hat you put on last week, but the job has changed. Week 5 was the materials decision — which language, which framework, which database, which host, and which of those you had never touched before — recorded in architecture decision records so nobody, including you in October, has to re-litigate it. That was what you will build with. This week is how it fits together. You are in the design phase of the life cycle, and this is the last week of it: next week you put on the project-manager hat and estimate the build, and the week after that a room reviews this document and baselines it, at which point changing the design starts costing a change request instead of an eraser.
A technical specification — a system design document, an SDD, a “tech spec,” pick your employer’s word — is the bridge between a requirement and a commit. On one side is FR-07: the system shall notify a user about food about to expire. On the other side is a function you type at 11pm in Week 12. Between them sit at least a dozen decisions: what “about to expire” means in days, who computes it, where it is stored, what happens when the send fails, whether the same item can notify twice, what a user with zero items sees. You will make those decisions this week, on purpose, in daylight — or in Week 12, one at a time, exhausted, in the middle of writing something else. There is no third option. The specification is not extra work. It is the same work, done when it is cheap.
Here is the honest warning: this is the week most capstone students quietly skip. You have an idea you love, a stack you just chose, and a keyboard right there, and writing a design document feels like procrastination with extra steps. It is not. Every hour of design this week buys back multiple hours of construction later — I will not quote you a multiplier to two decimal places, because anyone who does is selling something, but the direction of the effect is not in dispute and you will feel it in Week 12 either way. You will feel it as time you have or time you don’t.
The AI thread runs from both sides, and both sides are sharp. As a tool, an assistant is genuinely good at specification work: it drafts interface contracts, enumerates the error cases you would forget, and — its best trick — argues against your design when you ask it to. As a workload, if your project has an AI feature, that feature is now a dependency you must specify: a prompt contract, a token and latency budget, and defined behavior for the moment the model returns confident nonsense. The spine rule does not move. The assistant drafts; you decide; you sign your name and answer for it in Week 8.
The apologetic question is the one the epigraph asks: what are you building on? Both houses in that parable got the same storm, and only one was standing afterward. That is not a metaphor I am stretching to fit software. We take it seriously in §6.12.
6.1 — What a Specification Is For: the Reader Is a Stranger, and the Stranger Is You in Week 12
A technical specification answers one question precisely enough to act on: how will this system satisfy its requirements? It is not the requirements — those are in docs/requirements.md, from Weeks 3 and 4. It is not the code. It is a set of decisions, each written so it can be checked, disputed, and later blamed.
The way to know whether yours is good is to name its readers. There are three, and none of them is your professor.
| Reader | When they read it | What they need from it |
|---|---|---|
| The reviewer | Week 8, in the design review | Enough detail to find the flaw before it costs money |
| You, in Week 12 | 11pm, mid-integration, tired | A decision already made, so you don’t make it badly |
| Whoever inherits it | After you graduate | The why, which the code never contains |
Two of the three are strangers to your intentions, and one of them is you. That is the stranger test, the standard this course holds a specification to: hand the document to a competent developer who has never spoken to you, point at one requirement, and ask them to build it. If they must ask you a question to start, the specification has a hole where an answer should be. A capstone-sized specification has seven load-bearing parts, and the rest of this chapter builds them in order.
| Part | The question it answers | Built in |
|---|---|---|
| Context & containers | What is inside the boundary, outside it, and what runs where? | §6.2 |
| Component responsibilities | Who does what, and who owns which piece of state? | §6.3 |
| Interface contracts | What can be called, with what, returning what? | §6.4 |
| Data model | What is stored, keyed how, with what invariants? | §6.5 |
| Sequence flows | How do the critical paths actually run? | §6.6 |
| Errors & edge cases | What happens when it doesn’t work? | §6.7 |
| Open questions & risks | What have you not decided, and what does it block? | Milestone 6 |
For a project this size that lands around eight to fifteen pages. Shorter and you are hand-waving; much longer and you are writing prose instead of making decisions. The measure is not page count — it is decisions per page. The blank structure is in code/tech-spec-template.md; copy it into your repository today.
Coach’s Note — A specification is a promise to your future self, and your future self cannot renegotiate. Every fuzzy sentence you leave in is a loan at a terrible rate, payable in Week 12 in the one currency you will have run out of: hours.
One more thing to settle. This document is alive until Week 8 and baselined after it. Right now you can rewrite the data model on a whim for the price of an afternoon; after the design review, the same change costs a change request, a change-log entry, and a look at the schedule. That asymmetry is deliberate and it means this is the week to be aggressive about changing your mind. Break your own design now, while breaking it is free.
6.2 — Context, Containers, Components: Three Levels of Diagram, and When to Stop
The classic failure in a student design document is not too few diagrams. It is one diagram at the wrong zoom, doing three jobs badly. Use three levels. This is Simon Brown’s C4 model — Context, Containers, Components, Code (https://c4model.com) — and it earns its place because it tells you when to stop drawing.
Level 1 — Context. Your system is one box. Around it: the people who use it and the systems it calls. Nothing inside.
Level 1 — Context (PantryPilot, our running example; yours will differ)
Household member ──adds items, views "expiring soon"──▶ ┌────────────┐
browser · 3–6 people │ PantryPilot│
Product Lookup API ◀──barcode lookup─────────────────────│ the whole │
3rd party · rate-limited · may be down │ system │
Model provider API ◀──recipe suggestion (optional)───────│ │
3rd party · priced per token · may refuse └────────────┘
Level 2 — Containers. Open the box. A container is a separately runnable thing: a browser client, an API service, a worker, a database, an object store, a queue. Each gets a technology label and a one-line responsibility; each arrow gets a payload and a protocol.
Level 2 — Containers (what runs, and what it is written in)
┌────────────────┐ HTTPS / JSON ┌──────────────────────────────┐
│ Web client │ ─────────────▶ │ API service │
│ browser app │ ◀───────────── │ handlers · auth · domain │
│ <your choice> │ 200 / 4xx / 5xx│ logic · holds all secrets │
└────────────────┘ └──────────────┬───────────────┘
┌────────────────┐ SQL ┌──────────────▼───────────────┐
│ Expiry worker │ ─────────────▶ │ Relational database │
│ daily 06:00 │ │ users · items · products · │
│ <your choice> │ │ notifications │
└───────┬────────┘ └──────────────────────────────┘
│ HTTPS — server side only; the API key never reaches a browser
▼
Product Lookup API · Model provider API
Level 3 — Components: open one container — the one where the hard part lives — and show the modules inside. One or two containers, never all of them. Level 4 — Code: don’t; class diagrams of your own source have a half-life of about four days. The rule for stopping is simple: draw level 3 only where a reviewer would otherwise have to guess.
Four rules of diagram hygiene, which is where most student diagrams fall apart. Every box carries a responsibility and a technology — Backend is not a label; API service — handlers, auth, domain logic (Python/FastAPI) is. Every arrow carries a direction, a payload, and a protocol; a bare arrow communicates the least useful available fact, that two things are somehow related. Every diagram carries a legend, a version, and a date, because you will make three versions before Week 16. And the trust boundary is drawn — mark what runs on the user’s machine and what runs on yours, because secrets live on exactly one side of that line.
Coach’s Note — The arrows are the design. Anybody can name four boxes; the arrows are where the coupling, the latency, the failure modes, and the secrets live. Beautiful boxes and bare arrows means you have drawn a picture of a system you have not designed yet.
A word on the second example, because a different shape of project needs a different diagram. TraceLens — the command-line log-anomaly tool — has no browser, no auth, no HTTP. Its context diagram has a log source on the left, a report sink on the right, and an operator at a shell; its container diagram has one container. That does not make the diagram optional; it makes it small. Draw the diagram your system actually has, not the one you saw in somebody else’s blog post. For tooling, Appendix A has the setup — and note that text-based diagram tools such as Mermaid (https://mermaid.js.org) and PlantUML (https://plantuml.com) have one property that matters enormously: the source is a text file that lives in Git and shows up in a diff. A photograph of a whiteboard does not diff. Whatever you choose, commit both the source and the rendered image under docs/diagrams/.
6.3 — Module Responsibilities and the Single-Owner Rule
The diagram shows the boxes. The component responsibility table says what they are for, and it does more work per line than any other part of the document.
| Component | Responsibility (one sentence, starts with a verb) | Owns (state) | Depends on | Serves |
|---|---|---|---|---|
auth | Authenticates a household member and issues a session | sessions | database | FR-01, NFR-06 |
pantry | Creates, updates, and lists items in a household’s pantry | items | database, products | FR-02, FR-03 |
products | Resolves a barcode to a product name and unit, caching results | products | Lookup API, database | FR-03 |
expiry | Computes days-to-expiry and selects items that qualify for notice | (none) | pantry | FR-06, FR-07 |
notify | Renders and delivers an expiry digest, once per item per window | notifications | expiry, mail transport | FR-07 |
suggest | Produces a meal suggestion from items on hand, with a non-AI fallback | (none) | pantry, model provider | FR-09 |
Four rules keep that table honest. One sentence, starting with a verb. If you cannot say a component’s job without “and” appearing twice, you have two components. utils is not a component; it is a confession.
The single-owner rule. For every piece of state and every behavior, exactly one component is responsible. If pantry and expiry both compute days-to-expiry they will disagree — not today, but on the day one is fixed for a daylight-saving bug and the other is not. If two components write the same table, one of them is wrong and you do not yet know which.
Dependencies point one way. Read the “Depends on” column as a graph. If you can start at a component, follow dependencies, and arrive back where you started, that cycle means those components are really one component you have not admitted to. Break it: extract the shared piece, or invert the dependency so the lower-level module knows nothing about the higher one. And traceability runs both directions: every component serves a requirement, every Must-priority requirement has a component. A component that serves nothing is scope you invented. A Must requirement no component serves is a hole you found in Week 6 instead of Week 14 — which is the entire point of writing this down.
Coach’s Note — To find a bad decomposition fast, read your responsibility sentences out loud in a row. The bad ones sound like job titles — “handles the data layer,” “manages the frontend.” The good ones sound like verbs a person could do — “resolves a barcode to a product.” Job titles hide decisions. Verbs expose them.
6.4 — Interface Contracts: Endpoints, Payloads, Status Codes, Errors
An interface is the part you cannot change quietly. Internals you rewrite on a Tuesday; a contract has a caller on the other side. So it gets specified in more detail than anything else in the document. Every interface — HTTP endpoint, CLI command, library function, queue message — needs the same eight facts: name and purpose (and the requirement it serves), authorization, inputs with types and validation, success output with an example, every error, idempotency, side effects, and limits.
Here is the same contract written badly and written well. The bad one is not a straw man; it is what a first draft usually looks like.
BAD: POST /items — adds an item. Returns the item. Errors return an error.
GOOD: POST /items (serves FR-03)
Auth session cookie required; 401 if absent, 403 if the session's
household does not match household_id.
Request { "household_id": uuid required
"barcode": string optional, 8–14 digits
"name": string required if barcode absent, 1–120 chars
"quantity": integer required, 1–999
"expires_on": YYYY-MM-DD optional, must be >= today }
Success 201 Created — { "id": uuid, "name": "...", "quantity": 2,
"expires_on": "2026-08-14", "source": "lookup" | "manual" }
Errors 400 invalid_field 401 not_authenticated 403 wrong_household
409 duplicate_item 422 lookup_unavailable 429 rate_limited
body: {"error":{"code":"...","field":"...","message":"..."}}
Idempotency client sends Idempotency-Key; a repeat within 10 minutes
returns the original 201 body, not a second row.
Writes one row in items; possibly one row in products (cache fill).
Limits body <= 4 KB; 60 requests/minute per session.
About twenty lines, and it removed roughly fifteen decisions from Week 12. Choose status codes deliberately and use them identically across every endpoint; the semantics are defined in RFC 9110 (https://www.rfc-editor.org/rfc/rfc9110.html), and this is the short version you will actually use:
| Code | Use it when | Do not use it when |
|---|---|---|
| 200 / 201 / 204 | Success with a body / created a resource / success with nothing to say | You created something and returned 200 |
| 400 | The request is malformed or fails validation | The caller simply isn’t allowed — 401/403 |
| 401 vs 403 | We don’t know who you are / we do, and you can’t | Using them interchangeably |
| 404 | The resource does not exist, or must appear not to | Your own code threw and you are hiding it |
| 409 | The request conflicts with current state | Plain validation failure — that’s 400 |
| 429 | The caller exceeded your rate limit; set Retry-After | You exceeded someone else’s — that’s your 503 |
| 500 | Your code broke. Log it; never leak the trace | You can name the client’s mistake |
One error envelope for the whole system, decided once, here. The shape matters less than the fact that there is exactly one of it; a codebase with three error shapes has a client with three error handlers and a bug in two of them. And the discipline is identical off the web — TraceLens is a CLI, so its contract reads:
tracelens scan <path> [--since ISO8601] [--format json|text] [--fail-on high]
stdout: the report and nothing else (so it can be piped)
stderr: progress, warnings, diagnostics
exit 0: completed, no findings at or above --fail-on
exit 1: completed, findings at or above --fail-on
exit 2: could not scan (bad path, unreadable file, malformed input)
Flags are parameters, exit codes are status codes, and the stdout/stderr split is your response envelope. Same eight facts, different clothes.
Coach’s Note — Write the contract for the interface you are least sure about first. The endpoint you cannot fully describe is the one where your design is still fuzzy, and finding that out costs twenty minutes today or two days in Week 12.
6.5 — The Data Model: Entities, Relationships, Keys, and the Migration You Will Need
Brooks earned his epigraph. Show me your tables and I know your system; show me your handlers and I know only your habits. The data model is the load-bearing wall, and it is the hardest thing to change once real data sits on it. Specify each entity with six things: purpose, key, attributes with type and nullability, invariants, relationships, and lifecycle.
Entity: item (serves FR-02, FR-03, FR-06)
Purpose: one physical thing in a household's pantry, until consumed.
id uuid PK
household_id uuid NOT NULL FK -> household(id) ON DELETE CASCADE
product_id uuid NULL FK -> product(id) (null = manual entry)
name text NOT NULL 1–120 chars
quantity integer NOT NULL CHECK (quantity > 0)
unit text NOT NULL enum: 'ea' | 'g' | 'ml'
expires_on date NULL (null = does not expire)
created_at timestamptz NOT NULL default now()
consumed_at timestamptz NULL (non-null = no longer in the pantry)
Invariants
I1 An item with consumed_at set never appears in an expiry notice.
I2 (household_id, name, expires_on) is NOT unique — two identical
yogurts are two rows, on purpose.
I3 expires_on is a calendar date in the household's timezone.
Relationships household 1 ──< item >── 0..1 product
Volume ~200 rows/household · 6 households · ~50 new rows/week
Six decisions worth making now, before anything depends on them:
- Surrogate keys, plus a unique constraint on the natural key. A generated
idis the primary key;barcodeis separately unique onproduct. Natural keys look elegant until the real world changes one. - Timestamps in UTC, one type, everywhere — and decide here, once, where conversion to local time happens. Half of all “wrong day” bugs in student projects are a conversion happening in two places.
- Money in minor units as integers. Never floats. Write that sentence in now if your project touches money at all.
- Enumerations are constrained, not free text. A
unitcolumn that accepts any string will containg,G,grams, andgby Week 11. - A deletion policy per entity — hard or soft, cascade or restrict — in the schema, not in your memory.
- Nullability means one thing per column, and you can say it in words. If you cannot (“expires_on is null when… uh, sometimes”), you have two concepts in one column.
Now the part students skip: the migration plan. From Week 9 forward your database holds data you care about, and “I dropped it and recreated it” stops being an acceptable answer. Decide three things and write them down: (1) migration tool, or numbered SQL files applied in order and tracked in a schema_migrations table — both fine at this scale; (2) forward-only or reversible — forward-only is acceptable if you say so; (3) where they live and how a stranger runs them, e.g. migrations/0001-initial.sql, applied by the setup script your Week 14 clean-machine test will execute. A migration a stranger cannot run is not done. Finally, here is the smell you should be able to recognize in your own draft:
BAD: items(id, user, data1, note, exp, stuff, created)
exp text: "8/14", "next tuesday", "2026-08-14" -> cannot be compared
user the display name, duplicated in three tables -> breaks on a rename
data1 sometimes quantity, sometimes unit -> cannot be queried
stuff undocumented JSON, written by two modules -> no single owner
Coach’s Note — You can refactor a function over lunch. You cannot refactor a table full of data you care about over lunch. Spend a disproportionate share of this week on the data model; if you get only one part of the specification right, make it this one.
6.6 — Sequence Flows for the Three Paths That Matter Most
You do not diagram every flow. You diagram three, chosen on purpose: the money path (the one flow the system exists to perform), the risky path (the one crossing a boundary you do not control), and the failure path (the risky path again, with the boundary broken). The third is what separates a specification from a picture.
Flow 2 — "Add an item by barcode" (serves FR-03; risky: third-party lookup)
Client API service Product Lookup API Database
│ POST /items │ │ │
│ {barcode, qty} ──▶│ 1. validate + authz │ │
│ │ 2. SELECT product ───────────────────────▶ │
│ │ ◀──────────── cache miss ───────────────── │
│ │ 3. GET /product/{barcode} ──▶ │
│ │ ◀───── 200 {name, unit} ───── │
│ ◀── 201 {item} ───│ 4. INSERT product, then INSERT item ─────▶ │
Now the same flow told honestly. This table is worth more than the diagram above it.
| Step | What can go wrong | System behavior | User sees |
|---|---|---|---|
| 1 | Barcode is 6 digits | Reject before any I/O | 400, inline message |
| 2 | Database unreachable | Fail fast; do not call the third party | 503, “try again shortly” |
| 3 | Lookup times out (>2s) | One retry with backoff, then give up | ”Enter the name yourself” |
| 3 | Lookup returns 429 | No retry; honor Retry-After; log | Same manual-entry prompt |
| 3 | Lookup returns an empty name | Treat as a miss, not a hit | Same manual-entry prompt |
| 4 | Cache insert races another request | Unique constraint on barcode; upsert | Nothing — invisible |
| 4 | Item insert fails after cache fill | Item not created; stray cache row is harmless | 500, nothing saved |
Look at what just happened. Specifying the failure branch forced three decisions the happy path never surfaced: the timeout value, the retry policy, and — the important one — that a failed lookup must not stop the user from adding an item. That last one is a product decision hiding inside a technical one, and you just made it in Week 6 instead of finding it in Week 12 with a user standing in front of you holding a can of beans.
Coach’s Note — If drawing a sequence flow does not change your interface contract at least once, you drew it after you decided instead of to decide. Draw it first. Let it break something.
6.7 — Error Handling, Edge Cases, and What the System Does When the Network Dies
A specification that describes only success describes about a third of the code you are going to write. Sort failures into six categories and give each a policy, decided once and applied everywhere.
| Category | Example | Policy to decide now |
|---|---|---|
| Invalid input | 14-digit barcode, negative quantity | Reject at the boundary; it never reaches the database |
| Not authorized | Wrong household, expired session | 401 vs 403, and what the client does with each |
| Not found | An item id that doesn’t exist | 404 — and whether existence itself is a secret |
| Conflict | Two edits to the same item | Last-write-wins, or a version check and 409 |
| Dependency failure | Timeout, 5xx, rate limit | Timeout value, retry count, backoff, fallback |
| Exhaustion | Free tier hit, disk full, 10,000 items | Detect, degrade, and tell somebody |
For every call that leaves your process, answer three questions in the document. What is the timeout? A number, in seconds — “the default” is not an answer, because on some clients the default is forever. What happens on failure? Retry how many times, with what backoff, and with what hard cap; or fail immediately. An uncapped retry loop is not resilience, it is a denial-of-service attack you wrote against your own dependency. Does the user find out? Silent failure is the worst of the three options; a user told “we couldn’t reach the product database — type the name and we’ll fix it later” is a user still using your software.
Then build an edge-case register. It lives in the specification and in Week 11 it becomes test cases almost verbatim: the empty state (zero items, first run — the most-skipped screen in student software and the first one a grader sees); exactly one of something, where your layout assumed a list; ten thousand of something, and whether anything paginates; the same request twice (double-click, retry, refresh); unicode, emoji, and right-to-left text in every free-text field; a 500-character name; a concurrent edit by two roommates in the same second; clock and calendar boundaries — daylight saving, month ends, February 29; each dependency down, individually; and the free tier running out, which happens in Week 12, because that is when free tiers run out.
Three things your specification should forbid outright, in a sentence each: no silent catch-and-continue, no stack trace rendered to a user, no unbounded retry.
6.8 — Specifying an AI Component: Prompt Contract, Budget, and Fallback
If your project has an AI feature, this section is not optional. A hosted model is a dependency with four unusual properties: it is nondeterministic, it is priced per unit of text, its latency is measured in seconds rather than milliseconds, and it can return an answer that is fluent, well-formatted, and wrong. Specify it as a prompt contract.
| Field | What you write down |
|---|---|
| Purpose | The requirement it serves, in one line |
| Inputs | Exactly what context is assembled, from which tables, in what order |
| Privacy | What must never be sent — names, emails, anything your NFRs promised |
| Prompt template | A path in your repo, e.g. prompts/suggest-recipe.txt — versioned, not a string literal buried in a handler |
| Model & parameters | The exact model identifier and the date you verified it; temperature, maximum output length, stop conditions |
| Output contract | The exact schema you require back, the validator that enforces it, and what happens when validation fails — retry once with a repair instruction, then fall back. Never ship unvalidated output to a user or a database |
| Budget | Tokens per call, target p50/p95 latency, cost per call × calls per day, and a hard daily cap |
| Fallback | The non-AI behavior that still satisfies the requirement |
| Logging | What you record of prompt and response, where, for how long, and who can read it |
Two rows carry most of the weight. The fallback: your requirement is not “call a model,” it is FR-09, suggest a meal from what is on hand. The model is one implementation; a keyword match against a small local recipe list is another — worse, but working. Specify the fallback and you have also specified what to build first in Week 10, before the AI feature exists at all. The validator: model output goes through a schema check before it goes anywhere, and never into a privileged action — no generated SQL executed as written, no generated shell command run, no generated identifier trusted as a foreign key. Constrain the output to a shape you can verify, verify it, and keep the human where the judgment lives. Here, the human decides whether to cook the thing. That is the loop.
And on numbers: model prices, rate limits, context windows, and deprecation schedules change often, and any figure printed here would be wrong by the time you read it. So write each number into your specification with the date you checked it and the URL you checked it against. In Week 12, when the bill or the latency surprises you, you will know exactly which assumption expired. And read the provider’s data-retention and training terms yourself before you send one real user’s data through it — that is a Week 4 non-functional requirement with teeth.
Coach’s Note — Specify the fallback before you specify the model call. A feature that only works when a third party is having a good day is not a feature you can demo in Week 16, and Week 16 has a way of landing on somebody’s bad day.
6.9 — Two Specifications for the Same Feature: One Vague, One Buildable
This is the section this book exists for. Same requirement, same student, same project. Two documents. Version A — the one that usually gets written:
FR-07 — Expiry notifications
The system shall notify users about food that is about to expire.
Notifications should be timely and should not be annoying. A background
job will handle this. The UI will show expiring items in a different color.
Three sentences, professional-sounding, and they decide nothing. Watch what happens when a builder — you, in Week 12 — sits down with it. “About to expire” means how many days? From when, in which timezone? Email, in-app, or both? One notice per item or one digest per household? How often can the same item notify? Do consumed items notify? Items with no expiry date? What if the send fails? What does a household with zero expiring items get? Who computes it — worker, API, or client? Where is “we already told them” stored? What color, and does it pass the contrast NFR? Eleven questions, zero answers. Each will be answered eventually — at 11pm, by a tired person, with no notes, inconsistently across two code paths. Version B — the one that gets built:
FR-07 — Expiry notifications Priority: Must
Owner: notify · Depends on: expiry, pantry, mail transport
Definition An item is "expiring soon" when expires_on is within
EXPIRY_WINDOW_DAYS (default 3, per-household, range 1–14) of the current
date in the household's timezone, and consumed_at is null.
Trigger The expiry worker runs daily at 06:00 in the household's
timezone, and is idempotent: run twice in one day, it sends one digest.
Behavior
1. Select expiring-soon items per household (see Definition).
2. Exclude items with a notifications row for the same
(item_id, expires_on) — one notice per item per expiry date.
3. If the set is empty, send NOTHING. No "nothing to report" email —
this is the "not annoying" requirement, made concrete.
4. Else send ONE digest, up to 20 items, sorted by expires_on
ascending, with a "+N more" line beyond 20.
5. Insert one notifications row per listed item, with sent_at.
Data reads items, household · writes notifications(id, item_id,
expires_on, sent_at, channel). No public endpoint; the in-app list
reuses GET /items?expiring_within=3.
Errors Mail transport fails -> retry twice (30s, 300s); on final failure
log ERROR and write NO notifications rows, so tomorrow retries the same
items. Household without a verified email -> skip, log INFO, no retry.
Edge expires_on today -> included; past -> included, labeled "expired";
null -> never; timezone changed -> next run uses it, no backfill.
UI BOTH a color and a text label ("2 days") — color alone fails NFR-08.
Acceptance (Week 11 turns these into tests, verbatim)
AC-07.1 expiring in 3 days, never notified -> appears in the digest
AC-07.2 worker run twice in one day -> exactly one email
AC-07.3 household with nothing expiring -> zero emails
AC-07.4 mail transport down -> no notifications rows
OPEN QUESTION (blocks build of `notify`; needed by Week 9)
Which mail transport? Blocked on the cost check from Milestone 5.
Owner: me. Decide by: end of Week 7.
Version B is longer. That is not a defect; it is the point.
| Version A | Version B | |
|---|---|---|
| Decisions made | 0 | 11 |
| Decisions consciously deferred | 0 | 1 — named, dated, owned |
| Test cases derivable | 0 | 4, verbatim |
| Questions a stranger must ask you | 11 | 0 |
| Time to write / time it saves later | 4 min / none | ~35 min / hours, and one wrong data model |
Notice the open question. A good specification is allowed to not know something; it is not allowed to pretend to know, or to leave the gap invisible. An open question with a blocker, an owner, and a date is a professional artifact. An unmarked hole is a landmine. The full worked artifact — both versions, plus an interface contract, a data-model excerpt, and a sequence flow — is in code/pantrypilot-spec-excerpt.md, with a matching requirements excerpt in code/sample-requirements.md. More good-versus-bad pairs for every artifact in this course are in Appendix B.
6.10 — Using an Assistant on a Specification, and Where It Fails
Specification work is one of the places a modern assistant helps most — and one of the places an unverified draft does the most damage, because a wrong specification gets copied into code by a diligent person who trusted it.
Where it is genuinely good. Drafting an interface contract from a requirement: paste FR-03 and your data model, ask for the eight facts from §6.4, get a solid first pass in thirty seconds. Enumerating error and edge cases — it has read more failure modes than you have, and breadth is what it is for. Converting between forms: a responsibility table into a diagram description, a schema into a migration skeleton. And the best use, which almost nobody tries — arguing against your design:
“Here is my container diagram, my component responsibility table, and my interface list. You are the senior engineer who will inherit this project after I graduate. Name the five places this design will break first. For each, name the requirement it violates and the cheapest change that would prevent it. Do not compliment the design.”
You will get a review. Some of it will be wrong. Two items will be things you already half-knew and were avoiding.
Where it fails, predictably. It does not know your requirements unless you paste them, and it will confidently design for the requirements it imagines you have. It invents plausible specifics — endpoint paths, config keys, header names, limits — that are the right shape and the wrong fact; every one gets checked against the vendor’s documentation before it enters your document, exactly as in Milestone 5, and more urgently here, because a specification is what a builder trusts. It over-architects for an audience of one: ask it to design a system and you may get a queue, a cache, three services, and an event bus for a project with six users and one developer — your novelty budget from Week 5 did not reset, and every box is something you must build, run, debug, document, deploy, and demo. It optimizes for looking like a specification: fluent structure, headings in the right order, and underneath, sentences that decide nothing. Run the §6.9 test on anything it produces — count the decisions. And it cannot make your tradeoffs: whether to cut the AI feature, whether the schedule survives a normalized schema, whether your roommates will actually use this.
The workflow that holds: you decide the boundaries and the data model; the assistant drafts the contracts; you verify every fact; you rewrite every sentence you would not want to defend in Week 8. Then log it — which sections were AI-drafted, what you verified, what you changed — in docs/ai-usage.md, in the format given in Appendix C. You will sign this document. Sign only what you have read.
6.11 — Interactive Lab: The Architecture Sketchpad
On this chapter’s page you will find The Architecture Sketchpad. Use it now, before you draw anything in your real tool.
You get a canvas and a palette of typed nodes — client, API service, background worker, database, cache, queue, object store, third-party API — which you drag out and join with labeled edges. As you build, it generates the artifacts a specification actually needs: a component responsibility table, an interface list with request and response shapes, and a warning panel that fires when your drawing makes a claim you did not intend.
Do three passes. One — build your real system. Not the demo, not PantryPilot. Yours. Put down only the containers you will actually build, run, and demo in Week 16; if you catch yourself dragging out a queue because it looks serious, drag it back. Two — break it on purpose. Draw an edge from the client straight to the third-party API and watch the secret-exposure warning fire — that arrow means your API key is in a browser. Make two components depend on each other and watch the cycle warning; that means they are one component. Delete the data store and see what a system with nowhere to put state looks like. Leave a component with no owning responsibility and watch it get flagged. Every warning is a comment you would otherwise receive in Week 8, when it is expensive. Three — export and rewrite. Copy the generated responsibility table into your document as a starting point, then rewrite every sentence in your own words. The sketchpad can tell that auth depends on the database. It cannot tell you that auth owns sessions and nothing else. That sentence is yours.
What it teaches is worth naming: a diagram is a set of claims, not a picture. Every box claims something will exist and be maintained. Every arrow claims a dependency, a protocol, a failure mode, and sometimes a secret crossing a boundary it should not. The sketchpad makes those claims visible — which is exactly what a design review will do to you in two weeks, with a human on the other side of the table.
6.12 — What Are You Building On?
“Everyone then who hears these words of mine and does them will be like a wise man who built his house on the rock. And the rain fell, and the floods came, and the winds blew and beat on that house, but it did not fall, because it had been founded on the rock.” — Matthew 7:24–25 (ESV)
Read the rest of it — Matthew 7:26–27 — and notice the detail almost everyone misses: both houses get the same storm. The rain falls on both. The floods come to both. The winds blow and beat on both. Jesus does not describe a wise builder who enjoyed better weather. The storm is the constant; the foundation is the only variable in the story.
Your capstone will get the storm. Not might — will. It arrives around Week 11 or 12 with a name: the integration that does not integrate, the API that changed under you, the week you are sick, two exams on the same Thursday, the roommate who was going to test it and didn’t. Nothing in this book will spare you that. What this week decides is whether the storm hits a house with a foundation or a house with a very impressive front door.
So what is a foundation, in software? Not the framework — frameworks are siding. Not the language. The foundation is the set of decisions everything else rests on and that you cannot cheaply change later: the data model, the boundaries between components, the contracts between them. Sand is the material that looks exactly like ground until you put weight on it. In a capstone, sand is a schema you never wrote down, a boundary you never drew, a contract that lives only in your head. It holds fine in Week 7, when the load is nothing. It gives way in Week 12, when the load is everything.
Then there is the sharper edge of the parable, and it is aimed at you specifically this week. Both men heard the same words. Verse 24 says the wise man is the one who “hears these words of mine and does them.” Hearing was never the dividing line; doing was. You can read this chapter, agree with it, find §6.9 genuinely persuasive, close the browser, and go start coding. You will have heard. You will have built on sand anyway — with full information, which is the more foolish of the two available ways to do it.
Let me be careful here, because it would be cheap to shrink this parable into a project-management tip. Jesus is not teaching software design. The house is a life, the storm is judgment, and the rock is his word — obedience to a person, not adherence to a process. Paul takes up the same builder’s image in 1 Corinthians 3:10–11 and adds the thing that keeps it from becoming a slogan: no one can lay a foundation other than the one already laid, which is Jesus Christ. Your technical specification is not your foundation in that sense, and nothing you write this week has anything to do with your standing before God; that was settled outside of you, by Christ, and it is not on the rubric. But the picture holds for the smaller work too, and here is why: God is not chaotic, and the world he made rewards building on what is actually true. That is not a coincidence a Christian has to explain away; it is exactly what we would expect of an ordered creation. This course calls that ordinary work vocation — the daily, unglamorous service you render a neighbor through a job. Your neighbors here are concrete: the roommates who will use this, the reviewer who reads it in Week 8, the engineer who inherits it after you graduate. Careful design is not how you get saved. It is how you love the person who has to live in the house.
One caution, so nobody overcorrects. There is a second failure the parable does not describe, and I have watched students find it anyway: the builder who never builds — who spends Week 6, and 7, and 9 perfecting a foundation for a house he is too anxious to start. The parable commends a house that got built and stood. Not a survey; not plans in a drawer. Milestone 6 is a specification because Week 9 is a walking skeleton: the design exists to be built on, and a design that never bears weight was never a foundation at all. Week 2’s verse cuts both ways: “Unless the LORD builds the house, those who build it labor in vain” — Psalm 127:1 (ESV). Note what it does not say. It does not say they stop building. Diligence and dependence have never been opposites.
So: what are you building on? Answer it in writing this week, where a stranger can check your work. Then go build the house.
6.13 — Where Your Hours Went This Week
Roughly fifteen hours. A realistic shape:
| Work | Hours |
|---|---|
| Re-reading your SRS and NFRs; listing what must be decided | 1.0 |
| Context and container diagrams (two passes — the first is always wrong) | 2.0 |
| Component responsibility table + dependency and cycle check | 1.5 |
| Interface contracts for every Must-priority path | 2.5 |
| Data model: entities, keys, invariants, migration plan | 2.5 |
| Three sequence flows, including the failure branches | 2.0 |
| Error-handling policy, edge-case register, AI/third-party spec | 2.0 |
| Assembly, traceability pass, checker, reps, weekly quiz | 1.5 |
| Total | 15.0 |
Log it in docs/hours-log.csv the way you have since Week 1 — honestly, including the hour you lost redrawing a diagram because you started at the wrong zoom level. Next week you become the project manager and estimate the entire build, and the only calibration data you will have is the six weeks behind you. A padded log calibrates nothing. If this week ran to twenty hours, write twenty; that number is worth more than the one that makes you look on pace.
6.14 — Common Pitfalls
Pitfall: The diagram with unlabeled arrows.
Example: Four boxes — Frontend, Backend, Database, API — joined by bare lines.
Fix: Label every arrow with what flows, in which direction, over what protocol; label every box with a responsibility and a technology. If you cannot label an arrow, you have not designed that connection — you have hoped for it.
Pitfall: Specifying only the happy path. Example: A sequence flow in which the third-party lookup returns 200 and nothing else ever happens. Fix: For every external call, write the timeout, the retry policy, the fallback, and what the user sees. The failure branch is where design decisions actually get made (§6.6).
Pitfall: Architecture built for an audience instead of a user. Example: A queue, a cache, and three services in a system with six users and one developer, because the diagram looked more serious that way. Fix: Every container is something you must build, run, debug, document, deploy, and demo. Justify each against a requirement or delete it. Your novelty budget from Milestone 5 did not reset.
Pitfall: A specification that restates the requirement instead of deciding anything. Example: “The system shall notify users about expiring food. The notification module will handle notifications.” Fix: Count the decisions per section. If a builder must still ask you a question to start, that section is Version A from §6.9. Rewrite until the questions are answered — or explicitly marked open, with an owner and a date.
Pitfall: A data model with no keys, no types, and no invariants.
Example: items(id, user, data1, exp, stuff), dates stored as free text, a JSON blob written by two modules.
Fix: Every entity gets a primary key; every column a type and a nullability rule with a stated meaning; every table its invariants; every schema change a migration. Do it before Week 9 puts data you care about into it.
Pitfall: Pasting an assistant’s draft into the specification without verification.
Example: An endpoint that uses a header, a config key, or a limit the vendor does not actually have.
Fix: Check every specific fact — version, path, header, limit, price — against the vendor’s own documentation, and record the date. Then log the AI use in docs/ai-usage.md. You sign this document.
Pitfall: Writing the specification after the code, to satisfy the rubric.
Example: Week 14, reverse-engineering docs/architecture.md out of whatever got built.
Fix: There is no rescue for this one, only prevention. The specification is due now because its entire value is that it comes first. A retro-spec documents your accidents and calls them decisions. Graders can tell, and so can you.
6.15 — Reps
The reps are in the exercises, and this week they are not warmups — each produces a section of the document you are turning in. Preview:
- Rep 2 — draw the context diagram, then the container diagram, with every arrow labeled.
- Rep 4 — build the component responsibility table and audit it for cycles and shared ownership.
- Rep 5 — write one complete interface contract for the interface you understand least.
- Rep 8 — sequence the money path, the risky path, and the failure branch, and record what the failure branch changed.
- Rep 11 — rewrite a supplied vague specification into a buildable one, then count the decisions you added.
Then take the on-page Check Your Reps quiz — the ungraded rehearsal for Week 6 Quiz in Canvas. Together the weekly quizzes and the Week-8 checkpoint are 15% of your grade — but their real job is telling you every Friday whether you are still on pace. Appendix C has that arithmetic.
6.16 — This Week’s Milestone
Milestone 6 — Milestone 6: Technical Specification (System Design Document). It lands in your repository at docs/architecture.md, with diagram sources and rendered images under docs/diagrams/, and it feeds the design-and-architecture line of the Week-16 rubric. Start from code/tech-spec-template.md and check your draft with code/spec-check.py before you commit it.
Two weeks from now that document goes into a design review and gets baselined. Everything you leave vague this week, somebody else will find in front of you.
6.17 — Coach’s Final Word
Six weeks in. You have an idea you scoped, requirements a stranger could verify, non-functional requirements with numbers in them, and a stack you chose on evidence and wrote down why. This week you turned all of it into something that can be built: boundaries, contracts, a data model, and an honest account of what happens when it breaks.
Take one idea out of this week ahead of any template. A specification is a set of decisions, and decisions are cheap now and expensive later. That is the entire economics of design. The fifteen hours you spent were not spent describing your project; they were spent buying back the hours in Week 12 when you would otherwise be standing at a keyboard at 11pm deciding what “about to expire” means.
Both houses get the same storm. Go make sure yours is standing on something.
See you on Monday.
Up next: the exercises builds the specification section by section · Milestone 6 is Milestone 6 · then Chapter 7, where you put on the project-manager hat and find out what this design actually costs. Reference: Appendix A (workbench and diagramming tools), Appendix B (the document kit — templates and worked good/bad examples), Appendix C (the grading contract), Appendix E (glossary). Previous: Chapter 5.
Week 6 Knowledge Check
POST /items — adds an item. Returns the item. Errors return an error. pantry depending on expiry, and expiry depending on pantry. Per §6.3, what does that cycle mean?pantry and expiry both compute days-to-expiry they will disagree, not today but on the day one is fixed for a daylight-saving bug and the other is not. And if two components write the same table, one of them is wrong and you do not yet know which. items(id, user, data1, exp, stuff)
exp text: "8/14", "next tuesday", "2026-08-14"
user the display name, duplicated in three tables
data1 sometimes quantity, sometimes unit
stuff undocumented JSON, written by two modules 400 is malformed or invalid, not "you are not allowed"; 401 is "we don't know who you are" and 403 is "we do, and you can't"; 409 is a conflict with current state, not a plain validation failure; and 500 means your code broke — log it, and never leak the trace. Decide one error envelope for the whole system, here, once: a codebase with three error shapes has a client with three error handlers and a bug in two of them. docs/architecture.md. Where do you actually stand?FR-07 — Expiry notifications
The system shall notify users about food that is about to expire.
Notifications should be timely and should not be annoying. A background
job will handle this. Errors will be handled appropriately. spec-check.py flags, so it would not exit 0. And proportionate means the data model and the risky interface get pages while the genuinely obvious parts get a sentence — not that everything gets a sentence.