Chapter 12 · Week 12

Architecture First

What does it mean to build well?

Chapter 12 — Architecture First

“Come, let us build ourselves a city and a tower with its top in the heavens, and let us make a name for ourselves.” — Genesis 11:4

“And I saw the holy city, new Jerusalem, coming down out of heaven from God, prepared as a bride adorned for her husband.” — Revelation 21:2


Why This Matters

For three weeks you have been working at the method and small-class level. Spec a method. Prompt for it. Review it. Fix it. Test it. Ship it. That is the senior’s hour-by-hour work, and you can now do it well.

This week we zoom out. Before any method is specced, before any prompt is written, before any test is written — someone has decided what classes exist, what they do, what they don’t do, how they talk to each other, and where state lives. That deciding is architecture. It is the senior engineer’s most non-negotiable contribution. It is the thing AI fundamentally cannot do for you, because it is a question of judgment — of taste, of trade-offs, of values you have and the AI does not.

Here is the rule the chapter will defend: a senior who delegates architecture to AI is no longer a senior. They are a vibe coder who has gotten the AI to write 90% of the code instead of 60%. The output may compile. The system will, predictably, become a mess no one wants to extend in six weeks. This is the most common failure mode of AI-heavy projects in the real world right now. The students who learn to refuse this failure are the students who will be running engineering teams in ten years.

You are eleven chapters into building taste. You have written specs. You have reviewed code. You can recognize good and bad. Now you decide the shape of the system before the AI writes a line of it.

Coach’s Note — Of all the Phase 2 chapters, this is the one whose absence does the most damage in practice. The other failures (bad prompts, missed bugs) can be caught by review. An architectural failure cannot — it is baked into the system and only reveals itself when you try to extend it. Get this right and the rest of your engineering life gets meaningfully easier.


12.1 — What Architecture Is

Architecture is the answer to the question: what is the shape of this software, and why?

At minimum it answers:

  1. What classes exist? (Names, responsibilities.)
  2. What does each class not do? (Boundaries.)
  3. How do classes talk to each other? (Interfaces, dependencies, data flow.)
  4. Where does state live? (Who owns what data.)
  5. What is hidden behind what? (What’s public, what’s private, what’s an interface.)
  6. What can change later without breaking everything else? (Where the seams are.)

Architecture is the diagram you would draw on a whiteboard before writing a line of code. The diagram is not the code. The code implements the diagram. You can write a thousand correct lines of code on top of bad architecture and produce a system that is harder to maintain than a hundred lines of correct code on good architecture. This is not a metaphor; it is the experience of every engineer who has been around long enough to inherit other people’s systems.

Architecture is also the boundary discipline. A senior engineer’s primary act of design is deciding what does not go in this class. The temptation is always to add — one more helper method, one more piece of state, one more responsibility. The senior says no. The class does one thing. The next thing goes in the next class. The boundary stays clean.

Three principles do most of the architectural work:

  • Single responsibility. Each class does one thing.
  • Clear interfaces. Each class communicates with others through a deliberately chosen public API.
  • Hidden internals. Each class’s data and helpers are private until proven they need to be public.

These are not new. They were Coding 1 material. Phase 2 makes them non-negotiable senior judgment because the AI will not enforce them for you.


12.2 — Why AI Cannot Do This For You

AI can write any module you describe. AI cannot decide which modules to ask for.

Consider what happens when you say to your AI assistant: “Build me a habit tracker.”

The AI will produce something. Possibly an App class with everything in it. Possibly a HabitTracker, a Habit, and a Main. Possibly fifteen classes if it’s feeling ambitious. The output will compile. It will probably work on a small example. But:

  • The class boundaries the AI chose were not chosen for your use case — they were chosen because they pattern-matched against habit-tracker-shaped code in the training data.
  • The persistence layer was probably hard-coded inside the main class, instead of being swappable.
  • The internal data structures probably leaked through public methods.
  • The dependencies are probably tangled — HabitTracker knows about file paths, the file format knows about UI strings, the UI knows about persistence details.
  • There is no documented intent for why anything was decided.

In two weeks when you want to add “remind me at 9am” or “sync to another device” or “export to JSON,” every change requires touching half the codebase, because the seams aren’t where they need to be.

The architecture is wrong. The AI did not know your requirements (most of them weren’t in the prompt), did not know your conventions, did not know your team, did not know what would change later — because the AI does not know anything outside what you handed it. Architecture is the place where context matters most and where AI has the least context. That’s why it stays with you.

The right move:

“Implement this HabitRepository interface that I’ve designed. The interface is below. The persistence implementation should use a single JSON file. The implementation should not depend on any class outside this package.”

That is a request for a specific module within an architecture you designed. The AI can do it well. Because you decided the shape. You set the boundaries. You named the interface. The AI fills in a body. The senior/junior model works exactly as designed.


12.3 — A Worked Example: The Habit Tracker

Let’s design one to show what architectural thinking looks like in practice.

The brief

A small command-line habit tracker. The user defines named habits (e.g., “read scripture,” “exercise,” “call parents”). Each day they record check-ins on which habits they did. The program persists state across runs. The program supports streak calculation — “I have read scripture 14 days in a row.”

That’s the brief. Notice it is not a spec. The spec is what we are about to produce.

Bad: just ask the AI

If you handed the brief above to the AI and asked for code, you would get something. It would compile. It would have a HabitTracker class with public fields, a main method that handled the menu and the persistence, a hard-coded filename. The streak logic would be inside the same class. Adding any feature would touch the whole file.

We are not doing that.

Good: design first, then prompt

Step 1 — list responsibilities, then group them.

What does the system need to do?

  • Define new habits.
  • Record a check-in for a habit on a specific date.
  • List all habits.
  • Compute the current streak for a habit.
  • Persist state.
  • Load state at startup.
  • Provide a CLI.
  • Parse user commands.
  • Display output.

Step 2 — name the boundaries.

Group those responsibilities into classes that each do one thing:

ClassResponsibilityKnows about
HabitHolds the name and check-in dates for one habit. Computes streaks.LocalDate, Set<LocalDate>
HabitTrackerThe collection of habits. Add, list, find by name.Habit
HabitRepository (interface)Save and load the HabitTracker’s state.HabitTracker
JsonHabitRepositoryImplements HabitRepository using a JSON file.HabitRepository, HabitTracker, JSON library
CliRead commands from stdin, dispatch to HabitTracker, format output.HabitTracker, HabitRepository
MainWire everything together and run the CLI.All of the above

Six classes. Each does one thing. The dependencies flow downward — Main knows about everything; Habit knows about almost nothing. This is the architecture.

Step 3 — draw it.

+------------+
|    Main    |
+------+-----+
       |
       v
+------------+       +-------------------+
|    Cli     |<------|  HabitRepository  |  (interface)
+------+-----+       +---------+---------+
       |                       ^
       v                       |
+--------------+      +--------+----------+
| HabitTracker |<-----| JsonHabitRepository|
+------+-------+      +-------------------+
       |
       v
   +-------+
   | Habit |
   +-------+

The arrows are “depends on.” Main depends on Cli and on the wiring. Cli depends on HabitTracker and HabitRepository. JsonHabitRepository depends on HabitRepository (the interface) and HabitTracker. Habit depends on nothing application-specific.

Notice the interface: HabitRepository. If next week we want to swap from JSON to a SQLite file, we add a SqliteHabitRepository that implements HabitRepository. No other class changes. That is the seam.

Step 4 — write the interfaces and signatures, in code.

public final class Habit {
    public Habit(String name);
    public String name();
    public void checkIn(LocalDate date);
    public boolean checkedIn(LocalDate date);
    public int currentStreak(LocalDate today);
    public Set<LocalDate> checkIns();   // unmodifiable view
}

public final class HabitTracker {
    public void define(String name);
    public Habit get(String name);              // throws if missing
    public List<Habit> all();                   // unmodifiable view
    public void recordCheckIn(String name, LocalDate date);
}

public interface HabitRepository {
    void save(HabitTracker tracker) throws IOException;
    HabitTracker load() throws IOException;
}

public final class JsonHabitRepository implements HabitRepository {
    public JsonHabitRepository(Path file);
    public void save(HabitTracker tracker) throws IOException;
    public HabitTracker load() throws IOException;
}

public final class Cli {
    public Cli(HabitTracker tracker, HabitRepository repo, InputStream in, PrintStream out);
    public void run();
}

That is roughly two screens of Java. There is no implementation code yet. What there is: a complete contract for the entire system. Anyone could now implement any one of these classes against the others, without coordination, and the pieces would fit.

Step 5 — now prompt the AI, one piece at a time.

“Implement Habit to the following signatures. Use only java.time and java.util. currentStreak(today) is the number of consecutive days ending at today on which checkedIn(d) is true. checkIns() must return an unmodifiable Set<LocalDate> — no caller mutation. Reject null arguments.”

You get back a Habit. You review it. You write tests. You ship it.

Then you ask for HabitTracker. Then JsonHabitRepository. Then Cli. Then Main. Each prompt is small and tightly scoped. Each output is reviewable in five minutes. Each module has its own tests.

Total code: maybe 400 lines across the six files. Almost all of it AI-generated. Every architectural decision is yours. The system in two weeks, when you want to add a feature, is easy to extend, because the architecture said so.

This is the workflow. This is what Phase 2 builds you toward. Architect like a senior. Implement like a senior with a junior. The AI is exceptional at the second move. You — and only you — do the first.


12.4 — The Single Responsibility Principle, In Plain Words

A class should have one reason to change.

That phrasing is the Robert Martin version, and it is right but abstract. Here is the plain version:

A class does one thing. If you find yourself describing the class with the word “and” — “this class loads habits and saves them and formats them for display and parses user input” — you have already failed.

In the habit tracker example, Habit does one thing: hold a habit’s state and answer questions about it. HabitTracker does one thing: manage the collection. JsonHabitRepository does one thing: persist to JSON. Cli does one thing: translate between user input and HabitTracker calls. None of these classes need the word “and” to describe.

If you cannot describe a class without “and,” split it. That is the move. Almost always, the new boundary is obvious once you see the “and” — one class becomes the part before the “and,” another becomes the part after.

Common compositional smells

SmellWhat it meansFix
Class with too many fieldsIt’s holding state for multiple responsibilitiesSplit
Class with too many public methodsIt’s serving multiple callers’ needsSplit
Class that imports both java.nio.file and a UI frameworkIt’s doing both I/O and presentationSplit
Class whose name has “Manager,” “Util,” “Helper,” “Service”It probably has no clear responsibilityRename until you can describe it cleanly, then split
Class with a 200-line methodThe method itself is doing multiple thingsExtract methods within, then check if the class is still single-responsibility

The fixes are all “split.” Architecture is the practice of making the splits at the right places. Taste is what tells you where.


12.5 — Interfaces As Seams

An interface in Java (public interface Foo { ... }) is a contract without an implementation. Multiple classes can implement the same interface; callers can hold references to the interface type rather than the concrete type.

Interfaces are the senior’s primary architectural tool because they are seams — places where one implementation can be swapped for another without touching the rest of the system.

Use cases:

  1. Persistence is replaceable. Define HabitRepository. Today’s implementation is JSON. Tomorrow’s is SQLite. Caller code only knows the interface; persistence is decoupled.
  2. External services can be faked in tests. Define Clock instead of calling LocalDate.now() directly. In production, the real clock; in tests, a clock you can fast-forward.
  3. Strategies can vary. Define StreakRule. One implementation is “must check in every day.” Another is “weekends don’t break the streak.” The class that asks for a streak doesn’t care which rule is active.
  4. Plugins and extensions. Define CommandHandler. Each command is a class implementing it. Adding a command means adding a class, not editing a 400-line switch.

The cost of an interface: one extra file, one extra layer of indirection, one moment of “wait, where does this actually do the work?” The benefit: a system you can rearrange without rewriting.

The rule: introduce an interface where you predict the implementation might change, and only there. Premature interfaces are clutter. Late interfaces are refactors. The senior’s call is when.

A small example

Without an interface — Cli directly creates and uses JsonHabitRepository:

public final class Cli {
    private final HabitTracker tracker;
    private final JsonHabitRepository repo;          // concrete dependency

    public Cli(HabitTracker tracker, Path saveFile, ...) {
        this.tracker = tracker;
        this.repo = new JsonHabitRepository(saveFile);  // hard-coded
        ...
    }
}

Switching to SQLite later: rewrite this constructor, every test that constructs Cli, and any other class that touches the repository.

With an interface — Cli depends on HabitRepository:

public final class Cli {
    private final HabitTracker tracker;
    private final HabitRepository repo;              // interface dependency

    public Cli(HabitTracker tracker, HabitRepository repo, ...) {
        this.tracker = tracker;
        this.repo = repo;
        ...
    }
}

Switching to SQLite later: implement SqliteHabitRepository, change one line in Main to construct the new one instead of the JSON one. Nothing else changes.

This is the architectural payoff. The two-line difference at design time saves an afternoon of refactoring later. You don’t always need it — sometimes the persistence really won’t change. But when in doubt, the interface is the senior move.


12.6 — Module Boundaries and Data Flow

A module in Java is roughly “a package” or “a small set of related classes.” The boundaries between modules are the places where one engineer’s work hands off to another’s. They are also the places architecture lives or dies.

Two questions to ask at every module boundary:

  1. What data crosses this boundary, in what shape?
  2. What knowledge does each side need about the other?

If the data crossing the boundary is messy (a Map<String, Object> representing “habit data”), the modules are coupled — both sides have to know the implicit shape, and a change anywhere ripples. If the data is typed (a Habit record, immutable), the boundary is clean.

If module A needs to know “what JSON format module B uses,” they are coupled too tightly. Module B should expose a typed API; module A should not see the JSON.

A common newcomer mistake: passing strings or maps across boundaries because “they’re flexible.” Flexible means “anybody can put anything in there,” which means “nobody knows what’s actually in there.” Use proper types.

Coach’s Note — Java 17 makes this easy. Records, sealed interfaces, type-checked collections — the language gives you the tools to make boundaries typed. Use them. A record Habit(String name, Set<LocalDate> checkIns) {} is a much better boundary artifact than Map<String, Object>.

Data flow visualized

In the habit tracker, the flow is:

User input (String) → Cli (parses) → HabitTracker (typed method call)
                                          → Habit (typed method call)
                                          → returns typed result
                              ← Cli (formats) ← typed result
                  ← User output (String)

Strings only at the very edges. Everything in the middle is typed. The user types text; we parse it as soon as possible into typed values; everything internally moves typed values; only at output do we convert back to text.

This is a pattern worth memorizing: parse at the edges, type in the middle, render at the edges. Most well-architected systems do this.


12.7 — The Two-Pass Pattern

Architecture, like writing, is rarely right on the first pass. The senior’s working pattern:

Pass 1 — draft. Quickly sketch the classes, interfaces, and dependencies. Don’t agonize. Get something on paper (or in a design.docx file). Time-box: 20–30 minutes for a small project.

Pass 2 — refine. Walk through every class and ask:

  • Can I describe this class in one sentence without “and”?
  • Are any two classes really one class?
  • Is any class really two?
  • Are any dependencies pointing the wrong direction?
  • Where are the likely future change points? Should those be interfaces?

After Pass 2, then write the signatures. Then write the prompts. Then write the code.

The cost of Pass 2 is 20 minutes. The cost of skipping Pass 2 is a redesign in week three. Pay the 20 minutes.


12.8 — Babel and Jerusalem

The chapter’s apologetic frame draws on a contrast Scripture sets up early and resolves late.

Genesis 11 describes the tower of Babel. Humans, unified in language and ambition, decide to build a city and a tower “with its top in the heavens, and let us make a name for ourselves.” The project is ambitious. The work is real. The materials are good (“they had brick for stone, and bitumen for mortar”). The construction was probably impressive — for a while. The problem is not that the tower was poorly engineered. The problem is what the tower was for.

The motive is named in the text: let us make a name for ourselves. The work is oriented toward the builders’ glory. It is built without reference to the only thing that could give it lasting meaning. It is technically competent and existentially confused. And so it is scattered.

Revelation 21 closes the bookends. “And I saw the holy city, new Jerusalem, coming down out of heaven from God.” The city is not built by humans alone, not built for human glory, and it is described in extraordinary architectural detail — twelve gates, twelve foundations, the wall measured, the materials named. It is a city, and it is built well. The architecture matters. The shape carries meaning. The city is good because its order reflects something.

Architecture, the chapter says, is not neutral. The shape of a system carries values. A system designed for one engineer’s cleverness carries the values of that cleverness. A system designed for maintainability carries the values of patience and care for the next engineer. A system designed for honest reporting of data carries the values of truthfulness. The choice of what to make swappable and what to bake in is a moral choice — what does this system make easy to do, and what does it make hard?

The vibe-coder approach to building software is, in a small way, Babel-shaped. The motive is “ship the thing fast, take the credit.” The work is real. The materials (the AI’s code) are good enough. And the thing scatters in six months because nothing was built to last, because nobody was thinking about lasting.

The senior’s approach is, in a small way, Jerusalem-shaped. The motive is “build something that serves what it’s supposed to serve, that the next engineer can extend, that holds up under inputs the original author didn’t think of.” The work is the same. The materials are similar. The architecture is different, because the values are different.

Christian engineers should be the people in the room who notice when a project’s architecture has slipped into Babel-shape and pull it back. Not because building well is a Christian monopoly — it isn’t. But because we have a long tradition of taking seriously the question what is this for?, and that question is exactly the one architecture answers.

It also matters that good architecture is humble. Babel was built confidently. Jerusalem comes “down out of heaven from God” — gift, not achievement. The senior engineer who designs well does so by accepting limits — admitting they don’t know the future, building in seams so the future can be accommodated, naming the boundary between “what I decided” and “what I’m leaving open.” Humility is architectural. Pride is monolithic.

Coach’s Note — Don’t preach this when you architect. Just architect well. The values are visible in the work. Your fellow engineer in six months — Christian or not — will know what your system was built for by reading the boundaries you drew. Build it for them. That posture is the apologetic.


12.9 — The Senior’s Three Deliverables

Pulling together the chapters of Phase 2: the senior engineer’s three deliverables, before any AI prompt for code is written.

  1. The spec. (Chapter 2 + Chapter 10.) What the system promises. What each method takes and returns. What the error behavior is. The contract.

  2. The architecture. (This chapter.) What classes exist. What they do. What they don’t do. How they talk. Where the seams are.

  3. The tests. (Chapter 4 + Chapter 11.) The executable specification. The thing that says, in machine-readable form, “if the code does this, it is acceptable.”

Once those three exist, the AI’s job is to fill in method bodies. That is well within the AI’s competence. The senior reviews each body. The tests check the behavior. The architecture stays clean because the architecture was decided before anyone (you or the AI) started typing implementations.

Skip any of the three and the partnership degrades. Skip all three and you have vibe coding.

The discipline: spec, architect, test — then prompt. Memorize the order. Do not let the AI tempt you into “let’s just see what it comes up with.” That sentence is the beginning of a system you’ll regret.


12.10 — Common AI Pitfalls (Week 12 Edition)

Pitfall: You hand the AI a vague problem and accept the architecture it invents. What’s happening: You delegated the most senior part of the job to the most junior part of the team. Fix: Design the architecture yourself before prompting. The AI implements your architecture, not its own.


Pitfall: Your design has a Manager, Helper, Util, or Service class. What’s happening: That class probably has no clear single responsibility — those names are placeholders for “I haven’t decided what this class is.” Fix: Rename until you can describe it cleanly. If you can’t, split.


Pitfall: Your design has one giant class that does everything. What’s happening: You collapsed the architecture and made it monolithic. Now the AI can produce only monoliths. Fix: Split by responsibility. Aim for 3–8 classes for a small project.


Pitfall: Your classes all know about each other. What’s happening: Dependencies are tangled. Changing one will require changing all. Fix: Sketch the dependency arrows. They should flow in one direction. If they form cycles, you have a problem to redesign before coding.


Pitfall: You introduced an interface “just in case” for every class. What’s happening: Premature interfaces. Now you have twice as many files and no clarity. Fix: Introduce interfaces only where you predict the implementation might change, and only there.


Pitfall: Your classes pass Map<String, Object> to each other. What’s happening: You’re avoiding the work of defining types. The boundaries are now untyped; any change ripples. Fix: Use records and concrete types. Java 17 makes this cheap.


Pitfall: Your design has no design.docx document, just code. What’s happening: You can’t review your architecture because it lives only in your head and in the implementations. Next week’s you will not remember why anything was the way it was. Fix: Write the design down before coding. A page of text and a diagram. The act of writing reveals the bad decisions.


12.11 — Reps

Open the exercises. This week’s reps focus on design before code — drawing diagrams, naming responsibilities, deciding what’s an interface and what isn’t. Several reps ask you to describe an architecture in prose before producing any code.

Rep 1. Take a small program description and produce a class diagram and responsibility list — no code yet.

Rep 5. Take a single-class implementation and refactor it into a multi-class design without changing behavior.

Rep 9. Identify five “Manager”/“Util”/“Helper” classes in your own past work or in a public open-source project. Rename or split.

Full set in the exercises.


12.12 — This Week’s Project: Design Before You Prompt

You’re ready for Project 12: Design Before You Prompt, in Project 12.

The setup: given a problem (“a small habit tracker that records daily check-ins, persists across runs, supports streak calculation”), produce a complete design document before writing any code. Class diagram. Responsibilities. Public APIs. Data formats. Then implement, using AI for individual methods within your modules but never for the architecture.

Three tiers:

  • Normal — full design document + implementation.
  • Medium — include a swappable persistence interface with two implementations.
  • Hard — get a classmate code review on your design, document what changed and why.

12.13 — Coach’s Final Word

Four chapters into Phase 2. You have learned to prompt (Chapter 10), to review (Chapter 11), and now to architect (this chapter). Together those are the senior’s whole job. The AI does everything else.

A year from now, on a real engineering team, the question of whether you ship reliable software will come down to this: did you do the architectural thinking up front, or did you let the AI’s pattern-matching make decisions you should have made? The students who learn to do the up-front work are the ones whose code lasts. The students who skip it are the ones whose code becomes someone else’s nightmare in week three. Be the first kind.

Two chapters left in the “AI skill” arc. Chapter 13 puts iteration on top — what to do when the AI’s first answer is wrong and you have a path to get it right. Chapter 14 takes the honest measure of authorship: what is yours, what is the AI’s, and how do you say so. Then capstone prep, then the final.

You’re past the halfway point of the book. The work is harder now. The work is also more like the work of a real engineer. That is intentional. Stay sharp.

See you Monday. Bring your whiteboard.


Up next: Read the exercises — design-heavy reps. Then open Project 12. After that, Chapter 13 — Iterative Refinement.