Everything interesting in software happens at a boundary

Drawing sheet 008 of the set At the Boundary: five boundaries, one rule.

The worst bug I ever found was not inside the code. It was on the line between two pieces of code that both worked.

A rider's app reported how far it had travelled. The server took that number and priced the delivery. Each side did its job. The app measured; the server billed. Nobody wrote a bad function. The bug lived in the handshake, where a claim from one side was treated as a fact by the other. One rider noticed, and billed accordingly.

Seven years of shipping production systems have taught me that this is not a story about one bug. It is the shape of nearly every serious bug I have shipped or caught. The middle of the code is fine. The trouble lives at the boundary.

What a boundary is

A boundary is any line where data changes hands. A request enters your server. A document enters your parser. A prompt leaves for a language model. A dependency enters your build. A block of text leaves your editor for a chat window.

On one side of the line is something you do not control: a user, a file, a vendor, a model, a package maintainer. On the other side is code you trust, because you wrote it and tested it.

Everything that crosses the line arrives as a claim. The rider's app claims a distance. The PDF claims to be a document. The package claims to be maintained. The boundary's whole job is to turn claims into facts, or to refuse them. When the boundary does not do that job, the trusted code inside does exactly what it was told, with data it should never have believed.

That is why the middle of the code is so often fine. The function that computes a delivery fee from a distance is correct. It was simply handed a lie.

Five boundaries I meet in every system

I have built a marketplace, a reading app, a redaction engine and a tool that finds conversations. Five boundaries show up in all of them. Each has its own way of failing and its own move that closes it.

1. A request enters your server

The client sends numbers. Some of them decide money: a distance, a quantity, a total, a rate. The failure is trusting a number from the party who benefits from it. The rider who reported 50 kilometres on a 2-kilometre trip is the obvious case. The subtler case is a stock count read at the start of an order and decremented at the end, so two customers both win the last item.

The move is boring. Recompute anything you can from data you already hold. Lock anything that two requests might touch at once. Treat every number in a request body as a suggestion.

2. A document enters your parser

Documents carry more than words. Unicode has a block of tag characters that render as nothing at all and that a language model reads as text. Zero-width characters split a word so a keyword filter misses it. Bidirectional overrides make text display in a different order than it is stored, which is how the Trojan Source attack made code read one way and compile another.

The failure is a document that carries instructions you never saw. If that document reaches a model, the model may follow them. The move is to scan at the boundary, before the text reaches a screen or a prompt. Strip what has no legitimate reason to exist. Flag what might be legitimate, like an instruction-shaped phrase, and let the caller decide. A security paper that quotes an attack is still a valid document.

3. A prompt leaves for a model

When your app sends a customer's message to a model, the message leaves your infrastructure and lands in another company's. Under European data law that company is a sub-processor, which is the rule that says you must list everyone who touches your customers' data and what they receive.

The failure is quiet. Nothing crashes. A name, an email and a card number travel to a provider you never listed, and sit in logs you do not control. The move is to redact on the way out and restore on the way back, so the model reads a sentence that still makes sense and the real values never cross. The engineering that makes this safe under streaming, across chunk boundaries, is most of the work. The idea fits in three lines.

4. A dependency enters your build

Your build pulls in whatever the maintainer last pushed. Security scanners will tell you which packages have known vulnerabilities. Fewer will tell you which packages are quietly dead: an upstream archived two years ago with no advisory ever filed, because nobody was there to file one.

The failure is running on an unmaintained branch and not knowing it. The move is to audit health and maintenance, not only vulnerabilities. Release recency, repository activity, whether the thing you depend on still has a person behind it.

5. Text leaves your editor

Something breaks late at night. You copy the error log, paste it into a chat window and ask why it failed. The key was in the log. Nobody uploads a production secret on purpose. It travels inside something else.

The move is to scrub before the paste and restore after the answer. The hard part is not the keys with vendor prefixes, which a pattern can catch. It is the plain sentence: "my api key is" followed by something too short to look random. Anchor on the label, validate the value, and leave "password is required" alone because that is prose.

One rule

Verify at the line. Trust inside it.

Everything that crosses is a claim until the boundary turns it into a fact. Once it is a fact, the code inside can be simple, fast and confident, because it no longer has to defend itself. Most defensive code I see is defensive in the wrong place: null checks and sanity checks scattered through business logic, because the boundary never did its job and everyone downstream knows it.

Put the checking where the data arrives. Then stop checking.

Why I am writing this as a drawing set

Construction drawings have something most engineering writing lacks. A status. A revision. A number you can refer to. "Sheet 003" is a thing a colleague can point at. "That post I wrote in September" is not.

So the lessons in this essay, and the ones that follow over the next month, are numbered sheets. Some record what shipped, exactly as it shipped. Some are rules to build by. Some are post-mortems on my own work, stamped NCR, which on a building site means a non-conformance report.

This is sheet 008. It indexes the five boundaries. Sheets 003 and 006 are the request boundary, told as the two marketplace bugs above. Sheet 007 is the document boundary. The other three get their own sheets over the next two weeks. The whole set, in order, lives at godfreylebo.dev/sheets.