AIAI EngineerDec 20, 2025· 18:57

The Infinite Software Crisis – Jake Nations, Netflix

Jake Nations, engineering lead at Netflix, argues that AI-generated code accelerates the software crisis by conflating easy with simple—producing tangled, incomprehensible systems. He traces the crisis from 1968 to today's infinite code generation, citing Fred Brooks' 'No Silver Bullet' and Rich Hickey's definition of simple as 'one fold, no entanglement.' Nations presents a three-phase methodology—research (compressing 5 million tokens of code into a 2,000-word spec), planning (paint-by-numbers implementation steps), and implementation (using a manual migration seed)—to maintain human understanding. He warns that without this approach, engineers lose the ability to recognize dangerous complexity, and challenges listeners: will we still understand our own systems when AI writes most of the code?

Transcript

Confession0:00

Jake Nations0:21

Hey everyone, good afternoon. Um, I'm going to start my talk with a bit of a confession: I shipped code I didn't quite understand. I generated it, tested it, deployed it, couldn't explain how it worked. And here's the thing, though: I'm willing to bet every one of you have too.

So, now that we can all admit that we all ship code that we don't understand anymore, I want to take a bit of a journey and see how this kind of has come to be. First, we look back in history.

We see that history tends to repeat itself. Second, we've fallen into a bit of a trap: we've confused "easy" with "simple." Lastly, there is a fix, but it requires us not to outsource our thinking. So, I've spent the last few years at Netflix helping drive adoption of AI tools.

And I have to say, the acceleration is absolutely real. Backlog items that used to take days now take hours. And large refactors that have been on the books for years are finally being done. Here's the thing, though: large production systems always fail in unexpected ways.

Like, look what happened with Cloudflare recently. When they do, you better understand the code you're debugging. And the problem is, now we're generating code at such speed and such volume, our understanding is having a hard time keeping up.

Hell, I know. I've done it myself. I've generated a bunch of code, looked at it, thought, "I have no idea how this—what this does." But, you know, the tests pass, it works, so I shipped it. The thing here is, this isn't really new.

Every generation of software engineers has eventually hit a wall where software complexity has exceeded their ability to manage it. We're not the first to face a software crisis, we're the first to face it at this infinite scale of generation.

History Repeats1:45

Jake Nations1:56

So let's take a step back to see where this all started. In the late '60s, early '70s, a bunch of smart computer scientists at the time came together and said, "Hey, we're in a software crisis. We have this huge demand for software, and yet we're not really able to keep up, and the projects are taking too long, and it's just really slow.

We're not doing a good job." So Dijkstra came up with a really great quote, and he said, "When we had a few weak computers"—and I mean to paraphrase the longer quote—"when we had a few weak computers, programming was a mild problem.

And now we have gigantic computers, programming has become a gigantic problem." He was explaining, "As hardware power grew by a factor of 1,000, society's wants of software grew in proportion." And so it left us, the programmers, to figure out between the ways and the means: how do we support this much more software?

So this kind of keeps happening in a cycle. In the '70s, we get the C programming language, so we could write bigger systems. In the '80s, we have personal computers, now everyone can write software. In the '90s, we get object-oriented programming.

Inheritance hierarchies from hell were, you know, thanks, Java, for that. In the 2000s, we get Agile, and we have sprints and scrum masters telling us what to do. There's no more waterfall. In the 2010s, we get cloud, mobile, DevOps, you know, everything.

Software truly ate the world.

And today, now we have AI. You know, Copilot, Cursor, Claude Codex, Gemini, you name it, we can generate code as fast as we can describe it. The pattern continues, but the scale has really changed. It's infinite now. So, uh, Fred Brooks, who you might know from writing the Mythical Man month, he also wrote a paper in 1986 called "No Silver Bullet."

And in this, he argued that there would be no single innovation that would give us an order of magnitude improvement in software productivity. Why? Because he said the hard part wasn't ever the mechanics of coding. The syntax, the typing, the boilerplate.

No Silver Bullet3:33

Jake Nations3:46

It was about understanding the actual problem and designing the solution. And no tool can eliminate that fundamental difficulty. Every tool and technique we've created up to this point makes the mechanics easier. The core challenge, though, understanding what to build, how it should work, remains just as hard.

So if the problem isn't in the mechanics, why do we keep optimizing for it? How do experienced engineers end up with code they don't understand now? The answer, I think, comes down to two words we tend to confuse: "simple" and "easy."

Simple vs Easy4:04

Jake Nations4:14

We tend to use them interchangeably, but they really mean completely different things. Uh, I was outed at the speaker dinner as being a closure guy, so this is kind of clear here. But Rich Hickey, the creator of the closure programming language, explained this in his talk from 2011 called "Simple Made Easy."

He defined simple meaning one fold, one braid, and no entanglement. Each piece does one thing and doesn't intertwine with others. He defines easy as meaning adjacent, what's within reach. What can you access without effort? Copy, paste, ship. Simple is about structure.

Easy is about proximity. The thing is, we can't make something simple by wishing it so. Simplicity requires thought, design, and untangling. But we can always make something easier. You just put it closer. Install a package. Generate it with AI.

You know, copy a solution off a Stack Overflow. It's hu—it's human nature to take the easy path. We're wired for it. You know, as I said, copy something from Stack Overflow. It'sright there. Framework that handles everything for you with magic.

Install and go. But easy doesn't mean simple. Easy means you can add to your system quickly. Simple means you can understand the work that you've done. Every time we choose easy, we're choosing speed now, complexity later. And honestly, that trade-off really used to work.

The complexity accumulated in our codebases slowly enough that we can refactor, rethink, and rebuild when needed. I think AI has destroyed that balance. Because it's the ultimate easy button. It makes the easy path so frictionless that we don't even consider the simple one anymore.

Why think about architecture when code appears instantly? So let me show you how this happens. How a simple task evolves into a mess of complexity through a conversational interface that we've all come to love. You know, this is a contrived example, but, you know, say we have our app, we want to add, uh, some authentication to it.

Vibecoding Trap5:42

Jake Nations5:58

Say add auth. So we get a nice clean auth.js file. Iterate it on a few times, it gives a message five. You're like, "Okay, cool, we're going to add OAuth now too," because now we've got an auth.js and OAuth.js.

We keep iterating, and then we find ourselves that sessions are broken, and we got a bunch of conflicts. And by the time you get to turn 20, you're not really having a discussion anymore. You're managing context that's become so complex that even you don't remember all the constraints that you've added to it.

Dead code from abandoned approaches. Uh, tests that got fixed by just making them work. You know, fragments of three different solutions because you kept saying, "Wait, actually." Each new instruction is overwriting architectural patterns. When you said, "Make the auth work," here, it did.

When you said, "Fix this error," it did. There's no resistance to bad architectural decisions. The code just morphs to satisfy your latest request. Each interaction is choosing easy over simple. And easy always means more complexity. We know better, but when the easy path is just this easy, we take it.

And complexity is going to compound until it's too late.

AI really takes easy to its logical extreme. Decide what you want. Get code instantly. But here's the danger in that. The generated code treats every pattern in your codebase the same. You know, when an agent analyzes your codebase, every line becomes a pattern to preserve.

The authentication check on line 47, that's a pattern. That weird gRPC code that's acting like GraphQL that I may have added in 2019, that's also a pattern. Technical debt doesn't register as debt, it's just more code. The real problem here is complexity.

I know I've been saying that word a bunch in this talk without really defining it, but the best way to think about it is it's the opposite of simplicity. It just means intertwined. And when things are complex, everything touches everything else.

You can't change one thing without affecting ten others.

Complexity Types7:41

Jake Nations7:41

So back to Fred Brooks' "No Silver Bullet" paper. In it, he identified that there's two main types of complexity in every system. There's the essential complexity, which is really the fundamental difficulty of the actual problem you're trying to solve.

Users need to pay for things. Orders must be fulfilled. This is the complexity of why your software system exists in the first place. And then second, there's this idea of accidental complexity. Everything else we've added along the way: workarounds, defensive code, frameworks, abstractions that made sense a while ago.

It's all the stuff that we put together to make the code itself work. In a real codebase, these two types of complexity are everywhere, and they get so tangled together that separating them requires context, history, and experience. The generated output makes no such distinction, and so every pattern keeps just getting preserved.

So here's a real example from, uh, some work we're doing at Netflix. I have a system that has an abstraction layer sitting between our old authorization code we wrote, say, five or so years ago, and a new centralized auth system.

We didn't have time to rebuild our whole app, so we just kind of put a shim in between. So now we have AI. This is a great opportunity to refactor our code to use the new system directly. Seems like a simple request,right?

And no. It's like, the old code was just so tightly coupled to its authorization patterns. Like, we had permission checks woven through business logic, role assumptions baked into data models, and auth calls scattered across hundreds of files. The agent would start refactoring, get a few files in, and hit a dependency you couldn't untangle, and just spiral out of control or give up.

Or worse, it would try and preserve some existing logic from the old system and recreate it using the new system, which I think is not great too.

The thing is, it couldn't see the seams. It couldn't identify where the business logic ended and the auth logic began. Everything was so tangled together that even with perfect information, the AI couldn't find a clean path through. When your accidental complexity gets this tangled, AI is not the best help to actually make it any better.

I've found it only adds more layers on top. We can tell the difference, or at least we can when we slow down enough to think. We know which patterns are essential and which are just how someone solved it a few years ago.

We carry the context that the AI can't infer, but only if we time to make—take time to make these distinctions before we start.

So how do you actually do it? How do you separate the accidental and essential complexity when you're staring at a huge codebase? The codebase I work on at Netflix has around a million lines of Java, and the main service in it is about 5 million tokens last time I checked.

Context Compression10:00

Jake Nations10:14

No context window I have access to, uh, can hold it. So when I wanted to work with it, I first thought, "Hey, maybe I can just copy large swaths of this codebase into the—into the context and see if the patterns were merged, see if it would just be able to figure out what's happening."

And just like the authorization refactor from previously, the output just got lost in its own complexity. So with this, I was forced to do something different. I had to select what to include, design docs, architecture, diagrams, key interfaces, you name it, and take time writing out the requirements of how components should interact and what patterns to follow.

See, I was writing a spec. Uh, 5 million tokens became 2,000 words of specification. And then to take it even further, take that spec and create an exact step—set of steps of code to execute. No vague instructions, just a precise sequence of operations.

I found this produced much cleaner and more focused code that I could understand, because I defined it first and planned its own execution.

This became the approach which I called context compression a while ago, but you call it context engineering or spectrum of development, whatever you want, the name doesn't matter. What only matters here is that thinking and planning become a majority of the work.

Three Phases11:20

Jake Nations11:20

So let me walk you through how this works in practice. So you have step one, phase one, research. You know, I go and feed everything to it upfront. Architecture diagrams, documentation, Slack threads. I mean, we've been over this a bunch.

But really, just bring as much context as you can that's going to be relevant to the changes you're making. And then use the agent to analyze the codebase and map out the components and dependencies. This shouldn't be a one-shot process.

I like to probe. Say, like, what about the caching? How does this handle failures? And when its analysis is wrong, I'll correct it. And if it's missing context, I provide it. Each iteration refines its analysis. The output here is a single research document.

Here's what exists. Here's what connects to what. And here's what your change will affect. Hours of exploration are compressed into minutes of reading. I know Dex mentioned it this morning, but the human checkpoint here is critical. This is where you validate the analysis against reality.

The highest leverage moment in the entire process. Catch errors here, prevent disasters later. On to phase two. Now that you have some valid research in hand, we create a detailed implementation plan. Real code structure, function signatures, type definitions, data flow.

You want this to be so any developer can follow it. I, I kind of liken it to paint by numbers. You should be able to hand it to your most junior engineer and say, "Go do this." And if they copy it line by line, it should just work.

This step is where we make a lot of the important architectural decisions. You know, make sure complex logic is correct. Make sure business requirements are, you know, following good practice. Make sure there's good service boundaries, clean separation, and preventing any unnecessary coupling.

We spot the problems before they happen because we've lived through them. AI doesn't have that option. It treats every pattern as a requirement. The real magic in this step is the review speed. We can validate this plan in minutes and know exactly what's going to be built.

And in order to keep up with the speed at which we want to generate code, we need to be able to comprehend what we're doing just as fast.

Lastly, we have implementation. And now that we have a clear plan and, like, backed by clear research, this phase should be pretty simple. And that's the point. You know, when AI has a clear specification to follow, the context remains clean and focused.

We've prevented the complexity spiral of long conversations. And instead of 50 messages of evolutionary code, we have three focused outputs, each validated before proceeding. No abandoned approaches. No conflicting patterns. No wait, actually, moments that leave dead code everywhere.

To me, what I see as a real payoff of this is that you can use a background agent to do a lot of this work. Because you've done all the thinking and hard work ahead of time, it can just start the implementation.

You can go work on something else and come back to review. And you can review this quickly because you're just verifying that it's conforming to your plan, not trying to understand if anything got invented.

The thing here is, we're not using AI to think for us. We're using it to accelerate the mechanical parts while maintaining our ability to understand it. Research is faster. Planning is more thorough. And the implementation is cleaner. The thinking, the synthesis, and the judgment, though, that remains with us.

So remember that, uh, authorization refactor I said that AI couldn't handle? The thing is, now we're actually, you know, working on it now and starting to make some good progress on it. The thing is, it's not because we found better prompts.

Manual Migration14:30

Jake Nations14:43

We found we couldn't even jump into doing any sort of research, planning, and implementation. We actually had to go make this change ourselves by hand. No AI. Just reading the code, understanding dependencies, and making changes to see what broke.

That manual migration was, I'll be honest, it was a pain. But it was crucial. It revealed all the hidden constraints, which invariants had to hold true, and which services would break if the auth changed. Things no amount of code analysis would have surfaced for us.

And then we fed that pull request of the actual manual migration into our research process and had it use that as the seed for any sort of research going forward. The AI could then see what a clean migration looks like.

The thing is, each of these entities are slightly different, so we have to go and interrogate it and say, "Hey, what do we do about this?" Some things are encrypted, some things are not. We had to provide that extra context each time, uh, through a bunch of iteration.

Then, and only then, we could generate a plan that might work in one shot. And the key word, and might's the key word here, is we're still validating, still adjusting, and still discovering edge cases.

The three-phase approach is not magic. It only works because we did this one migration by hand. We had to earn the understanding before we could encode into our process. I still think there's no silver bullet. I don't think there's better prompts, better models, or even writing better specs.

Just the work of understanding your system deeply enough that you can make changes to it safely.

Knowledge Gap16:14

Jake Nations16:14

So why go through with all this? Like, why not just iterate with AI until it works? Like, eventually won't models get strong enough and it just works? The thing to me is, it works isn't enough. There's a difference between code that passes tests and code that survives in production.

Between systems that function today and systems that can be changed by someone else in the future. The real problem here is a knowledge gap. When AI can generate thousands of lines of code in seconds, understanding it could take you hours, maybe days if it's complex.

Who knows? Maybe never if it's really that tangled. And here's something that I don't think many people are even talking about at this point. Every time we skip thinking to keep up with generation speed, we're not just adding code that we don't understand.

We're losing our ability to recognize problems. That instinct that says, "Hey, this is getting complex," it atrophies when you don't understand your own system.

Pattern recognition comes from experience. When I spot a dangerous architecture, it's because I'm the one up at 3:00 in the morning dealing with it. When I push for simpler solutions, it's because I've had to maintain the alternative from someone else.

AI generates what you ask it for. It doesn't encode lessons from past failures. The three-phase approach bridges this gap. It compresses understanding into artifacts we can review at the speed of generation. Without it, we're just accumulating complexity faster than we can comprehend it.

Human Endeavor17:40

Jake Nations17:40

AI changes everything about how we write code. But honestly, I don't think it changes anything about why software itself fails. Every generation has faced their own software crisis. Dijkstra's generation faced it by creating the discipline of software engineering.

And now we face ours with infinite code generation. I don't think the solution is another tool or methodology. It's remembering what we've always known, that software is a human endeavor. The hard part was never typing the code. It was knowing what to type in the first place.

The developers who thrive won't just be the ones who generate the most code. They'll be the ones who understand what they're building, who can still see the seams, who can recognize that they're solving the wrong problem. That's still us.

That will only be us. I want to leave on a question. And I don't think the question is whether or not we will use AI. That's a foregone conclusion. The ship has already sailed. To me, the question is going to be whether we will still understand our own systems when AI is writing most of our code.

Thank you.