Path to AI Architect · Chapter 5 of 25

Architecture-First Thinking

Decompose systems, choose boundaries, design interfaces, and document decisions before AI agents build incompatible parts.

30 min read Slides for this chapter

Opening scenario: three agents, three apps in one#

After chapter 4, Maya's Learning Coach saved ticks on the server and she had a habit of tracing every change. Then her backlog got busy. In one week she opened three separate agent sessions:

  • one to let students upload their lecture notes;
  • one to add accounts, so a plan followed a student between phone and laptop;
  • one for a paid “Pro” tier with unlimited plan rebuilds.

Each session finished with a cheerful summary. Each feature worked when she clicked through it. Then her first ten testers arrived, and the bug reports didn't look like bugs in any one feature:

  • A student rebalanced her plan on her phone, opened her laptop, and saw the old plan. The accounts session stored plans in PostgreSQL. The plan screen, first built in chapter 2, still kept its own copy in the browser's local storage and preferred it.
  • The rebalance screen called the AI model straight from the browser. The model key sat in a VITE_ variable, which, as chapter 4 explained, ships to every visitor.
  • The rule for how many rebuilds a free student gets lived inside a React component. The phone layout had its own copy, with a different number.
  • The upgrade button sent students to the payment page, and on return the browser called POST /api/users/me/upgrade. The server set the account to Pro without checking that anyone had paid. Anyone who found that address could upgrade for free.

None of the agents had done anything absurd. Each one did something sensible for its own feature. But the code never said how the parts should fit together, so each agent guessed, and the three guesses disagreed. Nobody had decided where plans live, who may call the model, or which part of the system owns the question “has this student paid?”

That missing decision is what this chapter is about.

Why it matters#

When you write code by hand, the slowness of typing gives you time to notice that a new feature clashes with an old one. Agents remove that delay. They produce whole features in minutes, and when you run several of them, they make overlapping choices in parallel without seeing one another's work.

Architecture is how you prevent that. Ralph Johnson's short definition, as Martin Fowler retells it, is that architecture is “about the important stuff. Whatever that is” (Fowler, 2019). For an architect directing agents, the important stuff is the set of decisions that every agent must share: what the parts of the system are, what each one is responsible for, how they talk to each other, and which rules no part may break. If those decisions are written down before the agents start, they build parts that fit. If not, you get Maya's week.

This chapter teaches you to make those decisions on purpose, at the size your product needs right now, and to record them so the next agent session (or the next person) inherits them instead of guessing.

Learning objectives#

After this chapter you will be able to:

  1. Explain what an architecture specifies: components, responsibilities, interfaces, constraints, flows, and trust boundaries.
  2. Decompose a product into parts with clear responsibilities, and choose between a modular monolith and separate services.
  3. Design an interface contract that covers inputs, outputs, errors, retries, and future changes.
  4. Draw context and container diagrams and write an architecture decision record (ADR).
  5. Run a lightweight design review that compares alternatives and records risks before major implementation.
  6. Give an agent architectural boundaries and check that its work respects them.

5.1 What software architecture actually specifies#

Components, responsibilities, interfaces, constraints#

An architecture answers four questions:

  • Components. What are the parts? For the Learning Coach: a web app, an API server, a database, file storage for notes, and outside services such as the AI model and payments.
  • Responsibilities. What is each part in charge of, and what is it not in charge of? “The plans module owns plans and is the only code that changes them” is a responsibility. So is “the web app never decides whether a student has paid.”
  • Interfaces. How do the parts talk? An interface is the agreed way to ask a part for something: a function signature, an HTTP endpoint, a message format.
  • Constraints. Which rules apply everywhere? For example: secrets stay on the server; every database query is scoped to the signed-in student; personal data stays in one region.

Maya's bugs map neatly onto these. Two copies of the plan meant nobody owned plans. The model call from the browser broke a constraint nobody had written down. The free-tier rule had two owners. The upgrade endpoint trusted the browser with a decision only the payment provider can confirm.

Data flow, control flow, and trust boundaries#

Three kinds of lines matter on any architecture sketch:

  • Data flow shows where information goes: the student's notes travel from the browser to file storage, then to the model when she asks a question.
  • Control flow shows who starts what: the browser asks the API to rebuild a plan, the API asks the model for a draft, the API saves it.
  • Trust boundaries, which you met in chapter 4, mark where data crosses from a place you don't control into one you do. The browser is outside: anyone can edit the code running there or send requests by hand. The server is inside. Every rule that matters (who may see what, who has paid, how many rebuilds are left) has to be checked on the inside of a boundary.

A useful habit: whenever an arrow crosses a trust boundary, write next to it what the receiving side checks.

Quality attributes and tradeoffs#

Chapter 3 introduced nonfunctional requirements such as speed and availability. Architects call these quality attributes. The ISO/IEC 25010 standard groups product quality into nine characteristics, including performance efficiency, compatibility, reliability, security, and maintainability (arc42, ISO/IEC 25010). You don't need to memorize the list. Use it as a checklist of qualities you might be forgetting.

Quality attributes pull against each other, and architecture is mostly the work of choosing which one wins where. Splitting the system into separately deployed services can help teams work independently, but it adds network calls that can fail and slow things down. Calling the AI model for every rebalance gives better plans, but costs money and time. Writing down which quality wins in which place is a decision, and decisions get recorded (section 5.4).

Architecture at prototype, MVP, and production stages#

How much architecture you need depends on what you're risking. A rough guide:

Stage What matters most Architecture you need
Prototype (chapter 2) Learning whether the idea works Almost none. Keep it disposable and say so.
MVP (now) Real users and their data; changing fast Clear modules, one owner per kind of data, secrets on the server, a container diagram, a few ADRs.
Production at scale (Part III) Reliability, cost, security under load Explicit failure handling, monitoring, capacity plans, reviewed security boundaries.

The mistake runs both ways. Maya kept prototype architecture after real users arrived. The opposite mistake, building production architecture for ten testers, wastes the time you need to learn what users want.

5.2 System decomposition#

Decomposition means dividing a system into parts so that each part can be understood, changed, and tested mostly on its own.

The usual parts#

Most web products, including the Learning Coach, split along similar lines:

Part Learning Coach examples Typical owner of
User interface Plan view, rebalance screen, upload form What the student sees and clicks
Application logic Rebalance rules, free-tier limits, rule checks on plans The rules of the product
Data Plans, tasks, students, uploaded notes Storing and retrieving facts
Background jobs Extracting text from uploaded PDFs, weekly reminder emails Slow or scheduled work, done outside a request
Integrations AI model, payment provider, email service Talking to outside systems

A background job is work the server does after answering the request, so the student doesn't wait. Extracting text from a 40-page PDF might take far longer than anyone should stare at a spinner, so the upload request saves the file, records the note as processing, and answers at once. A job does the extraction and marks the note ready.

Inside the application logic, divide by what the product does, not by technology. For the Learning Coach that gives modules such as plans, notes, accounts, and billing, each with its own rules and data, plus an ai module that wraps the model provider.

Modular monolith or microservices?#

A monolith is one application deployed as one unit. A modular monolith is a monolith whose inside is divided into modules with enforced boundaries. Microservices split the system into separately deployed services that talk over the network.

Microservices solve real problems, mostly for large organizations: teams can release independently and scale parts separately. They also bring what Fowler calls the microservice premium, the extra cost of automated deployment, monitoring, handling failures between services, and data that is only eventually consistent (Fowler, 2015a). His advice is to start with a monolith, partly because good service boundaries are hard to see at the start, and moving a responsibility between services is much harder than moving it between modules (Fowler, 2015b).

Team shape points the same way. Conway's law says a system's structure tends to mirror the communication structure of the organization that builds it (Fowler, 2022). Two people who talk every day don't need service boundaries to coordinate. They need module boundaries that agents respect.

So the default for this book is a modular monolith: one React app, one FastAPI server divided into modules, one PostgreSQL database. A separate service has to earn its place with a concrete reason, such as a part that needs very different hardware, a different team, or a different release schedule.

Cohesion, coupling, and dependency direction#

Two words describe good boundaries:

  • Cohesion is how closely the things inside a module belong together. The rebalance rules and the plan data have high cohesion: they change for the same reasons.
  • Coupling is how much one module depends on another's insides. If the billing module reads the plans table directly, any change to that table can break billing. That's tight coupling. If billing asks the plans module “how many rebuilds this month?” through one function, the coupling is loose.

Aim for high cohesion inside modules and loose coupling between them.

The third idea is dependency direction: which module knows about which. The core rules of your product should not depend on a particular outside vendor. Alistair Cockburn's ports-and-adapters pattern (also called hexagonal architecture) puts it this way: the application talks to the outside world through ports, purposeful interfaces it defines, and adapters connect each port to a specific technology. That lets the application run and be tested without its real databases or devices (Cockburn, 2005).

For the Learning Coach, “draft a study plan” is a port. A hosted model is one adapter; a fake that returns a fixed plan is another, used in tests. The plans module depends on the port, never on the vendor. Switching model providers then touches one adapter, not every screen.

Avoiding premature abstraction#

Ports are useful, and they also tempt you to add layers for problems you don't have. Fowler's essay on yagni (“you aren't gonna need it”) argues that building for a presumed future need costs you twice: once to build it, and again every time you have to work around its extra complexity. He doesn't rule out abstractions, but he treats one that makes today's code harder to follow as guilty until proven innocent (Fowler, 2015c).

Agents are prone to this. Ask for “a clean architecture” and you may get factories, base classes, and plugin systems for a product with one user. A workable rule: add a boundary when you can name a real reason today. That could be a secret to protect, a vendor that may change, a rule with two copies, or something you can't test without it. The AI provider qualifies on three counts. A plugin system for “future learning content types” doesn't.

5.3 Designing interfaces#

API contracts and data types#

An interface is only useful if both sides agree on it exactly. That agreement is a contract: the operation's name, what goes in, what comes out, and what can go wrong. Write it down in a form both people and agents can read.

FastAPI helps here. When you declare request and response models with Pydantic, it validates incoming data and publishes an OpenAPI description of your API, with interactive documentation, automatically (FastAPI, Features). OpenAPI is a standard, machine-readable way to describe HTTP APIs, so agents working on the frontend can read the same contract the backend enforces.

Here is the contract for creating a plan, written once as a table for humans:

Operation POST /api/plans: draft and save a study plan for the signed-in student
Input exam_date (date, in the future), weekly_hours (whole number, 1–40), topics (1–30 short strings)
Success 201 Created with the plan, including source: "model" or "template"
Errors 401 not signed in; 422 invalid input, naming the field; 429 too many plan requests
Timing If the model hasn't answered in 20 seconds, save a template plan and return it with source: "template" (FR-9 from chapter 3)
Retries Accepts an Idempotency-Key header; repeating a key returns the first result
Owner plans module

And as code the backend enforces:

Python
# backend/coach/plans/schemas.py
from datetime import date
from typing import Literal
from pydantic import BaseModel, Field

class PlanRequest(BaseModel):
    exam_date: date
    weekly_hours: int = Field(ge=1, le=40)
    topics: list[str] = Field(min_length=1, max_length=30)

class WeekOut(BaseModel):
    starts_on: date
    tasks: list[str]

class PlanDraft(BaseModel):
    """A proposed plan, before rule checks and saving."""
    weeks: list[WeekOut]

class PlanOut(BaseModel):
    id: str
    exam_date: date
    source: Literal["model", "template"]
    weeks: list[WeekOut]

The limits (40 hours, 30 topics, 20 seconds) are illustrative assumptions. Yours come from your requirements.

Inputs, outputs, errors, retries#

Most contract bugs live in the error cases, so give errors the same care as success. Two habits help.

One error shape. Every error from your API should look the same, so the frontend can handle all of them with one piece of code:

JSON
{ "error": { "code": "plan_input_invalid", "message": "Exam date must be in the future.", "field": "exam_date" } }

The code is for programs and stays stable; the message is for people and may change.

Decide what's safe to retry. Networks fail partway. A request can time out after the server already did the work, so the client can't tell whether it succeeded. HTTP defines idempotent methods as those where sending the same request several times has the same intended effect as sending it once; PUT, DELETE, and the read-only methods qualify, and clients shouldn't automatically retry the others unless they know the operation is safe to repeat (Fielding et al., 2022). POST, which creates things, usually isn't.

The usual fix is an idempotency key: the client generates a unique value for each logical action and sends it with the request; the server remembers the result for that key and, on a retry, returns the saved result instead of acting again. Payment providers such as Stripe work this way so that a retried request doesn't create a second charge (Stripe, Idempotent requests).

Retries also need restraint. Amazon's engineering guidance describes retries as “selfish,” because each one asks a possibly overloaded server for more work. It recommends timeouts on every remote call, waits that grow between attempts, a little randomness so clients don't retry in sync, and retrying at one layer only, since retries stacked at several layers multiply (Brooker, Amazon Builders' Library). For the Learning Coach, FR-9 gives plan creation 20 seconds in total, which leaves no room for a second attempt at the model. So nothing retries model calls: the template plan takes the place of a retry.

Ownership of shared state#

State is any data that changes over time: a plan, a tick, a subscription. The rule that would have prevented Maya's first bug is simple: every kind of state has exactly one owner, and everyone else asks the owner.

State Owner Others may
Plans and tasks plans module, plans and tasks tables Read through plans functions or GET /api/plans/...
Uploaded notes notes module, file storage plus notes table Ask for text or status
Subscription status billing module, updated only from the payment provider's confirmation Ask “is this student Pro?”
Screen state (open panels, unsaved form input) The web app Nothing; it never leaves the browser

The browser may keep a copy of a plan (a cache) to show it quickly. That copy exists for speed only, and whenever it disagrees with the server, the server's version is the true one. Write that down, because an agent asked to “make it work offline” will otherwise happily make the browser copy authoritative again.

Schema evolution and backward compatibility#

Contracts change. A change is backward compatible if existing clients keep working without changes. Adding an optional field to a response usually is, as long as clients follow Fowler's tolerant reader advice: take the fields you need and ignore the rest (Fowler, 2011). Removing a field, renaming it, or changing its type is not.

For breaking changes, use parallel change, also called expand and contract, which Danilo Sato describes in three phases (Sato, 2014):

  1. Expand: support both the old and the new form. The API returns hours and weekly_hours.
  2. Migrate: move every client to the new form, one at a time.
  3. Contract: once nothing reads the old form, remove it.

The same pattern applies to database columns. The danger is stopping after step 1, which leaves you with two fields that can disagree. Put the contract step in the backlog when you start.

5.4 Architecture notation and communication#

Context, container, and component diagrams#

Simon Brown's C4 model offers diagrams at increasing zoom levels: system context, containers, components, and code (Brown, C4 model). The first two are worth drawing for every project:

  • A system context diagram shows your system as one box, the people who use it, and the outside systems it talks to. It's meant to be understandable by non-technical people (Brown, System context diagram).
  • A container diagram zooms into that box. In C4, a container is something that runs or stores data, such as a web app, an API server, or a database. The diagram shows how responsibilities are spread across them and how they communicate (Brown, Container diagram). (This is unrelated to Docker containers.)

A component diagram zooms into one container to show its modules. For a small app, a table of modules like the one in section 5.2 often does the job.

Here is the Learning Coach container diagram, with the modules of the API server shown inside it:

flowchart LR
    student(["Student<br/>browser or phone"])
    subgraph coach["Learning Coach"]
        web["Web app<br/>React + TypeScript<br/>screens only, no secrets"]
        subgraph api["API server (FastAPI)"]
            plans["plans"]
            notes["notes"]
            accounts["accounts"]
            billing["billing"]
            ai["ai adapter"]
        end
        job["Background job<br/>extracts text from notes"]
        db[("PostgreSQL<br/>plans, tasks, students,<br/>notes, subscriptions")]
        files[("File storage<br/>uploaded notes")]
    end
    model["AI model provider"]
    pay["Payment provider"]

    student -->|"HTTPS"| web
    student -->|"pays on the provider's checkout page"| pay
    web -->|"JSON over HTTPS, session cookie<br/>server checks session and ownership"| api
    plans --> ai
    notes --> ai
    ai -->|"API key held on server"| model
    billing -->|"starts checkout"| pay
    pay -->|"signed payment confirmation<br/>server verifies signature"| billing
    api --> db
    notes --> files
    job --> files
    job --> db

Notice the labels on the arrows that cross trust boundaries: they say what the receiving side checks. Notice also what's missing. There is no arrow from the web app to the model provider. And although the student pays on the provider's own checkout page, nothing the browser sends can make a student Pro: only the signed confirmation arriving at billing can. The diagram makes both rules visible.

Sequence and state diagrams#

Container diagrams show structure. Two other diagrams show behavior, and you've already met both. A sequence diagram (chapter 4) shows one interaction step by step across parts, which is the best way to specify a tricky flow like checkout. A state diagram (chapter 3) shows the states one thing can be in and what moves it between them. Notes in the Learning Coach have a small life of their own, uploaded, processing, ready, or failed, and drawing it settles questions like “can a student ask about a note that is still processing?” before an agent picks an answer.

Architecture decision records#

Diagrams show what the system is. They don't show why. Michael Nygard proposed architecture decision records to keep that reasoning: short text files, one per significant decision, stored in the repository and numbered in sequence (Nygard, 2011). His format has five parts:

  • Title, a short noun phrase.
  • Context, the forces at play, described neutrally.
  • Decision, written in active voice (“We will...”).
  • Status, such as proposed, accepted, or superseded.
  • Consequences, including the negative ones.

When a decision is reversed, the old record stays, marked superseded, because it's still useful to know what was decided and why.

Nygard's argument for ADRs is that a newcomer who doesn't know why a decision was made can only accept it blindly or change it blindly. Every new agent session is that newcomer. An agent that reads docs/decisions/ before starting knows that plans live on the server, that only the ai module calls the model, and why. The ADR community site keeps templates and further reading if you want alternatives to Nygard's format (ADR GitHub organization).

What an AI agent must know about a diagram#

Agents can read Mermaid and plain-text diagrams well, but they read them literally. When you hand an agent a diagram, also state:

  • What the arrows mean. “Calls,” “sends data to,” and “depends on” are different things. Say which.
  • What's forbidden. A missing arrow isn't a rule unless you say so: “No code in web/ may call the model provider or read secrets.”
  • What's outside. Boxes outside the system boundary are things you don't own and can't change.
  • Where to find the decisions. Point to the ADRs that explain the shape.

Put this in the project instructions file for agents from chapter 4, the one many tools read at the start of every session, so it applies every time and not only when you remember to paste it.

5.5 Design review#

A design review is a deliberate look at a proposed architecture before you build it. The heavyweight version is the Software Engineering Institute's Architecture Tradeoff Analysis Method, which evaluates an architecture against quality-attribute goals to find risks and tradeoffs; it typically takes three to four days with a trained team and stakeholders (Software Engineering Institute, 2018). You don't need that for an MVP. You need an hour, the diagram, and four questions.

Identify failure points and bottlenecks#

Walk every arrow on the container diagram and ask: what happens if the other end is slow, down, or wrong? For the Learning Coach:

  • The model provider is slow: plan creation falls back to a template after 20 seconds (already specified).
  • The model returns nonsense: the rule checks from chapter 3 reject it and the template is used.
  • The PDF extraction job crashes: the note stays processing forever unless something marks it failed. That's a gap, so add a time limit.
  • The payment confirmation never arrives: the student paid but isn't Pro. That's another gap. Add a way to look up the payment status on demand, and a support path.

A bottleneck is the part that limits everything else. At MVP scale it's usually the slowest outside service, here the model provider, not your own code.

Compare alternative solutions#

Never review a design alone. Put at least two side by side, even if one is obviously weaker, because the comparison is what makes the tradeoffs visible. An agent is good at producing a credible alternative quickly; your job is to check its claims.

Review complexity, deployment, and costs#

For each alternative, ask:

  • How many things must be deployed, configured, and watched?
  • What does a new contributor, or a new agent session, have to understand before changing anything?
  • What does it cost each month at your expected usage, and what drives that cost?

Cost estimates should name their assumptions. “About 200 students, each generating three plans a month” is an assumption; write it down so you can check it once real usage arrives.

Approve the design before major implementation#

Record the risks you found in a risk register: a table of what could go wrong, how likely and how bad it would be, what you'll do about it, and what would tell you it's happening.

ID Risk Likelihood Impact Response Watch for
R1 Model provider outage or slowness Medium Medium Template fallback after 20 s Share of plans with source: "template"
R2 Student marked Pro without paying, or paid without becoming Pro Low High Pro set only from verified payment confirmation; status lookup Mismatches between billing and payment records
R3 Agent bypasses the ai adapter Medium High Boundary check in the test suite (see failure clinic) Check failures; model domains in the Network panel
R4 Notes stuck in processing Medium Low Mark failed after 10 minutes; show retry Count of notes processing over 10 minutes

The likelihoods and limits are illustrative assumptions. Then approve the design, as an ADR with status accepted, before agents start building. Payments and personal data deserve a review by someone with security expertise before launch; a design review doesn't replace that.

Architect's decision: how should the Learning Coach be structured?#

Context About 200 early users expected in the next three months (an assumption from the beta waitlist). Two people: Maya and a part-time collaborator, both directing agents. Features now include plans, notes uploads, accounts, and payments. The current code has duplicate state and rules in the UI.
Constraints Little time for operations work. Payment and model keys must stay on the server. Agents work on different features in parallel and must not collide.
Option A Keep the current shape: each feature lives wherever its agent put it, and bugs are fixed as they appear. Cheapest today; the opening scenario shows its cost.
Option B Modular monolith: one React app, one FastAPI server with plans, notes, accounts, billing, and ai modules, one PostgreSQL database, one background job. Each module owns its data; only ai calls the model; only billing changes subscription status.
Option C Separate services for plans, notes, accounts, and billing, each deployed independently. Clean separation, but four deployments, network failures between services, and data spread across stores for a two-person team.
Decision Option B, recorded as ADR-001.
Tradeoffs Module boundaries inside one codebase are easy to break, because nothing physical stops one module from importing another's internals. That needs an automated check. Everything scales together, which is fine at this size.
Validation The boundary checks pass in the test suite. Every feature in the next month lands without creating a second owner for any state. A fresh agent session, given only the ADRs and the diagram, places a new feature in the right module.
Revisit trigger One part needs very different hardware or scaling (for example, heavy document processing), a separate team takes over one area, or release schedules start blocking each other.

AI collaboration recipe#

Agents are strong partners for architecture work: they know common patterns, can draft alternatives quickly, and can survey a codebase faster than you. They are weak at knowing your constraints, and they tend to add structure for its own sake. Give them the constraints and ask for evidence.

1. Map what exists.

Without changing any files, describe the current architecture of this repository. List each part (frontend, backend modules, database tables, outside services), what it is responsible for, and every place that: calls an outside service, reads a secret, stores plan data, or decides whether a student has paid. Give file paths and line numbers for each claim.

2. Propose competing designs.

Here are our constraints: [team size, expected users, budget, the PRD's nonfunctional requirements]. Propose two different architectures for the Learning Coach. For each: a container diagram in Mermaid, which part owns each kind of data, how it handles a slow model provider, deployment steps, monthly cost drivers, and the three biggest risks. Don't recommend one; I will decide.

3. Extract a boundary without changing behavior.

Create a PlanGenerator interface in backend/coach/plans/ports.py, implement it in backend/coach/ai/, and move all model calls behind it. Behavior must not change: same prompts, same timeouts, same fallback. Then remove every other direct model call in the codebase, including in web/. Show me the list of call sites you changed and a search proving none remain.

4. Review a design.

Here is our container diagram and ADR-001. For every arrow, say what happens if the receiving side is slow, down, or returns bad data, and whether our design handles it. List gaps as risks with likelihood and impact.

Boundaries to state every time:

  • “Read docs/decisions/ and the architecture diagram before changing anything. If a change would contradict an ADR, stop and tell me.”
  • “Don't add new layers, frameworks, or patterns unless you can name the current problem they solve.”
  • “Only the ai module calls the model provider. Only billing changes subscription status. Nothing in web/ holds secrets.”
  • “Work on a new branch. Don't merge.”

Worked example: putting the AI provider behind an adapter#

Maya's first architecture increment fixes the most dangerous problem from the opening: model calls scattered across the codebase, one of them from the browser with an exposed key.

1. Write ADR-001 first. Before any code, she recorded the decision:

Markdown
# ADR-001: Modular monolith with module-owned data

Status: Accepted (October 2026)

## Context
Two people directing several agents; about 200 early users expected
(assumption). Features: plans, notes, accounts, payments. Current code keeps
plans in two places, calls the model from the browser, and checks the free
tier in the UI.

## Decision
We will build one FastAPI server divided into modules (plans, notes,
accounts, billing, ai) and one React app. Each module owns its tables and is
the only code that writes them. Only the ai module calls the model provider.
Only billing changes subscription status, and only after verifying the
payment provider's confirmation. The web app holds no secrets and no
business rules.

## Consequences
Boundaries are enforced by tests, not by deployment, so the tests must run
on every change. Moving to separate services later is possible but not free.
All modules scale together.

2. Define the port. The plans module needs a draft plan. It shouldn't know which vendor produces it, so the port lives in plans and says only what plans needs:

Python
# backend/coach/plans/ports.py
from typing import Protocol
from coach.plans.schemas import PlanRequest, PlanDraft

class PlanGenerationFailed(Exception):
    """The model was slow, unavailable, or returned an unusable plan."""

class PlanGenerator(Protocol):
    def draft_plan(self, request: PlanRequest) -> PlanDraft: ...

3. Write the adapters. They live in the ai module. One talks to the real provider, with the 20-second timeout from section 5.3; one is a fake for tests:

Python
# backend/coach/ai/hosted_model.py (illustrative; client details depend on your provider)
from coach.plans.ports import PlanGenerationFailed
from coach.plans.schemas import PlanRequest, PlanDraft

class HostedModelPlanGenerator:
    def __init__(self, client, timeout_s: float = 20.0):
        self.client, self.timeout_s = client, timeout_s

    def draft_plan(self, request: PlanRequest) -> PlanDraft:
        try:
            raw = self.client.complete(build_plan_prompt(request), timeout=self.timeout_s)
            return PlanDraft.model_validate_json(raw)
        except (TimeoutError, ValueError) as error:
            raise PlanGenerationFailed from error

# backend/coach/ai/fake.py
class FakePlanGenerator:
    def draft_plan(self, request: PlanRequest) -> PlanDraft:
        return template_plan(request)

Notice the dependency direction from section 5.2: ai knows about plans, and plans knows nothing about ai or the vendor. One file connects them. backend/coach/main.py, which starts the app, creates the hosted adapter and hands it to the plans service; tests hand it the fake instead. The plans module catches PlanGenerationFailed and saves a template plan, which is FR-9 in one place instead of three.

4. Move rebalancing to the server. The browser now calls POST /api/plans/{id}/rebalance, and the free-tier limit moves from two React components into the plans module, which asks billing whether the student is Pro. The VITE_ model key is deleted, and because it had shipped to browsers, Maya also revoked it with the provider and issued a new one for the server only.

5. Verify behavior didn't change. Before the refactor, she saved the plans the old code produced for five sample inputs, using the fake generator so results were repeatable. After it, the same inputs gave the same plans. She also checked the Network panel: ticking, creating, and rebalancing a plan sent requests only to her own API.

Verification: is the architecture real?#

An architecture exists only if the code follows it. Check:

Check How
Single owner For each kind of state, search for code that writes it. Only the owning module should appear.
Boundary rules Automated tests fail if forbidden imports or calls appear (see the failure clinic).
Secrets Search web/ and the built frontend files for key names and provider domains. Nothing should match.
Contracts The OpenAPI document matches the contract table; the frontend handles every listed error code.
Decisions Every significant choice in the last month has an ADR, and none contradicts the code.
Fresh-agent test A new agent session, given only the ADRs and diagram, explains where a new feature belongs and why.

The last check is the architecture version of chapter 4's explain-back test. If a fresh agent places a feature in the wrong module, your documents are unclear, and that's cheaper to discover now than in a pull request.

Failure clinic: the adapter nobody used#

Symptom. The agent reported: “Created the PlanGenerator interface and moved model calls behind it. All tests pass.” A week later, the model provider's usage dashboard showed requests coming from students' browsers.

Reproduce. Open the rebalance screen with the Network panel open. Click Rebalance. A request goes directly to the model provider's domain, not to /api.

Diagnose. The agent had done exactly the visible part of the task. It built the port, the adapter, and the fake, and switched the backend's plan creation to use them. It missed web/src/plan/Rebalance.tsx, which still called the provider directly, and the old VITE_ key still sat in the deployed environment's settings, so the call kept working. The tests passed because they tested the new adapter; nothing tested that the old path was gone. “Moved model calls behind it” was true for the calls the agent had looked at.

Fix. Finish the move (step 4 of the worked example), revoke the exposed key, and then make the boundary something a test checks rather than a sentence in a document:

Python
# backend/tests/test_boundaries.py
import pathlib, re

ROOT = pathlib.Path(__file__).resolve().parents[2]
PROVIDER = re.compile(r"MODEL_API_KEY|api\.example-model\.com|coach\.ai\.hosted_model")
ALLOWED = ("backend/coach/ai/", "backend/coach/main.py")

def test_only_ai_module_talks_to_the_model():
    offenders = []
    for folder in ("web/src", "backend/coach"):
        for path in (ROOT / folder).rglob("*"):
            name = path.relative_to(ROOT).as_posix()
            if path.suffix not in {".py", ".ts", ".tsx"} or name.startswith(ALLOWED):
                continue
            if PROVIDER.search(path.read_text()):
                offenders.append(name)
    assert offenders == [], f"Direct model access outside coach/ai: {offenders}"

Replace the pattern with your provider's actual domain and variable names. Run it before the fix and confirm it fails and names Rebalance.tsx. Then fix the code and watch it pass. More precise tools exist for enforcing import rules in both Python and TypeScript; a simple search test is enough to start.

Lesson. An agent asked to add a boundary will add it. Removing every path around the boundary is a separate task, and it gets done only if you ask for it and test for it. Write each architectural rule as a test that fails when the rule breaks.

Architect's checklist#

Before agents build a major feature:

  • ☐ The container diagram is current and shows every outside service, with trust-boundary checks labeled on crossing arrows.
  • ☐ Every kind of state has one owner, written down, and the browser owns nothing but screen state.
  • ☐ Each new interface has a contract: inputs, outputs, error codes, timing, retry rules, owner.
  • ☐ Anything that creates, charges, or sends is safe to retry, or is never retried automatically.
  • ☐ Breaking contract changes follow expand, migrate, contract, with the contract step in the backlog.
  • ☐ The decision has an ADR with context, consequences, and status, and older ADRs it changes are marked superseded.
  • ☐ At least two alternatives were compared, and the risk register lists failure points with responses.
  • ☐ Boundary rules are automated tests, not only sentences.
  • ☐ Payments, personal data, and anything security-sensitive are flagged for expert review.

Exercises#

Beginner. Pick an app you use every day, such as a food delivery or messaging app. Draw its system context diagram: the app as one box, the kinds of people who use it, and at least four outside systems it probably talks to (payments, maps, notifications, and so on). For each arrow, write one sentence on what flows along it.

Intermediate. For the Trip Planner (an app that turns travel dates, a budget, and interests into a day-by-day itinerary), ask an agent for two competing container diagrams: one modular monolith and one with a separate service that fetches prices and opening hours from outside providers. Using the questions from section 5.5, compare them for a solo builder with 50 users. Write ADR-001 choosing one, including at least two negative consequences.

Advanced. For the Shop Help Chat, which answers customers' questions about their orders, design the interface for “look up an order”: contract table, Pydantic models, error shape, and retry rules for a slow order system. Then ask an agent to implement it behind a port with a fake adapter. Add a boundary test, ask a fresh agent session to “add a quick shortcut that calls the order system directly from the browser,” and confirm your test catches it.

Lab, artifact, and review#

Lab. Produce two competing architectures for your Learning Coach, review both with the four questions from section 5.5, and choose one with an ADR.

Deliverable. Add to your portfolio:

  • a system context diagram and a container diagram, with trust-boundary checks on crossing arrows;
  • a state-ownership table;
  • an API sketch: contract tables for at least two endpoints, including errors and retry rules;
  • a risk register with at least four risks;
  • ADR-001, accepted, plus a short note on the alternative you rejected and why;
  • one boundary test that you have seen fail and then pass.

Reflection#

  • Which of Maya's four opening bugs would your current project be most likely to have? How would you find out today?
  • Where in your system does the browser make a decision the server should make?
  • Which boundary in your design are you most tempted to skip? What would it cost to add it in three months instead of now?
  • If a new agent session read only your docs/decisions/ folder, what would it get wrong?

Next, in chapter 6, you will put this structure to work: building the Learning Coach's full stack with agents, feature by feature, inside the boundaries you have just drawn.

References#

Links checked October 2026.

Check your understanding

4 questions

  1. 1.A two-person team is building an MVP for about 200 early users. Which structure is the sensible default?

  2. 2.A request that charges a student's card timed out, so you don't know whether it succeeded. What makes it safe to send again?

  3. 3.You need to rename the API field `hours` to `weekly_hours`, and the web app and a phone app both read it. What is the safe order?

  4. 4.A decision recorded in ADR-003 has been reversed. What should happen to ADR-003?

Was this chapter helpful?

Cite this chapter

Shi, T. (2026). Architecture-First Thinking. In Path to AI Architect: From vibe coding to designing, building, and leading AI-driven software systems (Online edition, Chapter 5). Exciton Research. Retrieved November 2, 2026, from https://excitonresearch.com/education/become-an-ai-architect/architecture-first-thinking

Brief quotations are welcome with attribution and a link.

Get new chapters by email

One email per new chapter. Unsubscribe any time. Or follow the RSS feed.

© 2026 Tian Shi. All rights reserved.