Beyond faster code: AI and the engineering we could be doing
When I design software, I have a habit of coming back to the same problem over and over again.
What if this assumption is wrong? What if we need to support something slightly different? What happens to the rest of the system if we change this part?
Sometimes those questions uncover a real flaw. Sometimes they don’t lead anywhere. And sometimes I simply don’t have enough time to investigate them properly before we need to move on.
Lately I’ve been wondering whether AI might change that last part.
Everyone seems interested in how quickly agents can write code. That’s useful, obviously. But if implementation becomes cheaper, what if we use some of the time we save to question the design instead of just delivering the next feature?
What if we spent that time engineering instead?
For a long time, turning requirements into working code has consumed a substantial part of our effort. It’s valuable work, and the details can teach us a lot about the design. But implementation also competes for time with the questions we’d like to investigate before committing to an approach.
The obvious benefit of faster implementation is more output. If an agent helps us build a feature in a day instead of a week, we can ship it sooner or move on to the next one.
But that’s not the only way to spend the difference.
Think about the work that often gets squeezed between requirements and implementation.
We might sketch two approaches but investigate only one because the other would take too long to prototype. We might notice an awkward edge case and put it on a list for later. We might wonder how a design would behave under a different set of assumptions, then decide we don’t have the capacity to find out.
None of this is necessarily bad engineering. Time and attention are constraints, just like memory and CPU.
There are two ways AI can help here. Faster implementation can free up time we’d otherwise spend writing code. But AI can also make the investigation itself cheaper: tracing the consequences of a proposed change, examining an alternative, or working through an edge case we’d otherwise leave unexplored. The first gives us room to think; the second lets us take the investigation further.
The opportunity is to investigate more before deciding what deserves to be built.
Following a source generator down the rabbit hole
I’ve been working on MinimalOpenAPI, which generates C# code from OpenAPI contracts. During implementation or review, an AI agent identified a gap: an inline object schema used as an array item could end up mapped to object[], losing the type information the generator was supposed to preserve.
The immediate problem was easy to describe. The difficult part was deciding how far the solution needed to go.
What if an array contains an inline object whose properties contain other inline objects? What if one of those properties is another array of inline objects? And what if the nesting continues? Down the rabbit hole we go.
The agent helped trace why the array items were losing their types and explore recursive traversal and naming strategies. The generator couldn’t just find another schema; it also had to give each generated type a name. And the deeper the nesting, the longer those names could get.
Really ugly names, in fact.
The agent and I explored how to name the types generated from increasingly nested schemas. Shorter, prettier names would be easier to read, but my priority was predictability. I didn’t want an unrelated change in the OpenAPI document to rename existing generated types and force consumers to update their code.
So I accepted them. C# type inference and target-typed expressions help reduce how often those names need to be written explicitly. It wasn’t the prettiest solution, but it preserved something I valued more: stability.
We addressed the array-item limitation by generating typed records for inline array items, including nested inline schemas. The useful outcome of the exploration wasn’t a magical naming trick, but a deliberate compromise. I examined the uncomfortable cases and established what mattered most before we settled on the implementation.
The issue wasn’t just getting the generator to emit more code. It was deciding which properties of that code I wanted to preserve as the input became more complicated.
Designing something in a language I don’t know
Artisan Logbook offers a different example.
It’s an addon for World of Warcraft that records crafting events so I can examine what my characters have produced and how their professions perform over time. I know essentially nothing about Lua, the language WoW addons use.
Without AI, I’d probably never have started the project. I wasn’t particularly interested in learning another language just to build a tool for a game.
At one point the agent introduced a limit on the amount of crafting history the addon would retain. I asked why. The answer was that exploring the data involved repeated scans of the entire collection, which would become expensive as the history grew.
So I asked why we weren’t using indices.
That question didn’t require knowing Lua. It required recognizing a familiar data-access problem. The discussion went further, and we also explored rollups to preserve useful aggregate history when older raw events could no longer be retained indefinitely.
The agent didn’t just type what I told it. It contributed design ideas, and I challenged some of its assumptions. It was a conversation about what the system should do and how it should behave, conducted through a language I can barely read.
Not everything works perfectly. Some crafting events still aren’t handled correctly, despite several rounds of investigation. I’m willing to live with that because it’s a free hobby addon. I could spend a weekend digging into the implementation myself, but I’d rather spend that weekend playing the game.
If the consequences were different, my decision would be different too.
Walking further around the design
The MinimalOpenAPI rabbit hole is one example of a habit I’ve had for years: I keep walking around my designs and asking what happens when something changes.
I wrote before about why Residuality Theory resonates with me. One part I particularly like is using stressors to test how an architecture responds when its assumptions are challenged, and examining the different structures that remain possible under those conditions.
One question I’ve explored with AI is what happens when a system designed around one set of assumptions needs to accommodate a fundamentally different requirement.
We discussed which assumptions would no longer hold, what architectural changes might be necessary, and whether those changes could be introduced without breaking existing contracts or requiring a fundamental redesign.
Once we’d identified the main challenges and were reasonably confident that the architecture could accommodate the change, we documented the findings and moved on.
This gave us confidence at the design level, not a guarantee. We’d still need to validate those assumptions if the requirement became real.
We didn’t implement the new requirement. We simply made sure we hadn’t unnecessarily closed the door on it.
Architectural exploration is expensive when every alternative means hours of tracing dependencies, inspecting code, or prototyping. AI helped make that discussion more practical without deciding which future requirement deserved attention.
I’m not suggesting an agent should generate fifty stressors and call the analysis complete. Choosing a meaningful question and interpreting the answer are still part of the engineer’s job. The benefit is being able to walk a little further once we’ve chosen where to look.
The thinking still matters
There is an obvious objection to all this. Much of our engineering understanding comes from implementation.
Writing and debugging code exposes awkward requirements, hidden dependencies, and abstractions that looked much nicer on a diagram than they feel in practice. If we delegate implementation, we risk losing some of that feedback.
I worry about that myself. I’ve barely written code by hand for months. I’m getting better at directing agents and questioning their proposals, but sometimes I wonder whether I’m losing my edge.
And agents have their own bad habits. In an iterative session, each local fix can look sensible while the overall system accumulates layers of unnecessary complexity. I’ve found independent, sometimes deliberately adversarial reviews useful for spotting that kind of superstructure.
In this workflow, I often value tests as a snapshot of the system’s current behavior. When an agent changes something, existing tests help reveal changes to the behavior they’ve captured, including things nobody intended to touch. They don’t prove that the previous behavior was correct, that everything outside their coverage stayed unchanged, or that the new design is good. They give us something concrete to compare. Compilers, linters, benchmarks, and integration or end-to-end checks provide other kinds of evidence.
None of these tools can replace understanding the problem. An agent can produce a detailed analysis of a stressor and still miss the dependency that matters most. A proposed architectural adjustment can appear convincing and create an entirely different weakness.
The goal isn’t to stop thinking. It’s to avoid spending all our time on the parts of the work that don’t need our direct attention, so we can engage more seriously with the parts that do.
There is an organizational catch as well. Cheaper implementation doesn’t automatically give us more time for engineering. A company might simply expect more features in the same period.
That’s a plausible outcome, but it isn’t the only one. I’d like to use at least some of the additional capacity to make better decisions before committing to an implementation.
What happens when AI can do more of the thinking?
There’s a speculative question I keep coming back to. If AI can help examine more designs, alternatives, and consequences, could future systems begin to recognize useful structures across problems that seem unrelated to us? Could they help discover new engineering principles, rather than only apply existing ones?
I wouldn’t call a repeated pattern a principle just because an AI model found it. We’d need to understand its assumptions, limits, and ability to predict useful outcomes. We’d need independent ways to validate it.
And there’s a separate possibility: AI might eventually become capable of making and validating more engineering decisions without the same level of human guidance. Discovering new principles and independently applying known ones aren’t the same capability, and neither is guaranteed.
I don’t know how far either direction will go.
For now, I’m more interested in the opportunities already in front of us.
Recap
AI is making it cheaper to implement software. That’s valuable, but I don’t think faster implementation should be the only measure of what we gain.
We can use some of that capacity to challenge more assumptions, explore more alternatives, and walk further around a design before deciding what to build. AI can even help with the investigation itself, provided we remain willing to question its answers.
The most interesting thing about AI writing code might be what it allows us to do before we ask it to write any.
This article was produced using an AI-assisted editorial process and was reviewed with AI assistance. Read about my editorial process.
Support this blog
If you liked this article, consider supporting this blog by buying me a pizza!