Software Design Reference Supplements · Working with AI

Supplement · 5 · Working with AI

Multiplying Judgment, Not Replacing It

A generator that writes plausible code faster than you can read it changes where your leverage is. It moves from typing to three things: the boundary you specify, the review you run, and the decisions you refuse to delegate. This supplement makes each of those concrete.

Reading ≈ 24 minPractice ≈ 75 min

Mission tie-in: “use AI as a multiplier of judgment, not a replacement for it.” Lesson 0005 gave you the five-defect review and lesson 0010 the boundary spec. This page is what changes when you are leading an initiative rather than writing one function.

Why this is here

The situation that brings a lead here is one of volume and one of stakes. Volume: four engineers are each generating more code per day than any of them could review by reading, and “it passes the tests” has quietly become the quality gate. Stakes: someone pasted a production incident timeline into a chat window to get help writing the postmortem, and nobody had said whether that was fine.

Underneath both is a third problem that is easier to miss. AI is extraordinarily good at producing artifacts that look like the outputs of the work in this course — an architecture-driver table, an ADR, a threat model, a set of quality-attribute scenarios. The form is right. What is missing is the thing that made the artifact worth having: contact with real users, the real domain, and real production behaviour. An ADR that records a decision nobody made is worse than no ADR, because it is indistinguishable from one that matters.

Knowledge: four disciplines

1. Review generated code against boundaries and invariants

Reviewing generated code as if it were a colleague’s is a mistake in both directions: too suspicious about style, not suspicious enough about structure. The generator has read enormous amounts of ordinary code and reproduces its ordinary defaults — which is exactly what lesson 0005’s five defects are. What it cannot know is your architecture, because your architecture is not in its training data and usually not in the prompt either.

So run a protocol, in this order. The steps are cheap first and expensive last, and each one can reject the change before you spend the next:

  1. Boundary: which layers does this import? Does anything in domain/ now reach infrastructure? (lesson 0009)
  2. Invariant: can this code produce a state the domain forbids? Does it set a status field directly instead of asking an object to make a transition? (lesson 0011)
  3. Contract: does it change the shape of anything crossing a boundary — a schema, an event, a return type — without a compatibility story? (lesson 0045)
  4. Failure: what happens on timeout, duplicate, partial success? Is a failure being converted to None or swallowed?
  5. Seam: can this be tested without a network, a clock, or a database?
  6. Deletion: what did it add that already exists somewhere in the codebase?

Here is a generated implementation of “approve a return and create the label.” It is fluent, it would pass a superficial reading, and it fails four of the six steps:

# GENERATED. Prompt was: "add a method to approve a return and create a label"
class ReturnService:
    def approve(self, return_id: str) -> dict:
        row = db.execute(
            "SELECT * FROM returns WHERE id = ?", (return_id,)
        ).fetchone()

        label = requests.post(
            "https://api.carrier.example/v2/labels",
            json={"to": row["address"], "weight": row["weight"]},
            timeout=30,
        ).json()

        db.execute(
            "UPDATE returns SET status = ?, label_url = ? WHERE id = ?",
            ("approved", label["url"], return_id),
        )
        return {"status": "approved", "label_url": label["url"]}

Against the protocol, in order:

Review protocol · applied to the generated method
StepWhat the code doesWhy it fails
BoundaryA service method holds SQL and an HTTP call to a named vendor.The carrier’s URL is now a domain fact. The port from lesson 0009 exists in this codebase and was not used.
InvariantSets status = 'approved' by direct update.The state machine in lesson 0045 allows approved only from pending decision. This will happily approve an already-refunded return.
ContractReturns an untyped dict.Callers depend on string keys nothing enforces; the refusal case has no representation at all.
FailureA 30-second carrier call sits between the read and the write.Timeout leaves the return unapproved but possibly labelled at the carrier — and a retry buys a second chargeable label. No idempotency key.
Seamdb and requests are reached directly.No test runs without a database and a network, so nobody will write one.
DeletionAdds a second path to label creation.A CarrierGateway adapter already exists. This is duplication that will diverge.

The corrected version is not cleverer. It is the same feature expressed inside the structure that already existed — which is precisely what a boundary spec in the prompt would have produced the first time:

class ReturnService:
    """Approve one return. The carrier call happens after the transition is
    committed, because a failure to produce a label must not un-approve a
    return the customer has already been told about."""

    def __init__(self, returns: ReturnRepository, labels: LabelRequestQueue) -> None:
        self._returns = returns
        self._labels = labels

    def approve(self, principal: Principal, return_id: str) -> ReturnRequest:
        found = self._returns.get_for_owner(return_id, principal.customer_id)
        if found is None:
            raise ReturnNotFound(return_id)

        found.approve()               # the aggregate refuses illegal transitions
        self._returns.save(found)

        # one intent, one key: a retry re-uses the label instead of buying one
        self._labels.enqueue(return_id=found.id, idempotency_key=found.id)
        return found

Three structural facts moved: the transition is a method on the aggregate rather than a string assignment, the vendor call left the transaction and became a queued intent with an idempotency key, and the return type is a domain object rather than a dictionary. None of those is a matter of taste, and none of them is something the generator could have known.

The principle behind the principle

Generated code is fluent by construction: it is optimised to look like code that exists, and most code that exists puts SQL next to HTTP next to a status assignment. So fluency carries no information about correctness, which breaks the heuristic experienced reviewers rely on most — that badly structured code usually reads badly. When the tell disappears, you have to replace intuition with an explicit checklist, in a fixed order, run every time.

The order matters as much as the contents. Boundary and invariant come first because they are the two properties that are cheap to check, catastrophic to miss, and impossible for the generator to infer from the prompt. Style comes nowhere on the list; the generator is better at style than at structure, and a review that spends its attention on naming will spend none on the transaction that now contains a 30-second network call.

This is also why lesson 0010’s prompt-side move matters more as volume rises. Review is linear in the amount of code; specification is not. Putting “domain must not import the carrier SDK; approve() is a method on the aggregate; the carrier call is queued” into the prompt costs one sentence each and removes a whole class of finding from every future review.

2. Protect confidential product and system context

The useful question is not “is this tool safe” but “what does this specific input become if it is retained, logged, or read by someone else.” Answer it once, as a team, and write it down — because the alternative is that each engineer answers it privately under time pressure.

Context classification · what goes into a prompt
NeverOnly anonymizedFreely
Customer PII: names, addresses, order references, support transcripts, the free-text return notes.Production incident timelines — strip customer identifiers, internal hostnames, employee names.Public library and framework questions.
Credentials, tokens, private keys, connection strings — including “expired” ones.Schemas and contracts — keep the shape, replace table and field names that carry commercial meaning.Algorithm and data-structure reasoning with synthetic data.
Unreleased pricing, contract terms, vendor rates, roadmap dates under embargo.Architecture problems — describe the forces, not the company. “Two teams, different release rhythms, shared order-of-record.”Your own code where the domain vocabulary is generic.
Un-disclosed security findings, and any detail that would help exploit them.Log samples — redact identifiers and payloads; keep the structure and the error.Course exercises and the returns case in this course.

The middle column is where the actual skill lives, and it is a skill you already have to practise for other reasons: the anonymization technique in the portfolio guide — strip the identifying particulars, keep the forces — is the same operation. An architecture question survives anonymization almost perfectly, because what makes it interesting is the shape of the constraint, not whose company it is.

Two operational rules complete it. Check your organisation’s actual policy and the tool’s actual data handling rather than assuming either; a tool configured for zero retention on an enterprise plan is a different object from the same tool on a personal account. And treat the prompt as a place a mistake is permanent — once pasted, you cannot un-paste, so the review happens before the send, not after.

3. Record which decisions stay human-owned

The failure here is not that a model makes a bad decision. It is that nobody notices a decision was made at all — a default appears in generated code, survives review because it looks normal, and becomes architecture. Prevent it by writing down, in advance, which classes of decision require a named human and what evidence they require.

Decision ownership · returns initiative
DecisionAI mayHuman ownerEvidence required
Which outcome the initiative targetsnothing — it has no access to the businessProductUser research, support data, the baseline measure
Which architecture drivers are prioritiseddraft candidate scenarios for critiqueTech lead, with productStakeholder agreement; a measure that can fail
Data ownership and authoritative writerlist options and their failure modesTech lead, with the owning teamThe state model; agreement from Payments
Build versus buydraft the cost ledger structureTech lead, with productReal quotes, real rates, real volumes
Accepting a security or privacy riskenumerate threats for the workshopNamed accountable personRanked findings; a recorded acceptance with a revisit date
Rollout gate thresholdspropose telemetry and thresholdsTech lead, with operationsBaseline production behaviour
Whether a slice is donegenerate tests and checklistsThe teamPassing tests, observable behaviour, an operational owner
Naming in the ubiquitous languagesuggest candidatesThe team, with the domain expertThe expert uses the word unprompted
Implementation inside an agreed boundarydraft freelyReviewing engineerThe six-step protocol above

The last row is the point of all the others. This table is not a restriction exercise; it exists so that the large, fast, valuable category — drafting implementations inside a boundary someone chose deliberately — can run at full speed without anyone worrying that something important is being decided by accident.

4. Adversarial prompts, without accepting fabricated facts

The strongest use of a generator in design work is not generating designs. It is attacking yours. A model is tireless, has no stake in your decision, and is very good at enumerating failure modes — which is precisely the work humans skip because it is unrewarding and slightly demoralising.

Four prompts worth keeping. Each is written to demand enumeration and mechanism, never conclusions, because conclusions are where fabrication lives:

Here is a state machine for a return: [states and transitions]. Generate every sequence of events that reaches an illegal state or a state with no exit, including duplicate delivery, out-of-order delivery, and a message arriving after a timeout. For each, state the exact sequence. Do not suggest fixes yet.

You are the on-call engineer at 3 a.m. This design has just failed in production. Write the first five minutes of the incident channel: what alert fired, what you cannot tell from the dashboards, and which question you cannot answer with the telemetry described here.

Argue the strongest possible case against this decision, as the engineer who advocated the other option. Use only the constraints I gave you. If you need a fact I have not given you, say “I need X” instead of assuming it.

Review this contract as a hostile consumer. For each field: what happens if it is missing, null, duplicated, or a type I did not expect? Which of my assumptions about it are not written down anywhere?

Now the discipline that makes the output usable. Every one of these produces a list, and a list of possibilities is not a list of facts. Sort each item into one of three buckets before it touches your design:

Three buckets for anything a model tells you
Checkable here, now
“Your retry has no idempotency key.” Open the code. This is the best kind of output: it is a claim about an artifact you hold, so verification costs seconds.
A hypothesis that needs evidence
“Customers abandon the flow at the reason-code step.” Plausible, possibly true, entirely unevidenced. It becomes a line on the assumption map with a test, not a finding.
An assertion about the world
“This carrier’s API guarantees at-most-once delivery.” Treat as unverified until you have read the vendor’s documentation yourself. Confident phrasing is not evidence; a citation you have not opened is not evidence either.

The third bucket is where real damage happens, because a fabricated fact arrives in the same fluent register as a correct one, and once it is in a document it acquires the authority of having been written down. The rule is blunt and worth stating as a team norm: AI artifacts never substitute for user, domain or production evidence. A model can help you design a study, draft a survey, or list the questions to ask a domain expert. It cannot be the domain expert, and its confident answer about your production behaviour is a guess about a system it has never seen.

† A practical tell: ask the same question in two separate sessions, phrased differently. Genuine reasoning from the constraints you supplied tends to survive rephrasing; a confabulated specific — a version number, a limit, a guarantee — often does not.
Field notes · the failure and the countermeasure
You see thisWhat it costsThe move
“The tests pass” as the review.Generated tests test generated behaviour, including the defect.Run the six-step protocol; check boundary and invariant first.
An ADR drafted end to end by a model.A decision record for a decision nobody made.The human writes context, decision and accepted cost; AI may draft the options section.
A production log pasted into a prompt.Customer data leaves your boundary irreversibly.Redact first; keep structure and error, drop identifiers and payloads.
A confident claim about a vendor’s API.A design built on a guarantee that does not exist.Open the vendor documentation yourself before it enters a document.
Four engineers, four private prompt styles.Four different architectures arriving in one codebase.Shared boundary spec in the repository; the same spec in every prompt.
A generated user persona or quotation.Fabricated evidence with the shape of research.Mark it a hypothesis; talk to three real users this week.
Review time falling as generation rises.The gate moves from your judgment to the generator’s defaults.Cap the size of a reviewable change; specify structure up front.
What people get wrong after reading this
“If I review carefully enough, generation volume doesn’t matter.”
Review is linear in the amount of code and your attention is not. Past some volume, careful review is a claim rather than a practice. The scalable lever is specification — boundaries, ports and invariants in the prompt — because it removes whole categories of finding from every future review at a fixed cost.
“Generated tests prove the generated code is right.”
They prove it does what it does. A test written from the same misunderstanding as the implementation passes cheerfully. Tests you care about come from examples of the rule — and the negative acceptance cases in supplement 4 are ones a model will not think to write unless you ask.
“Anonymizing means removing names.”
A schema with renamed tables can still identify a company by its structure, and an incident timeline with names stripped can still identify a customer by its particulars. Anonymize by rewriting to the forces — two teams, different rhythms, a shared order-of-record — which is also what makes the question answerable.
“It cited a source, so it is verified.”
A citation is a claim like any other until you open it. This applies to citations that turn out to exist, too: existing and saying what was claimed are different properties.
“Adversarial prompting means asking it to find problems.”
Asking for problems produces a plausible list. Asking for enumerated sequences — every ordering that reaches an illegal state — produces something you can check one item at a time. Demand mechanism, and the fabrication becomes visible because a fabricated mechanism does not survive being traced.

When this is the wrong move

Skill: keep the judgment

Reviewing generated code, the first two things to check are:

A model states a delivery guarantee for your carrier’s API. You should:

Which decision should never be delegated to a generated artifact?

Practice: set your team’s working agreement

  1. Run the protocol on real output. Take a generated change merged in the last two weeks. Walk the six steps in order and record what each one finds. Then check: would the change have been rejected at step one or two if anyone had looked?
  2. Write the boundary spec once and put it in the repository — layers, ports, forbidden imports, the invariants that must be enforced by objects rather than by callers. Every engineer pastes the same paragraph into every prompt. Four private prompt styles produce four architectures.
  3. Fill in the context classification table for your organisation, in three columns, with your actual tool and plan named. Check it against the real policy rather than assuming. Publish it where people work, not in a wiki nobody opens.
  4. Write your decision-ownership table. Eight to ten rows. For each: what AI may do, the named human owner, and the evidence required. Make sure the “draft freely” row is visibly the largest category.
  5. Run one adversarial session against a design you already believe in, using the state-machine or hostile-consumer prompt. Sort every item into checkable / hypothesis / assertion. Fix the checkable ones today; put the hypotheses on the assumption map with a test.
  6. Audit for fabricated evidence. Look through your initiative’s documents for any user quotation, statistic, or vendor guarantee whose source you cannot name in ten seconds. Either source it or delete it. Both outcomes are wins.
Teach it back
  1. What is currently your quality gate on generated code — honestly, not aspirationally? If it is “the tests pass,” whose defaults are now setting your architecture?
  2. Which decision on your current initiative arrived as a default in generated code rather than as a choice someone made?
  3. What is the most sensitive thing you have pasted into a prompt this month, and what would it have cost to anonymize it first?
  4. Name one claim in your design documents that came from a model and has never been verified. What would checking it take — and why has nobody?
Reveal: the ownership test

One question settles almost every case: if this turns out to be wrong in six months, who explains it, and can they?

“The model suggested it” is not an explanation any organisation accepts, and it is not one you would accept from someone else. So the practical rule follows directly: you may delegate the drafting of anything you could defend on your own, and nothing else. That line moves as your judgment grows, which is the right way round — the tool amplifies whatever review capability you already have, and amplifies zero to zero.

Note what this does not say. It does not say generate less. Inside a boundary you chose, against invariants you specified, reviewed by a protocol you run every time, you should generate as much as you can review — and spend the time you saved on the users, the domain and the production evidence that no generator has access to.

Your win

You can run a six-step structural review on generated code and name what each step catches, classify what may enter a prompt and anonymize the rest to its forces, publish a decision-ownership table that makes the fast path obviously safe, drive adversarial sessions that produce checkable enumerations rather than plausible lists, and hold the line that AI artifacts never substitute for user, domain or production evidence.

Read deeper

Ask your agent-teacher to run the six-step protocol on its own last output, then tell you which step it would have failed if you had not specified the boundary — and make it point at the line.