Software engineering when machine writes the code

16 min read Original article ↗

In 1968, a group of computer scientists gathered at a NATO conference in Garmisch, Germany, and coined the term “software crisis.” The problem they identified wasn’t that computers were bad or unreliable. It was that computers had become too powerful for the existing methods of programming to handle. Edsger Dijkstra later put it memorably: “As long as there were no machines, programming was no problem at all; when we had a few weak computers, programming became a mild problem, and now we have gigantic computers, programming has become an equally gigantic problem.”

I think about this quote a lot lately. Not because we have gigantic computers (though we do), but because we now have something arguably more disruptive: machines that write the code themselves. And much like in 1968, the challenge isn’t that these tools are bad. It’s that they’re so good that our existing mental models for what it means to be a software engineer are struggling to keep up.

I’ve been using AI coding assistants extensively. Cursor, Claude, various agents and orchestration tools. I’m not writing this as a skeptic. I ship faster than I ever have. Features that would’ve taken me a week now take less than a day. Bugs that would’ve required hours of printf debugging get diagnosed in minutes. The productivity gains are real and substantial.

And yet something nags at me. A feeling that in the rush to capture these productivity gains, we might be trading away something important. Not in a luddite “new thing bad” sense, but in a more nuanced way that I want to try to articulate here.

The Jevons Paradox of code

In 1865, the English economist William Stanley Jevons observed something counterintuitive about coal consumption. As steam engines became more efficient and required less coal per unit of work, you’d expect overall coal consumption to decrease. Instead it increased dramatically. The improved efficiency made coal economically viable for applications that previously couldn’t justify the cost. More efficient use of a resource leads to more total consumption of that resource, not less.

This pattern, now called Jevons Paradox, shows up everywhere. More fuel efficient cars lead to more driving. Faster internet leads to more bandwidth consumption. And I suspect we’re seeing it with AI assisted coding too. If AI makes writing code ten times more efficient, we won’t write one tenth the code. We’ll write more code than ever before, creating systems of unprecedented complexity.

Look at what people are shipping with these tools. Compilers written in a weekend (I’m sure not product ready, but still). Game engines ported from TypeScript to Rust in their entirety. Full featured email clients built by single developers. The ceiling of what an individual or small team can build has risen dramatically.

This is genuinely exciting. But it also means the systems we’re building are getting more complex, faster than our ability to understand them might be growing. And this is where I start to worry about the equilibrium we’re settling into.

What does it mean to understand code you didn’t write?

Here’s a scenario I think about often, even if I’ve been careful to avoid it so far. You ask an AI to implement some feature. It produces clean, well structured code that passes all your tests. You review it, it looks reasonable, you ship it. Three weeks later something breaks in production at 2am and you’re staring at this code trying to understand what it’s actually doing and why.

The code works and has worked. But you never built a mental model of how it works. You reviewed it the way you might review a colleague’s pull request, checking for obvious issues, but you didn’t internalize it. And now that it’s broken in some edge case, you’re essentially reverse engineering your own codebase.

This is different from the “premature closure” problem I wrote about previously, where we accept the first plausible solution without exploring alternatives. This is something more fundamental. It’s about whether understanding is a prerequisite for ownership, and what ownership even means when a machine wrote the code.

I think there’s a meaningful distinction between code you wrote and code you reviewed. When you write code, even if you’re following a well worn pattern, you make dozens of small decisions along the way. You think about edge cases as you write each branch. You consider error handling as you type each function call. You build a mental model through the act of creation. When you review code someone else wrote, you can check for issues, but you’re fundamentally reconstructing a mental model rather than building one from scratch.

With AI generated code, this gap gets amplified. The AI might produce idiomatic, well structured code that looks like code you would’ve written. But it made different micro decisions. It might handle an edge case in a way you wouldn’t have thought of. It might use an abstraction pattern that’s unfamiliar to you. The code can be perfectly good and still be opaque to you in ways that matter when things go wrong.

When the context exceeds the context window

There’s a more practical version of this problem that shows up in debugging. Modern AI assistants have large context windows, but production systems are larger. When you’re debugging a gnarly issue in a distributed system, the relevant context might span dozens of files, multiple services, decisions made years ago by people who’ve since left, and operational knowledge about how the system actually behaves under load. Stuff that was never written down anywhere.

I’ve found that AI is remarkably good at debugging problems where the relevant context fits in the window. Give it a stack trace, the relevant code, and a description of what’s happening versus what should happen, and it’ll often identify the issue faster than I would. But the really hard bugs, the ones that take senior engineers days to track down, are usually hard precisely because the context is too large and diffuse to fit in any window.

These are the bugs where you need to know that this particular service acts weird when its connection pool gets exhausted, and that weirdness interacts poorly with the retry logic in another service, and the whole thing only shows up when traffic patterns shift in a way that happens maybe once a month. The knowledge you need to debug this isn’t in any single file. It’s not even in any single codebase. It’s spread across code, config files, Datadog dashboards, old incident reports from two years ago, and the heads of people who’ve been running this thing in production.

I don’t think AI will always be bad at this kind of debugging. Context windows will get larger, retrieval will get better. But right now, in early 2026, this is a genuine gap. And it means that the kind of deep systems understanding that lets you debug the hard problems is still valuable. Maybe more valuable than ever, because the easy problems are getting automated away.

The junior engineer’s dilemma

This brings me to something that worries me more than my own productivity. I’ve been writing software for a long time. I’ve built up mental models and intuitions through years of making mistakes, debugging my own terrible code, and slowly developing a sense for how systems behave. When I use AI to accelerate my work, I’m leveraging that existing foundation.

But what about someone just starting out? If you’ve never written a state machine by hand, have you developed an intuition for how state machines can go wrong? If you’ve never manually implemented a caching layer and dealt with the cache invalidation bugs that inevitably follow, will you recognize when an AI generated caching solution has subtle invalidation issues?

The traditional path to becoming a skilled engineer involved a lot of what felt like wasted time. Writing code you’d later throw away. Implementing things from scratch that libraries could’ve done for you. Spending hours debugging issues that a senior engineer could’ve identified in minutes. This process was inefficient in a narrow sense, but it built something important: the pattern recognition and intuition that lets you spot problems before they happen and debug issues efficiently when they do.

I’m not sure what the right path looks like for someone starting their career today. The economic pressure to ship fast is real. If your peers are using AI to produce ten times the output, you can’t easily opt out. But I also think that skipping the struggle entirely is likely to produce engineers who can ship features but can’t debug production incidents, who can implement solutions but can’t evaluate trade offs, who know what works but not why.

How to learn deeply in an age of AI assistance

I’ve been experimenting with ways to use AI that preserve and even enhance learning rather than replacing it. This isn’t about avoiding AI. It’s about being deliberate with it, using it in ways that build understanding rather than just bypassing the hard parts.

One approach I’ve found valuable is what I think of as “AI as a Socratic tutor.” Instead of asking the AI to write code for me, I ask it to explain concepts, walk me through how something works, describe the trade offs between different approaches. I then write the code myself or have it assist in writing smaller blocks. This is slower than having the AI YOLO it, but I end up with a much better mental model.

For example, I recently needed to implement a particular kind of concurrent data structure. Instead of asking the AI to implement it, I asked it to explain the different approaches to concurrent data structures, the trade offs between lock free and lock based designs, the memory ordering considerations on different architectures. We had a long back and forth where I asked follow up questions and poked at edge cases. Then I implemented it myself, referring back to our conversation when I got stuck.

The implementation I ended up with was probably not as good as what the AI would’ve produced directly. But I understand it deeply. I know why each synchronization primitive is there. I know what would break if I changed the memory ordering. And crucially, I’d know how to debug it if something went wrong in production.

Another technique that works well is using AI as a code reviewer for my own code. I write the implementation myself, then ask the AI to review it critically. What edge cases am I missing? What are the performance implications? How would this behave under high load? This gives me the benefits of a second pair of eyes without losing the learning that comes from writing the code myself.

I also find it valuable to use AI to explore the space of solutions rather than just produce one. I ask things like “what are three different ways to approach this problem?” or “what would a functional approach look like versus an object oriented one?” This helps me understand the trade offs and build intuition for when different approaches make sense.

For genuinely learning new domains, I’ve started using AI to create deliberate practice exercises. Say I want to understand a new database’s query planner. I might ask the AI to generate a series of increasingly complex queries along with questions about how the planner will handle them. I predict the behavior, check my prediction, and learn from where I was wrong. This kind of active learning is way more effective for me than just reading documentation.

The zone model of AI usage

Not all code is created equal in terms of how much deep understanding matters. I’ve started thinking about my work in terms of zones that warrant different levels of AI involvement.

There’s code that represents your core domain logic. The consensus algorithm in your distributed database, the replication logic that keeps your replicas in sync, the state machine that coordinates your distributed transactions. For this code, I think there’s a strong argument for doing more of the work manually, or at least being extremely deliberate about understanding every line. When this code breaks, you need to be able to debug it under pressure. When requirements change, you need to understand the implications deeply enough to modify it safely. The cost of not understanding this code is very high.

Then there’s code that’s important but more standard. API endpoints, database queries, form validations. This is code where the patterns are well established and the risk of subtle bugs is lower. I’m comfortable using AI more heavily here, though I still review carefully and make sure I could explain what the code is doing if asked.

Finally there’s code that’s genuinely boilerplate. Test scaffolding, migration files, documentation, glue code between well understood components. Here I let AI take the lead almost entirely. The cost of subtle bugs is low and the patterns are so standard that there’s little learning value in writing them manually.

This isn’t a rigid framework. Sometimes code you think is boilerplate turns out to have subtle requirements that matter a lot. Sometimes your core domain logic is so well specified that AI can handle it reliably. The point is to think deliberately about where understanding matters rather than applying the same approach to everything.

On the fear of obsolescence

I want to address something directly that I think underlies a lot of the anxiety around AI coding tools. Many engineers worry, whether they admit it publicly or not, that these tools will eventually make them obsolete. If AI can write code better than humans, why would anyone pay human engineers?

I think about this a lot and I’ve come to a somewhat optimistic view, though not for the reasons people usually give.

The standard reassurance is that AI is “just a tool” and engineers will still be needed to direct it, like how calculators didn’t eliminate mathematicians. I find this argument unsatisfying because it ignores that tools can and do eliminate jobs, even if they create new ones. The question isn’t whether the total number of jobs changes but whether your particular skills remain valuable.

My optimism comes from a different place. Software isn’t primarily about writing code. Code is the artifact, but the value is in solving problems for people. Understanding what to build matters as much as building it. Understanding why something broke and how to fix it requires judgment that goes beyond pattern matching. Understanding the second and third order effects of architectural decisions requires thinking about systems at a level of abstraction that current AI still struggles with.

More practically, I think the Jevons Paradox effect means that demand for software will increase faster than AI increases supply. If every company can now build software ten times faster, they won’t fire ninety percent of their engineers. They’ll build ten times as much software. And all of that software will need to be maintained, debugged, extended, and eventually replaced.

The engineers who thrive in this environment will be the ones who can work effectively with AI while maintaining the deep understanding that lets them handle the problems AI can’t. The engineers who struggle will be the ones who either refuse to use AI at all and can’t keep up with productivity expectations, or who use AI as a crutch without developing underlying skills and can’t handle anything outside the happy path.

Preserving the joy of understanding

There’s one more thing I want to touch on, which is less about careers and more about what makes this work enjoyable. I got into software engineering because I love understanding how things work. I find genuine pleasure in tracing a request from the browser through the network stack through the load balancer through the application server to the database and back. I like knowing why things are the way they are, how they fit together, what would happen if you changed this or that.

There’s a risk that heavy AI usage erodes this pleasure. If you’re primarily reviewing and approving code rather than writing it, if you’re directing rather than doing, some of the joy of craft might get lost. I’ve felt this myself. Days where I “shipped a lot” by orchestrating AI but ended the day feeling like I didn’t actually do anything.

I don’t think this is inevitable though. The techniques I mentioned earlier for using AI in ways that enhance learning also preserve the joy of understanding. When I use AI as a tutor and then implement something myself, I get the satisfaction of having built something and understood it. When I use AI to explore the solution space, I engage my curiosity in ways that feel generative rather than passive.

But it requires being intentional about it. The path of least resistance is to optimize for output, to accept the first working solution, to ship and move on. This is sometimes the right choice. But if it becomes the default choice, I think we lose something important about what it means to be an engineer.

Finding the equilibrium

I started this essay by invoking the software crisis of 1968. That crisis was resolved, eventually, through new methodologies, new tools, new ways of thinking about software that could handle the scale of what we were building. Structured programming, object oriented design, version control, automated testing, continuous integration. Each of these emerged as a response to complexity that existing methods couldn’t handle.

I think we’re in a similar moment now. The old mental models of what it means to be a software engineer are getting disrupted by tools that can write code faster and often better than we can. We haven’t yet developed the new methodologies and mental models that will let us thrive in this environment.

What I’ve tried to articulate here is my current best thinking on what those new models might look like. Use AI heavily for the work where understanding is less critical. Use AI as a learning tool for domains where you want to develop expertise. Be deliberate about maintaining deep understanding of your core systems. And don’t lose the joy of understanding how things work, because that joy is both what makes this profession worthwhile and what will keep you valuable as the tools continue to improve.

The machines can write code now. The question is what kind of engineers we want to be in a world where that’s true.