Software Design Reference Core Designer · Structuring for Stability

Lesson 0010 · Core Designer · Module 2

Guiding AI Toward Better Structure

Lesson 0005 taught you to review AI output. This lesson teaches you to steer it before it writes: specify the seams, then let the AI fill them in.

Reading ≈ 10 minPractice ≈ 25 min

Mission tie-in: "guide AI toward better structure instead of patching bad outputs" — the exact skill that separates developers who stay in control from developers whose codebase becomes the AI's drafting table.

Knowledge: you are the architect, AI is the drafter

A drafter draws what the architect specifies. Ask an AI to "build a checkout" and you will get a god function with SDK calls inside — because you specified a feature, not a structure. Ask it to implement a structure you already designed and the output inherits your design.

The recipe — specify the seams first:

  1. Name the boundary. Which parts are domain, which are infrastructure? State it in the prompt: "domain code must not import the SDK."
  2. Declare the ports. Give the interfaces: PaymentGateway, OrderRepository. "Implement the adapter, not the port."
  3. State the invariants. "An order can never be confirmed twice." AI will encode what you tell it to protect.
  4. Ask for the tests. "Write the tests first, with an in-memory fake adapter." The tests document the structure as much as they verify it.
# prompt shape (paraphrased):
# Implement checkout for the CheckoutService below. Do NOT modify this
# class or the PaymentGateway port. Domain code must not import Stripe.
# Write StripeAdapter in infrastructure/, plus tests using a FakeGateway.

class PaymentGateway(Protocol):
    def charge(self, amount: Money, token: str) -> str: ...

class CheckoutService:
    def __init__(self, gateway: PaymentGateway, orders: OrderRepository): ...

Now the AI's freedom is bounded by your structure. Bad output is still possible — AI loves a helper explosion — but the defects are now in the filling, not the frame.

† The review loop stays: even with a boundary spec, run the five-point checklist from lesson 0005 on the result. Steering reduces defects; it doesn't eliminate them.
The principle behind the principle

Everything you leave unspecified is filled in with the statistical default of the model's corpus, which is the tutorial shape from lesson 0005. That single fact tells you where your leverage is — and where it is not.

It is not in adjectives. "Write clean, maintainable, well-structured code" cannot steer anything, because it does not select between candidate outputs: the model already believes its output is clean, and so does the next one you ask. Leverage lives in constraints that are checkable — a type signature it must not alter, an import that must not appear, a test that must pass. Checkable constraints do two jobs at once. They narrow generation, and they let you verify the result without reading every line, which matters because review, not generation, is the resource you are short of.

The deeper asymmetry is about time. A structural defect is paid at change time, months after the code was written, by whoever is on call. The generator is optimising for plausibility now and has no stake in the next twelve months; you are the only party in the loop that does. That is the entire reason the human owns the structure and delegates the filling — not because AI writes worse lines, but because nobody else in the transaction is holding the long end of the cost.

It is also worth being precise about what a frame improves. Specifying structure mostly reduces variance, not the average line. You get fewer catastrophic shapes — the god function with the SDK inside, the module-level cache — rather than uniformly better code. That is exactly the improvement you want from a component you cannot supervise line by line.

Where it stops applying. Over-specification destroys the thing AI is genuinely good at: exploring an unfamiliar API, generating a large volume of mechanical code, showing you three shapes you had not considered. For exploratory work a tight frame produces a confident, well-structured answer to the wrong question, which is harder to detect than a mess. And a frame you have not thought through is worse than no frame at all: the model will implement your bad boundary faithfully, at speed, in enough places that moving it later becomes a project.

How a senior designer writes the brief

The task: sync inventory from a supplier API, to be generated rather than hand-written.

They start from what they are unwilling to review. Not "what should this code do" but which mistakes would I fail to notice in a diff? Three answers: dependency direction, because an import inside a new file looks like every other import; error semantics under partial failure, because the happy path is what a reviewer reads; and whether the tests actually exercise the seam or quietly patch around it. Those three become the specification. Everything else — naming, HTTP details, retry timings — is left free, because a wrong choice there is visible in review and cheap to change.

The alternative they reject is the forty-line prompt. It dictates naming conventions, file layout to the character, docstring style, and the logging format, and it reads like rigour. In practice it does two harmful things: it buries the three constraints that matter among thirty that do not, and it spends the model's attention on compliance with cosmetics. The better artefact is smaller and harder — the port pasted in as literal code, one forbidden import, one test that must pass. Three sentences you could check mechanically beat forty you could not.

Then the move that outlives the session. A prompt is gone when the window closes; the next developer, and the next generation, know nothing about it. So the durable form of "domain must not import the SDK" is not a sentence in a chat — it is an import contract in CI:

# architecture test: the rule survives the conversation that agreed it
FORBIDDEN = {"stripe", "requests", "boto3"}

def test_domain_imports_no_vendors() -> None:
    for module in walk_modules("app/domain"):
        assert not FORBIDDEN & imports_of(module), module

Now the constraint is enforced by something that does not get tired on a Friday afternoon, and it applies equally to code written by a person, by a model, or by a person in a hurry using a model. This is the fitness-function idea from lesson 0022, aimed at the specific problem that generated code arrives faster than review can inspect it.

The consequence at team scale. Take a team of five where roughly a third of new code is generated. Without a checkable frame, structural defects arrive faster than review removes them; nothing dramatic happens in any given week, and within a quarter the architecture the team believes it has and the architecture in the repository have quietly separated. With three machine-checked constraints, drift is caught by a build, not by a reviewer's diligence — and the reviewer's attention goes where a machine cannot help: whether the business rule is right at all.

Field notes · the sentence, and the defect it prevents
Put this in the promptWhat it pins downWhat you get without it
“domain/ must not import stripe”Dependency direction (lesson 0009).SDK calls inside the business rule.
“implement the adapter, not the port”The interface stays yours.A port quietly reshaped to match the SDK.
“an order cannot be confirmed twice”The invariant lives in the object.A status flag every caller must remember.
“tests first, with an in-memory fake”The seam is proven by a test.Tests that patch requests and freeze the defect.
“no parameters beyond this case”Speculative generality is blocked.A configurable framework for one use.

When this is the wrong move

Do not frame exploratory work. When the question is "what would this look like using library X", a specification pre-commits you to a design you are still trying to evaluate, and you get a fluent answer that forecloses the alternatives you were asking about. Do not specify a boundary you have not earned: if you do not yet know where the seam belongs, saying it confidently produces a guess embedded in twenty files rather than in one. Do not bother for genuinely mechanical output — a data migration, a batch of fixtures, a translation of a schema — where structure is irrelevant and correctness is checked by running it. And apply the arithmetic honestly: for twenty lines, writing the specification costs more than writing the code, so write the code.

What people get wrong after reading this
A good enough prompt removes the need to review.
Framing shifts where the defects land — into the filling rather than the frame — which lets a review be shallow in the right places. It does not empty the tail, and the tail is where the expensive defects live. Steering and reviewing are complements, not substitutes.
Telling it to follow SOLID or write clean code works.
Those are adjectives with a bibliography. They do not discriminate between candidate outputs, because every output already claims them. Ask for the consequence instead: which module may import what, which invariant the object must refuse to break, which test must pass.
More context always helps.
More context improves convention-matching and API usage, and it also lets the model reproduce your existing defects with confidence and at scale. If your codebase already swallows exceptions, a well-contextualised model will swallow them in your house style. Context is a mirror, not a filter.
The specification is the prompt.
A prompt lasts one session. The durable artefact is a test, a lint rule, or an import contract — something the next person and the next generation cannot bypass without noticing. If a structural rule matters enough to say twice, it matters enough to encode once.

Skill: prompt with the design first

Which kind of instruction most reliably changes the structure of generated code?

"Domain code must not import the SDK" is an instruction about:

Asking AI to write tests with a fake adapter serves to:

Practice on your own code

Pick a small feature you'd normally hand to AI. Before writing the brief, answer the senior question: which mistakes here would I fail to notice in a diff? Specify only those, as constraints something could check. Then generate, and run the five-point checklist from lesson 0005: how many defects did the spec prevent, and which one of your three constraints could you turn into a test that outlives the session?

In the lab: the returns-resolution service is the place to try this end to end. The phase-1 checkpoint asks you to write a boundary brief for one new capability, generate against it, review the result with the five-point checklist, and then convert your most important constraint into an executable check that fails the build if a later change breaks it.

Reveal: a before/after comparison

Before: "Write a function to sync inventory from the supplier API." Result: one function doing HTTP, parsing, merging, and saving, with requests imported throughout.
After: "Implement InventorySyncService using the SupplierFeed port below; adapters in infrastructure/; tests with a canned feed." Result: domain service with two injectable ports, thin adapter, and tests that run offline. Same feature, opposite structure.

Teach it back
  1. Explain to a junior, in two sentences, why "write clean code" is a useless instruction and "domain must not import stripe" is a useful one.
  2. Take one structural rule your team currently enforces by review this week and name its cost: how many times it has been said in pull requests, and how many times it has still slipped through.
  3. Predict what your architecture looks like in a year if generated code keeps arriving faster than it can be reviewed, and no rule is machine-checked. Name the first property that stops being true.

Your win

You can now treat AI as a drafter with a brief: structure specified up front, defects reviewed after. The combination is the Core Designer's AI workflow.

Read and watch deeper

Draft a boundary spec with your agent-teacher for a feature you're about to generate — then compare the spec you'd have written alone.