Skip to content
Leadership Garden Leadership
Garden

The Typing Was the Safety Harness

18 min read
Table of Contents

The hard parts of software engineering are still hard. The forcing functions moved. The work did not.

I have been trying to make this argument to colleagues for months. Most of the popular framings of the AI shift get it wrong in the same way. They focus on the visible surface changes: a new orchestrator role here, the death of the typing bottleneck over there, prompt engineering as its own specialty alongside the older disciplines, agent-fleet management starting to look like a real job. The underlying work, the part of engineering that distinguishes one practitioner from another, looks the same as it did three years ago. We just used to do most of it for free, by accident, as a side effect of typing.

The “we are all orchestrators now” framing has something real underneath it, but the framing itself is incomplete in a way that matters. The angle I want to argue is narrower and more useful than “your job is now to manage agents.” Your job is to recreate, on purpose, the safety pass that typing used to do for you for free.

Typing code was doing load-bearing work that nobody invoiced for, an implicit in-line form of design review and quality assurance. We removed it, and most teams haven’t realized they still need it.

The bottleneck was never typing

This is the uncontroversial part. If you can type 80 words a minute and you ship a few thousand lines on a great week, the math says typing was never the bottleneck. Maybe four hundred lines on a focused day. Out of six hours of deep work, the actual finger-on-keyboard time is small. The rest is something else.

Most people would call the something else “thinking.” That is true but vague. The something else is a stack of activities that happen while you are writing code, that happen because you are writing code, and that you do not do at any other time.

You notice when the shape of the problem is wrong.

You realize the boundary between two modules is in the wrong place because filling in the second module makes the first one’s interface look stupid.

You fight with three lines of code for an hour and at the end of the hour you understand that the data model is broken, not the code.

You write a function signature and find that the inputs you thought you needed are wrong. Fixing the upstream call site forces you to fix the type definition, and the type definition surfaces a subtle assumption nobody had written down.

You read the previous engineer’s code while you are touching it and fix the bug you spot, the one nobody filed a ticket for.

You write a test and discover, while writing the assertion, that the function you just wrote actually has three exit conditions instead of two, and one of them silently swallows errors.

None of this is typing. But it was happening during typing, which is why we never separated the two, and which is why we are mostly surprised now that the work didn’t come along when the typing left.

The conventional split for software engineering used to be something like fifty percent writing code, fifty percent testing. That split was already wrong. In my experience watching senior engineers work, the writing is more like five or ten percent of the actual time spent, and the rest is the noticing. The noticing is what we are losing.

Slowness was load-bearing

The slowness of writing code was doing a kind of work we never named. It forced you to think carefully about each line, and when the slowness left, the thinking left with it.

Take a concrete comparison. In the old workflow, you sat down to write a small data model. Halfway through, you realized one field needed to be a discriminated union instead of an optional. You changed it. You moved on. The change cost you maybe ninety seconds of thinking and two lines of code. Nobody filed a ticket. Nobody wrote a design doc. The fix happened because you were the one writing the code and you noticed.

In the new workflow, you write a prompt that produces the data model. The model uses an optional. You scan the output, think “looks fine,” and move to the next thing. The bug ships. Or, worse, the model passes through a code review where the reviewer is also scanning at the same level of attention, because the volume of agent output has made deep review economically impossible. Two weeks later somebody hits a state the optional can’t represent. Now you are debugging it as a runtime issue instead of catching it as a design issue.

Same bug, both workflows. The difference is when it surfaced. The old way caught it during writing, at the cheapest possible point in its lifecycle. The new way catches it in production, under load, with state already in the wrong shape on disk.

Slowness was a free QA pass we ran on every line of code, paid for in our time. We never called it QA because it looked like writing.

We have always done stochastic engineering

One of the recurring complaints about agent-driven coding is that it makes software engineering stochastic. The same prompt produces different code on different days. You don’t know what you’re going to get.

That complaint is fine as far as it goes, but the framing is wrong. Software engineering, at any scale that matters, has always been stochastic. If you have ever run a project with three interns, or staffed a team mostly with new grads, you know this. You can write the most lovingly detailed ticket in the world, attach the cleanest mock, link the right design doc, and the code you get back will surprise you. Not because the engineer is bad. The engineer is fine. They just don’t have your model of the system in their head. Their version of “the right answer” is shaped by their experience, which is not yours. So you get something close to what you asked for, with a few decisions you wouldn’t have made, and you correct course in review.

This is not a degraded form of engineering. It is engineering. The way real systems get built at scale is by senior engineers specifying constraints, defining interfaces, owning the architecture, and accepting that the people filling in the boxes will sometimes color outside the lines. Code review exists for this reason. Type systems exist for this reason. Integration tests exist for this reason. The whole apparatus of professional software engineering, the part of it that isn’t about typing, is a response to the stochastic nature of code produced by humans of varying experience working on the same system.

When you accept that engineering has always been stochastic, the agents stop looking like a category change. They look like a new kind of contributor. The contributor is faster and cheaper than a human, and more willing to commit to a bad decision and stay committed to it.

The job of the senior engineer is the same as it was. You specify constraints. You define interfaces. You own architecture. You run code review. What changed is the contributor on the other side of the contract.

The shape an agent reaches for when nobody is watching

Here is the failure mode that does not show up in most of the writing on this topic. The cost of doing the wrong thing for ten thousand more lines is the same, for an agent, as the cost of doing the right thing for ten thousand lines. The agent doesn’t feel the difference. The human used to.

What this means in practice is that agents will commit to architecturally absurd decisions and stay committed to them. The failure mode is real, even if it does not show up every time, and once you start seeing it you can’t unsee it. I have watched agents pile every entity in a domain into one giant union type because that was the shape that satisfied the first few test cases. I have watched them build a manifest-of-manifests-of-manifests structure for a problem that wanted a flat list. I have watched them invent a parallel identifier scheme to track relationships the database was already tracking, then stack thirty features on top of that scheme before anyone noticed. A human engineer would not write any of these things, because the slowness of typing the wrong shape would have made the human stop, look around, and notice. The agent had no such forcing function. It produced text at a speed where the wrongness of the shape never became expensive enough to interrupt the production of more text.

I think about this every time I open a pull request from someone who has been heavily relying on an agent. The code is often fine at the line level. The bugs worth catching live one floor up. A data model with one too many concepts. A service boundary in the wrong place. An enum that swallows half its cases without comment, or an identifier scheme that conflates three different things into one type. The bugs that survived to PR did so because they happened at a level the human reviewer was no longer being forced to think about.

This is the same pattern I wrote about in No, AI didn’t break PRs . AI did not invent these failure modes. It amplified them and the speed at which they reach you.

The QA was hidden in the writing

I am not the first person to notice that “writing code” was doing more than producing characters. Rubber-duck debugging is the cleanest version of the same idea. You talk through code to a rubber duck and the bug surfaces, not because the duck contributes anything but because the act of articulating the problem to something outside your head forces you to model the system carefully. Pair programming and design docs are doing related work. We have always sort of known that the medium of typing forced a kind of slow, careful thought we couldn’t get any other way. We just never had to defend the practice, because the alternative wasn’t available.

Now the alternative is available, and the defense matters.

The QA that used to live in typing has to live somewhere now. If you don’t put it somewhere, you don’t have it.

This is the verification debt I’ve written about before, viewed from a different angle. Most teams are discovering it in production, six months after they handed coding to agents, when they start tripping over architectural decisions nobody remembers making.

In my experience, this work goes into a small number of artifacts and rituals that you have to be willing to do by hand. I have come around to a set of practices that look, from the outside, like the kind of work a tech lead does. The naming convention “everyone is a tech lead now” is fine. The more useful framing is that the forcing functions that used to be implicit have to be made explicit, and the explicit version is more work in some ways and less work in others.

Here is what I have landed on, partly through my own work and partly through watching engineers I respect figure out their own version of the same thing.

Write your schemas by hand, every one of them. If it lives at a system boundary and other people will rely on its shape, you should be the one typing it. This applies to database schemas, but also to protobuf definitions, OpenAPI specs, and GraphQL types. Even small schemas. I have spent days on schemas only a few hundred lines long, because the schema needed to be correct in a load-bearing way and I refused to let the agent touch it. Agents can write schemas. That is not the issue. The issue is that you want the slow QA to happen at the schema level, because every downstream bug worth debugging at 2 AM is traceable to a wrong shape in a load-bearing place. Get the schema right and you have fenced off the territory where the worst bugs live.

Write your interface and API contracts by hand, for the same reason. Function signatures and request shapes. Error types, retry semantics, and idempotency guarantees on top of that. These are the artifacts that constrain everything downstream. If the contract is wrong, the implementation can be perfect and the system will still be broken.

Write the tests in a separate context from the implementation. This one is non-obvious. If you ask an agent to write code and then ask the same agent to write tests for the code, the agent has the code in its context, and the tests will reify whatever the code already does. What you get is a hypothesis-free assertion of current behavior dressed up to look like coverage. So write the tests in a fresh session, against the API contract you have already pinned down. Then let an agent fill in the implementation against the tests. If the agent tries to modify the tests, push back hard. The tests are the contract for what the implementation has to do, and the moment the implementation is allowed to rewrite the contract you have lost the whole point.

The full pattern, in shorthand, is this. Schemas by hand. Interfaces by hand. Tests in isolation. Implementation by agent. Real correctness checks at the system boundary, not at the line level. The work that used to happen during typing now happens in a small number of artifacts that constrain everything downstream.

Some engineers go further and write all their tests by hand. They are probably right. Tests written by a careful human catch the bugs that tests written by an agent ratify. I find writing tests by hand miserable, so I split the difference and write them in an isolated context. The point is that somewhere, a human has to do the slow careful thinking, and the schemas-and-interfaces-and-tests triangle is where most engineers I trust have landed.

The shallow systems are not safer

It is tempting to read everything I have written so far as “this only applies to deep systems like databases and infra.” A common version of the argument from people who think the agent revolution is mostly fine goes: the failure modes only matter in narrow, deep systems with high implementation complexity. Broad shallow apps, where each feature has limited blast radius on the rest of the architecture, are different.

I want to push back on this, because the assumption that shallow systems are safer hasn’t matched what I see when I review code. If anything, shallow systems are where this pattern is most insidious, because each individual mistake looks too small to be worth fixing.

I review code for friends building side projects. Practice trackers, recipe apps, personal finance dashboards, the small weekend kind of thing where the domain is familiar and the surface area is modest. What I see, in repo after repo, is the same shape of drift. The first few features are clean, because the human cared. By feature ten, the agent has introduced a parallel identifier system that shadows the one the database already provides. By feature twenty, the validation logic exists in three places, each subtly different. By feature thirty, the data model has accumulated enough redundancy that some operations work and others crash, depending on whether two redundant fields happen to be in sync.

None of this gets caught in code review, because each individual diff looks fine. The drift is cumulative. The human is waving things through, one ugly choice at a time, and after thirty waves the system is unrecoverable.

The lesson is that shallowness of feature surface is not the same as shallowness of data model. The data model is always deep, even in a small app. And the data model is exactly the thing that gets mangled when you stop doing the slow QA pass on every piece of code that touches it. Watch your data models. Hand-roll them. Don’t let the agent touch them. This advice holds whether you are building a database or a hobby app, and it holds harder for the small projects, because nobody is watching small projects closely enough to catch the drift.

How the noticing gets learned

The honest question I get whenever I talk about any of this, usually from someone in their first or second job, is some version of: how is anyone supposed to learn it? The senior engineers in this conversation grew up typing. The noticing got built into their hands across years of unforgiving practice. If the typing has been outsourced to an agent, and the noticing was a side effect of typing, what does an engineer with three years of experience do? What does an engineer with zero years of experience do? This is, in a real way, the senior engineer crisis arriving early and from an angle nobody planned for.

My answer is partial. Here it is.

The first part is that the noticing can still be practiced directly. The way is the way I described above. Write the schema by hand. Write the interface by hand. Write the tests by hand or in a fresh context. Read what the agent produces and check the data model against the cases it has to support. Sit with the ugliness of a thing that looks like it works and ask whether it could be wrong. None of these require six years of typing experience. They require attention. The skill is the skill of not waving things through, of being willing to slow down at the artifacts that matter even when the agent is offering to skip them.

The second part is that we should be honest about what is lost. Some of the noticing came from spending years filling in code, day after day, and seeing the shapes of the bugs that recur. An engineer who has hand-written ten implementations of a key-value store knows in their bones that one-key implementations don’t work. An engineer who has only read agent output of key-value stores might not. We need to find ways to give people this grounding without the slow apprenticeship of typing, and I don’t think any of us has worked out a complete answer. Reading code carefully helps. Postmortems help. Putting people on call early helps. Asking junior engineers to design the schema before any code is generated helps. None of these on their own replace the kind of accumulated pattern-matching that years of typing produced.

What I am confident about is this. The temptation for less experienced engineers will be to lean harder on the agent because the agent is faster than they are. The agent is faster at producing characters, not at being right. If you outsource the noticing, you get further from the answer in less time. The engineers I have watched grow the most over the last year are the ones who use agents heavily but who refuse to outsource the parts of the work that look load-bearing, even when it would be cheaper in the short term to do so. They write their schemas. They review their own code before anyone else does. They stay in the loop on the architecture even when they could ask the model to draft it for them. It is more work, and it is also the only way I know to build the kind of judgment the work requires.

What changed is the price of one input

What is changing is not the difficulty of software engineering. What is changing is the price of one specific input. Code production used to be expensive, and most of what we did as engineers was structured around that constraint. We wrote design docs because writing code to validate a design was costly. We did code review because writing buggy code was costly. We had architects because rewriting a system was costly.

Now code production is cheap. The other costs haven’t moved at all. Designing the right data model still takes the same amount of careful thought it always did. Defining the right interface still requires the same number of arguments in a conference room. Knowing what to test for still requires you to have models of the system in your head. Operating the system in production is, if anything, more expensive than it used to be, because the systems are more complicated and the cost of getting paged at 3 AM has not dropped. The bottleneck has moved from where the code is produced to everywhere the code is produced toward: the shape of the answer, the contract of the interface, the test that proves it works.

Which means the job has not changed. The interesting part of the job, the part that distinguished a senior engineer from a junior one, was never the typing. It was the noticing. It was the catching the wrong shape in the wrong place at the wrong time. We got that for free, mixed in with the typing, and we never had to name it.

Now we have to name it. We have to put it somewhere. We have to do it on purpose.

If you take one thing from this, take this. The engineers who will look like wizards over the next two years are not the ones who prompt best. They are the ones who notice that typing was doing real work, who can recreate that work in artifacts the team can see, and who will hand-roll a schema or an interface even when the agent is right there offering to do it faster. The discipline has to come from somewhere. If you don’t build it in deliberately, the system doesn’t have it.

I have started insisting on this on every project I touch, including the small ones, including the ones where it feels like overkill. It is more work in some ways. It is less work in others, because the bugs that used to be caught in implementation now get caught at design, where they cost almost nothing. The trade is worth it. The work is mostly the same as it ever was. We just have to do it on purpose now.

Code is cheap. Being right is what is expensive, and that has always been the job.

Share

Explore further

Keep going with a few related posts, then branch into the topic hubs and collections around the same ideas.

Continue with these