Prompts as Specifications
What is precise speech?
Chapter 10 — Prompts as Specifications
“Let your speech always be gracious, seasoned with salt, so that you may know how you ought to answer each person.” — Colossians 4:6
“This is the chief article from which all our doctrine flows.” — Smalcald Articles, Part II, on the article of Christ’s office, framed precisely so later generations cannot mistake it
Why This Matters
Last week the rule was: write a spec, hand it to the AI, review what comes back. You discovered something most engineers learn the slow way: the quality of what comes back is bounded by the quality of what you sent in.
This week we make that observation operational.
A prompt is a specification. Not “kind of like” a specification. It is the specification — the one the AI will actually read. Every word in your prompt either narrows the space of acceptable outputs or fails to. Vagueness is not “leaving room for creativity.” Vagueness is handing the AI the decision about something you, the senior, should be deciding.
The chapter is structured around a single claim: precision in prompting is the same skill as precision in spec-writing, and it is the highest-leverage skill in the second half of this book. Five extra words in the right place in your prompt can save you two hours of reviewing wrong code. Five missing words in the wrong place can produce code that compiles, passes one test, and is silently wrong on the input that matters.
You are about to spend a week doing what the church has spent two thousand years doing: hammering out precise speech under pressure. The church’s word for it is “confession.” Yours, for this chapter, is “prompt.”
Coach’s Note — The single biggest predictor of a Phase 2 student’s grade is not which AI they use. It is whether their prompts are sharp. The Cs use AI casually and get casual code. The As prompt like they’re writing a spec — and the AI gives them what a spec asks for. The model doesn’t change. The prompts do. Make yours sharp this week.
10.1 — A Prompt Is a Specification
Recall Chapter 2 of this book. A specification is a written promise about behavior: this is what the method takes; this is what it returns; this is what happens on bad input; this is what is true before and after the call. A spec is information given to a reader so that the reader knows what to build.
A prompt is exactly that, with one twist: the reader is the AI, and the AI cannot ask follow-up questions in the way a human reviewer would. If your spec is ambiguous, a human teammate will message you on Slack. The AI will not. The AI will pick a reading of the ambiguity, often a plausible one, often the wrong one, and ship code based on that reading. The bug you find an hour later is the cost of the question that didn’t get asked.
So the central move of this chapter: before you press send on a prompt, read it as if you were the AI, and ask yourself what an honest reader could still get wrong. Every “could still get wrong” is a sentence to add.
The four parts of a sharp prompt
A prompt that produces good code almost always has these four parts. Sometimes implicitly, but explicit is better.
-
The role and the context. “You are writing a Java 17 method for me. The project uses only the standard library.” One or two sentences. Sets the frame.
-
The signature. The exact method signature you want, or the exact class shape. No prose paraphrase. The signature is the contract — write it.
-
The behavior. What the code does. What it returns. What it does on bad input. Edge cases. Examples with concrete inputs and outputs.
-
The constraints. What the code must not do. Libraries it should not use. Patterns it should not follow. Length limits. Style requirements.
A four-part prompt is dramatically more reliable than a one-part prompt. The next sections take each part in turn.
10.2 — Signature-First Prompting
The single most effective change you can make to your prompts: start with the method signature. Not a description of the method. The signature.
Bad — prose first:
Write a Java method that takes a list of integers and returns the average.
Good — signature first:
Implement this Java 17 method. Use only
java.utilandjava.lang./** * Returns the arithmetic mean of the given list. * @throws IllegalArgumentException if {@code nums} is null or empty. */ public static double average(List<Integer> nums)
The second prompt has already made every decision the first prompt left to chance:
- It’s
double, notDouble(no boxing). - It takes a
List<Integer>, not anint[]. - It’s a static method, not an instance method.
- It throws on null/empty, doesn’t return
0.0orNaN. - It uses only
java.utilandjava.lang, not some streaming library.
The AI is now constrained to write exactly the method you wanted. Every word in the signature is doing work.
Coach’s Note — When you find yourself prompting and getting “almost right” answers, the fix is almost always to give the AI the signature instead of describing it. Type the signature. Hand it over. The AI is much better at filling in a body than at inferring a signature.
10.3 — Specifying By Example
Examples are concrete specifications. Where a sentence is ambiguous, an example is not.
Compare:
Write a method that capitalizes the first letter of each word in a string.
vs.
Write a Java 17 method
public static String titleCase(String s). Examples:
titleCase("hello world")→"Hello World"titleCase("a")→"A"titleCase("")→""titleCase("hello world")→"Hello World"(preserve internal spacing)titleCase("HELLO WORLD")→"Hello World"(lowercase the non-first letters)titleCase(null)→ throwsIllegalArgumentException
The second prompt removes six different ambiguities that the first prompt left to the AI’s guess. Each example is worth a paragraph of prose. Where you can give an example, give it.
A real example is even better than a hypothetical one. If the function you’re prompting for is going to process strings from your existing codebase, paste a real string from your codebase in the prompt. The AI’s mental model of “what kind of input this is” tightens immediately.
10.4 — Negative Constraints
Half the bad AI output you see is the AI adding things you did not ask for. Helper classes you don’t need. A logging framework. A defensive check the spec didn’t require. An abstraction layer no one will ever use.
The fix: tell the AI what not to do. Negative constraints are at least as powerful as positive ones. A list of three or four well-chosen negatives will dramatically tighten the output.
Examples of useful negative constraints:
- “Do not add any helper methods. Everything goes in the one method above.”
- “Do not catch exceptions. Let them propagate.”
- “Do not use any third-party libraries —
java.utilandjava.langonly.” - “Do not add a
mainmethod or a usage example.” - “Do not add Javadoc unless I asked for it.”
- “Do not refactor any of the code I’m pasting; only implement the one new method I’m asking for.”
- “Do not use streams. Use a plain
forloop.”
That last one is taste — sometimes you want a Stream.collect(...), sometimes you want a 4-line loop. When you have an opinion, state it as a negative constraint. The AI complies.
10.5 — The Test-First Prompt
The single most effective prompt format you can learn this week:
Here is a failing JUnit 5 test. Produce a Java method that makes it pass.
@Test void titleCasePreservesInternalSpacing() { assertEquals("Hello World", TextUtil.titleCase("hello world")); } @Test void titleCaseRejectsNull() { assertThrows(IllegalArgumentException.class, () -> TextUtil.titleCase(null)); } @Test void titleCaseHandlesEmpty() { assertEquals("", TextUtil.titleCase("")); }The class is
TextUtil. The method should bepublic static String titleCase(String s). Use onlyjava.utilandjava.lang. Do not add helper methods.
The test-first prompt is so effective because it removes a whole layer of guesswork. The AI does not have to interpret your description of behavior; it can read the assertions and produce code that matches. The contract is in machine-readable form.
This is also the format that connects most directly to Chapter 4’s testing discipline. You already know how to write tests. Phase 2 turns the test-writing skill into a prompting skill. Same artifact, twice the value.
Coach’s Note — When you find yourself going back-and-forth with the AI more than three times on a single function, stop. Write a JUnit test that captures what you want. Send the test. The next response will be dramatically more on-target. Tests are the highest-bandwidth specification you can give.
10.6 — Context Windows: How Much to Include
A common new-engineer mistake: pasting the entire 800-line codebase into a prompt so “the AI has full context.”
A common opposite mistake: pasting a 12-line snippet and asking a question that requires understanding the surrounding 50 lines.
The right answer is in the middle, and it depends on the question.
Rules of thumb
| Question type | What to include |
|---|---|
| ”Implement this method, given its signature and spec” | Just the signature, the spec, and the imports the method will need |
| ”Why does this test fail?” | The test, the method under test, and the error/stack trace |
| ”Refactor this class” | The whole class |
| ”Add a new method to this class that uses existing private helpers” | The whole class (the AI needs to see the helpers) |
| “How do I use this third-party API?” | A link to the docs, or the API’s relevant signatures, plus your call site |
The most common rule: paste exactly enough context that a careful human reader could answer the question. Less than that, and the AI guesses. More than that, and the AI gets distracted by irrelevant details and sometimes “fixes” them.
When to give a whole file vs a snippet
Whole file when:
- The AI needs to see helpers, fields, or imports that the snippet doesn’t show.
- You want consistency with conventions used elsewhere in the file.
- The change spans multiple methods.
Snippet when:
- The change is local — one method, one block.
- The surrounding code is irrelevant to the question.
- You’re testing the AI’s understanding of a specific construct.
When in doubt: paste the whole file. It is cheaper to give too much context than too little, as long as you say which part you want changed. “Here is the full file. Modify only parseHeader.”
10.7 — Iterating on the Prompt, Not Just the Response
When the AI’s first response is wrong, you have two options:
- Iterate on the response. “That’s wrong because X. Fix it.”
- Iterate on the prompt. Edit your original prompt to be sharper, and re-send.
Beginners reach for option 1 almost every time. It is usually the worse choice.
Option 1 produces a patch on top of a flawed first answer. The AI’s second answer often contains the bug from the first answer in a buried form, because the AI is conserving the structure of what it just generated. You end up debugging both the original code and the patch.
Option 2 — editing the prompt itself — produces a clean second attempt. The AI starts over with better information. The new answer is almost always better than a patched answer.
The discipline:
- If the AI’s mistake suggests the spec was unclear, go back and fix the spec. Re-prompt with the updated version. Don’t patch.
- If the AI’s mistake was a hallucinated API or a typo, patch. That’s just an error correction.
- If the AI’s mistake suggests it didn’t understand the constraint, make the constraint louder. Re-prompt with the constraint listed three times in three places.
A clean prompt iteration is faster than two rounds of patching. Almost always.
10.8 — Two Prompts, Two Outcomes — The Worked Example
This is the project this week, but the pattern is worth walking through here.
Problem: validate that a string is a valid credit card number using the Luhn algorithm. (For non-spec-curious readers: Luhn is the checksum algorithm every credit card uses. You double every other digit from the right, subtract 9 if the doubled digit is ≥ 10, sum everything, and check if the result is divisible by 10.)
Prompt A — vague
Write code that checks credit card numbers.
What this typically produces (download CreditCard.java):
// VAGUE-PROMPT OUTPUT — half-right, riddled with assumptions
public class CreditCard {
public boolean isValid(String number) {
if (number == null) return false;
number = number.replaceAll(" ", "").replaceAll("-", "");
if (number.length() < 13 || number.length() > 19) return false;
// ... Luhn check ...
int sum = 0;
boolean alternate = false;
for (int i = number.length() - 1; i >= 0; i--) {
int n = Character.getNumericValue(number.charAt(i));
if (alternate) {
n *= 2;
if (n > 9) n = (n % 10) + 1;
}
sum += n;
alternate = !alternate;
}
return sum % 10 == 0;
}
}
What’s wrong with this output?
- It’s an instance method on a class, not a static utility. The AI guessed.
- It returns
falsefornull, instead of throwing. The AI guessed. - It silently strips spaces and dashes. Was that what we wanted? The AI guessed.
- It enforces 13–19 length. Real cards are 13–19 digits, so that’s almost right, but the spec didn’t say so. The AI guessed.
- It uses
Character.getNumericValue, which returns negative numbers for non-digit characters — but the code doesn’t check whether the character is a digit first. Subtle bug. - It has no tests.
- It has no Javadoc.
The code roughly works. It is also five decisions the senior didn’t make.
Prompt B — precise
Implement this Java 17 method. Use only
java.utilandjava.lang. No instance methods, no helper classes.public final class Luhn { /** * Returns true if the given string is a valid credit card number per the Luhn checksum. * * @param number a string of digits only — no spaces, dashes, or other characters. * Length must be between 12 and 19 inclusive. * @throws IllegalArgumentException if {@code number} is null, contains non-digit * characters, or has length outside [12, 19]. */ public static boolean isValid(String number) { ... } }Examples:
Luhn.isValid("4111111111111111")→true(a known-good test card)Luhn.isValid("4111111111111112")→false(last digit wrong)Luhn.isValid("411111111111")→ throws (too short)Luhn.isValid("4111-1111-1111-1111")→ throws (contains dashes — caller is responsible for normalization)Luhn.isValid(null)→ throwsAlgorithm: starting from the rightmost digit, double every second digit. If a doubled digit is ≥ 10, subtract 9 (equivalent to summing its digits). Sum all digits (doubled and not). The number is valid iff the sum is divisible by 10.
What this typically produces (download Luhn.java):
// PRECISE-PROMPT OUTPUT — every decision was made by you, not by it
public final class Luhn {
private Luhn() {} // utility class
public static boolean isValid(String number) {
if (number == null) {
throw new IllegalArgumentException("number must not be null");
}
int len = number.length();
if (len < 12 || len > 19) {
throw new IllegalArgumentException("length must be in [12, 19], got " + len);
}
int sum = 0;
boolean doubleIt = false;
for (int i = len - 1; i >= 0; i--) {
char c = number.charAt(i);
if (c < '0' || c > '9') {
throw new IllegalArgumentException("non-digit at position " + i + ": '" + c + "'");
}
int d = c - '0';
if (doubleIt) {
d *= 2;
if (d >= 10) d -= 9;
}
sum += d;
doubleIt = !doubleIt;
}
return sum % 10 == 0;
}
}
Same problem. Same AI. Substantially better code. The difference is not the AI’s effort; the difference is the spec the AI was given.
Notice especially:
- The class is
finalwith a private constructor — a utility class, as the prompt implied. - All edge cases are explicit throws with informative messages.
- The non-digit check happens before the digit arithmetic, fixing the subtle bug in Prompt A’s output.
- No silent normalization. The caller is responsible. The spec said so.
- The single helper,
c - '0', is direct ASCII arithmetic — faster and safer thanCharacter.getNumericValue.
This is what Project 10 trains: feel the difference between Prompt A and Prompt B, on a problem you choose, in your own hands, with your own tests.
10.9 — Prompt Templates Worth Memorizing
After enough reps you accumulate a small library of prompt templates that work. Here are five worth memorizing this week.
Template 1 — Implement-to-signature
Implement this Java 17 [method/class]. Use only [allowed packages]. Do not add [things you don’t want].
[signature with Javadoc]
Examples:
- [input] → [output]
- [input] → [output]
Template 2 — Test-first
Here are failing JUnit 5 tests. Produce a Java method that makes them all pass.
[tests]
The class is
X, the method ispublic static T method(...). Use only [allowed packages].
Template 3 — Refactor
Refactor this Java 17 [method/class]. The behavior must not change. The existing tests must still pass. Goals: [specific goals — readability, fewer lines, no streams, no nested ternaries, whatever].
[code]
Do not add functionality. Do not rename public methods.
Template 4 — Explain
Explain this Java 17 [code/method/error] to me in 3–5 sentences. I am a Java engineer comfortable with the standard library; assume I know the basics.
[code or error]
Template 5 — Find-the-bug
Here is a Java method with at least one bug. Find every bug, classify it (correctness / null-handling / performance / hallucinated API / other), and produce a corrected version.
[code]
Do not refactor for style. Only fix actual bugs.
These five templates cover roughly 80% of the AI prompting you will do in this course and in your first year on a real codebase. Internalize them now.
10.10 — Confessions as Precise Speech
When the church needed to draw a line — “this is what we mean, and this is what we don’t” — it wrote confessions. The Apostles’ Creed. The Nicene Creed. The Athanasian Creed. Then in the Reformation: the Augsburg Confession (1530), the Apology, the Smalcald Articles, the Small and Large Catechisms, the Formula of Concord. The Lutheran tradition you are studying under was born out of a century of hammering precise speech under pressure.
What is a confession? It is a paragraph (or a hundred paragraphs) of language so deliberately chosen that, four centuries later, a reader can still tell exactly what the writer meant. It is the opposite of the casual sermon, the blog post, the off-the-cuff tweet. It is what you write when you know that ambiguity later means schism later, and you are trying very hard not to cause a schism.
Read the opening of the Athanasian Creed sometime. “And the catholic faith is this, that we worship one God in Trinity, and Trinity in Unity, neither confounding the persons nor dividing the substance.” Every word does work. Strip one out and the sentence either becomes wrong or becomes vague enough to be wrong. The writers knew this. The writers agonized over each clause.
A prompt to an AI is, in scale, nothing like the Athanasian Creed. In kind, it is the same activity: language deliberately constructed so a careful reader produces a particular result, and a careless reader cannot mistake it.
The discipline transfers. Here is what the confessional tradition teaches the engineer who is about to write a prompt:
- Words land or they don’t; choose ones that land. “Validate” is not the same as “reject.” “Return” is not the same as “throw.” Pick the one you mean.
- Negation matters as much as affirmation. The Augsburg Confession spends as much energy on “and we reject this” as on “and we affirm this.” Your prompts should too — the negative constraints from §10.4 are this discipline in software shape.
- Examples are not decoration. When the Small Catechism explains a commandment, it gives examples of what it means to keep it and break it. Examples are what turn a principle into something a reader can act on. Same with prompt examples — they are not “extra”; they are the spec made concrete.
- Test it against the worst reader. The Lutheran confessors knew their writing would be read by hostile critics looking for ambiguity to exploit. They wrote to withstand that reader. Your prompt is going to be read by an AI that picks the most-likely interpretation, not the most-careful one. Write to withstand it.
- Don’t add what isn’t needed. Confessions are dense, not bloated. Each clause earns its place. Prompts, too — every sentence should narrow the output. If a sentence doesn’t, cut it.
A casual confession is not a confession; it is a fog. A casual prompt is not a prompt; it is a wish. Christians have known this about language for a long time. The engineering tradition is catching up.
Coach’s Note — The next time you find yourself typing a prompt that “kind of describes what I want,” stop and ask: would I sign this if it were a confession? If the answer is no, you have not yet specified the work. Spend the next two minutes making it sign-able. Send it after that.
10.11 — When to Stop Prompting
The discipline of prompting includes the discipline of stopping. Some signs you should put the prompt window down:
- You’ve sent the same prompt three times with three different phrasings. The AI is not learning; you are.
- The AI’s last three answers are slight variations of each other and none are right. You have hit a local maximum the AI cannot climb out of.
- You realize you don’t actually know what the right answer is. The AI definitely doesn’t, then.
- You’ve been at it for 30 minutes and haven’t written a line of code yourself.
When any of those land: close the AI window. Write the method by hand. It will take 10 minutes. It will work. This is also senior judgment.
Project 13 (two chapters from now) will train the opposite skill — driving the AI to green when you can see the path. But knowing when not to prompt is part of the same discipline. Senior engineers reach for the AI when it will save them time and reach for their own hands when it will not. Beginners reach for the AI by default. Become the former.
10.12 — Common AI Pitfalls (Week 10 Edition)
Pitfall: Your prompt is one sentence long. What’s happening: You are leaving every decision to the AI. The AI will guess. Some of the guesses will be wrong. Fix: Use the four-part structure from §10.1. Role + signature + behavior + constraints. Aim for at least 10 lines.
Pitfall: You included a signature, but the prompt also has loose prose (“make it robust,” “handle edge cases nicely”). What’s happening: You’re contradicting yourself — the precise signature says one thing, the vague prose invites the AI to add unspecified behaviors. The AI will follow the vague prose. Fix: Replace “handle edge cases nicely” with a list of the specific edge cases and the specific desired behavior on each.
Pitfall: You asked the AI for “a working example” of using some API.
What’s happening: “Working example” is an invitation to a 50-line demo with a main, error handling, comments, and a usage README. You wanted three lines.
Fix: Specify the exact size. “A three-line example that does X and assumes everything is already set up.”
Pitfall: The AI’s first response was 80% right. You patched it by replying “fix the X part.” You got back the X fix plus subtle changes elsewhere you didn’t ask for. What’s happening: The AI conserves the structure of its prior response, but it does also drift. Long conversation threads compound the drift. Fix: Iterate on the prompt, not the response (§10.7). Send the full updated prompt in a fresh thread for cleaner output.
Pitfall: You pasted your entire 1200-line file to ask about one method. What’s happening: The AI is now distracted by other parts of the file. It might “helpfully” refactor unrelated code, or get confused about which method you’re asking about. Fix: Paste only what is needed. Be explicit about which part you want changed.
Pitfall: You used the same prompt template across five different problems and got mediocre output on three of them. What’s happening: A template that works for “implement a method” doesn’t necessarily work for “refactor a class” or “explain code.” Pick the right template for the task (§10.9). Fix: Match the template to the task. There is no one universal prompt.
10.13 — Reps
Open the exercises for the full set. They are heavily prompt-centric this week — you will be writing and sending real prompts, comparing responses, and iterating.
Rep 1. Take a vague prompt and rewrite it as a four-part precise prompt. Send both. Diff the outputs.
Rep 4. Send a test-first prompt for a method you already know how to write. Confirm the AI produces code your tests pass.
Rep 9. Build one of the five prompt templates into a personal cheat-sheet you can paste from.
Full set in the exercises.
10.14 — This Week’s Project: Two Prompts, Two Outcomes
You’re ready for Project 10: Two Prompts, Two Outcomes, in Project 10.
The setup: pick a problem of moderate complexity. Write two prompts for it — one deliberately vague, one tightly precise. Run both. Submit both responses, both code samples (with tests for each), and a written one-page comparison. The comparison is the real deliverable. The code is a means to an end.
Three tiers:
- Normal — vague vs precise on one problem.
- Medium — add a test-first prompt as a third option, compare all three.
- Hard — build a prompt library: five reusable templates with documented use cases.
10.15 — Coach’s Final Word
If Chapter 9 taught you the senior/junior model, Chapter 10 teaches you the single most leveraged tool the senior has: the prompt itself. Sharp prompts produce sharp code. Vague prompts produce vague code. The model is the same in both cases.
For two weeks now you have been building a skill the broader internet does not have. Most people will continue to chat with AI casually and get casual results. You are learning to write a prompt the way Christians have written confessions for two thousand years: deliberately, under pressure, with every word earning its place. That skill will make the difference between your code and theirs. It already does.
One more pivot ahead. Chapter 11 turns from “the prompt I send” to “the code that comes back” — code review as a discipline. The two skills together are the senior’s two hands. You need both.
See you Monday. Sharpen your prompts.
Up next: Read the exercises — many reps require you to actually send the prompts and compare outputs. Then open Project 10. After that, Chapter 11 — Code Review.