Prompts Are Contracts: Bringing Software Engineering Discipline to Prompt Design
This piece is written for software engineers who already know how to design a function around a contract. By the end, you will have a concrete way to treat a prompt as a typed function rather than a piece of freeform writing you keep tweaking until it feels right.
The core claim is simple. What separates prompt engineering projects that keep breaking from projects that hold up under real traffic is not model quality. It is whether a contract exists at all. Without three layers of discipline applied to the prompt itself, an input contract, an output contract, and a change-history contract, even the strongest model will wobble once it hits production.
Many teams still treat prompts like marketing copy: edit sentences until the output feels right, ship when the demo looks convincing, then go back to tweaking wording whenever something odd shows up later. That approach survives the demo stage. The moment user volume grows and inputs diversify, it starts producing failures that never reproduce the same way twice.
A visual metaphor for the article’s key idea.
Reframing the Prompt as a Function
When a software engineer designs a function, the first questions are always the same. What input does it accept, what does it return, and what side effects does it have. A prompt has the exact same shape underneath. It is a function call handed to a language model, where the context is the input and the generated text is the output. The moment you see it that way, writing a prompt stops being an act of intuition and becomes an engineering task with the same rigor as writing a function signature.
Think about what makes a function signature good. The name is unambiguous, and the contract between input and output is explicit. If you were writing a function to extract people mentioned in a document, the name alone should tell you what it does, and the type hints alone should tell you what goes in and what comes out. A prompt needs the same explicitness. If the instruction never states the shape of the input, the expected shape of the output, and the rules for edge cases, the model fills those gaps differently every time it runs.
The stakes become obvious in a failure case. A prompt that simply says “extract the important information from this text” leaves everything up to the model’s discretion: what counts as important, what format the answer should take, what to do when nothing relevant is found. Discretion is another word for variance; send the same request ten times and you can get ten differently shaped responses back. Constrain the input to a defined scope and pin the output to a specific schema, and the number of blanks the model has to improvise shrinks dramatically.
Side effects deserve the same treatment they get in code. A function that touches external state has to document that behavior, and a prompt that calls a tool, references prior context, or enforces a specific tone needs those conditions written into the contract too. Leave it as tribal knowledge one person carries around, and the next engineer who edits the prompt will break it without realizing.
The practical payoff is that it makes collaboration possible. Once a prompt is treated as a function contract, it can be reviewed the way code is reviewed, and another engineer editing it later can see exactly what has to be preserved just by reading the contract.
The Input Contract: What Few-Shot and Chain-of-Thought Actually Do
How you feed examples to a model has an outsized effect on output quality. Few-shot prompting is a systematic way to exploit that effect, and chain-of-thought prompting is a way to steer the reasoning process itself. Restating both in contract language makes the distinction much sharper. Few-shot is a contract over the shape of the output. Chain-of-thought is a contract over the procedure of reasoning.
Zero-shot prompting asks for a result using instructions alone, and the model draws entirely on patterns it already learned during training. Adding even a handful of examples changes the dynamic completely. An example is not decoration; it is a literal contract showing the relationship between an input and the output that should follow it. In a sentiment classification task, one positive example and one negative example is enough for the model to read the pattern and apply the same rule to a new input. The more accurate the examples, the more consistently accurate the output becomes.
Picking examples well requires a few disciplines. Examples need to represent the edge cases of the task, not just the easy ones; if every example is clear-cut, the model fails exactly where the task actually gets hard. Mixing in ambiguous phrasing and genuinely debatable labels is what makes the contract hold up in the real world. The count matters too: too few and the pattern never fully lands, too many and the model starts memorizing examples rather than generalizing a pattern, which backfires on new inputs. Three to eight examples is a reasonable starting point for most tasks.
Chain-of-thought solves a different problem. Where few-shot teaches the shape of the output, chain-of-thought teaches the path that leads to it. Asking the model to reason step by step causes it to generate the intermediate logic alongside the final answer instead of jumping straight to a conclusion. The difference matters most in arithmetic and multi-step judgment tasks. Ask for the final answer only, and the model can skip straight to something closer to pattern matching, which measurably raises the error rate. Ask it to lay out the reasoning first, and it will sometimes catch its own mistake before committing to an answer.
Framing both techniques as contracts clarifies how to prioritize fixes. If output formatting keeps drifting, strengthen the few-shot examples. If reasoning accuracy keeps drifting, add chain-of-thought. Without that distinction, teams throw both techniques at every problem indiscriminately, the prompt balloons in length, and nobody can tell which change moved the needle.
The Output Contract: What Structured Output Gives You and What It Does Not
A prompt result that comes back as free-flowing natural language is pleasant for a human to read and a headache for a program to parse. Forcing the output format eliminates that pain and makes downstream integration far more stable. Most major model APIs now offer a way to force output into JSON, and once that mode is enabled, the model follows the specified structure.
Understanding exactly what the schema is doing here matters. A schema is a contract on the shape of the output, not a contract on its content. Define a schema for extracting people with fields for name, title, and organization, and the model will not deviate from that structure. But nothing in the schema stops it from inventing a title that never appeared in the source text, or attaching a person to the wrong organization entirely. A response can pass every structural check and still be factually wrong, and that wrong response flows straight into the next stage of the system unless something else catches it.
Missing that distinction leads to a common mistake. Teams adopt structured output, watch parsing errors disappear, and conclude validation is no longer necessary. Parsing success means the JSON syntax was valid; it says nothing about whether the values inside are correct. In production, the failures that hurt are almost never syntax errors but semantic ones: a field fabricated out of nothing, a number silently altered instead of rounded correctly, or a list padded to avoid returning empty when empty was the honest answer. All of these pass schema validation without any trouble.
Teams working with structured output need two layers of validation, not one. The first is schema validation, a mechanical check that field names and types match. The second is content validation, a check that values are grounded in the source input. For people extraction, confirming that an extracted name literally appears in the source text as a string match catches a meaningful share of fabricated values. Confuse these two layers and you ship schema-valid nonsense straight to the user.
Honoring the output contract ultimately requires a division of labor: let the model own the shape, and let code own the verification. Models are strong at generating text that sounds plausible and weak at guaranteeing deterministic rules hold. Code has the opposite profile. Putting each strength where it belongs is what turns structured output from a demo feature into something production can depend on.
Where the Contract Quietly Breaks
Honor both the input contract and the output contract, and there are still places where the whole arrangement can silently fall apart. The most common one is a model version change. Drop the same prompt into a new model version unchanged, and implicit behaviors the old version respected are not guaranteed to survive. A prompt is, to some degree, tuned to the quirks of a specific model, so swapping the model deserves roughly the weight of swapping a runtime, not the weight of a cosmetic tweak.
The second failure point is a slow drift in input distribution. The example text used when a prompt was first designed and the text actually arriving in production months later can diverge without anyone noticing in real time. As the user base widens or documents get longer, few-shot examples that were representative on day one gradually stop being representative at all. This kind of decay does not announce itself with a spike; it erodes accuracy slowly, which is exactly what makes it hard to catch.
The third failure point is uncoordinated editing. One engineer tightens a sentence, another adds an example, a third tweaks the output format, and if none of that goes through review, the original contract becomes impossible to reconstruct. In code, a compiler or a test suite flags this kind of drift immediately. A prompt is natural language, so it always looks syntactically valid no matter how far it has drifted, and that surface-level normalcy is exactly what makes the risk easy to miss.
All three failure points share one trait: the problem accumulates quietly instead of surfacing the way a broken build does, which is exactly why prompt engineering needs mechanisms for tracking changes and measuring results built in from the start.
Why Prompts Need Version Control Just Like Code
Prompts need a change history for the same reasons code does: trace how results shift across versions given the same input, roll back to a previous version when something breaks, and let anyone on the team check what version is currently live. Managing prompt strings in a version control system instead of a database or a loose config file lets a team reuse the exact same workflow it already trusts for code, with changes landing as commits and getting reviewed through pull requests.
In practice, the cleanest approach stores prompts as templates, with placeholders filled in with real values at runtime. Separating the instruction from the parameters means that when a change history is reviewed later, it is immediately clear whether the instruction itself changed or only the values passed through it changed. Without that separation, a shift in output behavior leaves you guessing whether the cause lives in the wording or in the data.
Recording what changed, why it changed, and how accuracy or consistency moved afterward makes root-causing and rolling back far easier. The record does not need to be elaborate; a few lines noting what problem a version fixed and how a metric moved is enough to stop the next person from repeating the same trial and error. Skip it, and the same mistakes get made over and over by different people on the same team.
Version control earns its keep most visibly when a model is being swapped or upgraded, since implicit behavior tends to shift along with the model version. With version control in place, adjusting a prompt for a new model becomes a safe experiment rather than a leap of faith: the existing version stays untouched, a new version is built alongside it for comparison, and the switch happens only once the new version has demonstrably earned it.
Version Control Without Evaluation Is Just Bookkeeping
Version control alone is not enough. Recording versions without a way to measure the quality of each one leaves no basis for deciding which one is actually better. Prompts need tests just like code does. The most basic test measures accuracy against a labeled dataset of inputs and expected outputs prepared ahead of time. That dataset does not need to be exhaustive from day one; adding tricky cases pulled from real production traffic over time is how it earns representativeness.
A few other metrics matter alongside accuracy. Consistency measures whether the same input produces the same output across repeated runs; because models generate answers probabilistically, some variance is expected, but when it grows too wide the feature starts feeling unreliable. Structural conformance checks whether the output actually respects the requested shape, essentially the automated version of the schema and content validation covered earlier.
Watching all three metrics together narrows down root causes far faster than watching any one alone. Low accuracy paired with high consistency means the prompt is consistently steering the model in the wrong direction, pointing squarely at the instruction itself. High accuracy paired with low consistency points instead at probabilistic variance, which calls for adjusting temperature or retry strategy rather than rewriting the instruction. Without separating these signals, teams chase the wrong fix and burn time on it.
Wiring this evaluation into the deployment pipeline gives prompt changes the same safety net code changes already get: score a new version against the labeled dataset before it ships, and block deployment if the metrics regress. At that point, editing a prompt stops being a gamble on gut feeling and becomes measurable, incremental improvement.
What to Change Today
The three layers covered here fit together as follows. Treat the prompt as a function with an explicit input and output range, strengthen the input contract with few-shot and chain-of-thought, honor the output contract with structured output plus content validation, and manage the change-history contract with version control and evaluation. These pieces stack in sequence; none is optional if the goal is a prompt that survives contact with real traffic. Honor the output contract without an input contract and you accumulate responses that are correctly shaped but wrong. Honor both without version control and you have no way to explain why quality dropped since last week.
The smallest thing worth doing today is picking one prompt currently in production and rewriting its input and output explicitly. Write out, line by line, exactly what should come in and exactly what should go out, and it becomes obvious how many blanks were quietly left up to the model’s discretion. Filling in those blanks one at a time is, in essence, what prompt engineering is.
None of this is a novel idea. It is simply the discipline software engineers already apply to designing, testing, and versioning functions, carried over unchanged to prompts. The trap is assuming that because a prompt is natural language, this discipline is optional. It is the opposite: because it is natural language, skipping the contract means nobody, including the person who wrote it, ends up knowing where its boundaries actually are.
For deeper worked examples and the full code, the companion ebook is available in full.
Chapter Illustrations
