Sitemap
DevOps<>AI

Musings Around DevOps<>AI

Don’t Learn to Code. Learn to Build Systems.

10 min readSep 4, 2026

--

Press enter or click to view image in full size

I’ve been thinking about the phrase “learn to code.”

For a long time, that was pretty reasonable advice. If you wanted to build software, you needed to learn how to program. And even though professional software engineering has always involved much more than writing code — requirements, architecture, testing, operations, security — the code was still the thing sitting in the middle of all of it.

AI is starting to make that relationship a little weird because AI can produce a lot of code now. The more capable these systems become, the more I think we need to separate two things we’ve historically treated as almost the same: producing code and producing a working system.

They’re not the same skill. And increasingly, I think the second one is the skill that matters.

The Test Looks Obvious After Production Writes It for You

There’s an old distributed-systems study that captures the problem really well. Researchers looked at roughly 200 production failures across Cassandra, HBase, HDFS, MapReduce, and Redis.

The surprising part wasn’t that distributed systems fail. It was how ordinary many of the failures looked once someone understood what had happened.

According to the research, 92% of the catastrophic failures involved incorrect error handling. Around a third involved handlers that were almost comically bad: empty catch blocks, enormously broad exceptions followed by process termination, or code paths containing comments that basically amounted to “we have no idea how we got here.”

Even more interesting, about 98% of the failures could be reproduced using only three nodes. Most were deterministic, and the clues needed to understand them were usually already sitting in the logs.

So these weren’t necessarily unknowable failures. They were sequences nobody had thought to test yet.

That matters because one conclusion from the research was that many of the failures could have been caught with relatively simple tests. Akshay Shah made a point about this in a Papers We Love talk that I keep thinking about.

The tests are simple after you know the failure.

Imagine a test that says:

Partition node A. Let node B begin recovery. Heal node A. Roll the write-ahead log. Move traffic back. Verify that no writes disappear.

Once you’ve experienced the outage, that test isn’t especially complicated. Before the outage, someone has to imagine that exact sequence.

That’s the hindsight problem.

We’re very good at turning incidents into regression tests. Production gives us the sequence, we encode it, and now we can make sure that particular failure never happens again.

Useful, absolutely.

But it doesn’t solve the harder problem:

How do you test the failure nobody has imagined yet?

Build a World That Can Discover the Test

This is where deterministic simulation testing gets interesting.

Instead of requiring a human to enumerate every failure sequence that might matter, you construct a small world where those sequences can emerge. You combine property-based testing, generated inputs, fault injection, unusual timing, network failures, and state transitions, then run enormous numbers of combinations.

The important part is determinism.

If the simulator discovers some bizarre interaction — one node fails, another starts recovering, the network heals, background maintenance kicks in, and suddenly an invariant breaks — you can replay that exact sequence.

The weird production accident becomes a reproducible experiment.

And many of these failures don’t require some theatrical disaster. They emerge from ordinary operations colliding in the wrong order: startup, recovery, node changes, compaction, network partitions.

Each event can be perfectly reasonable.

The sequence isn’t.

That changes the relationship between the engineer and the test. Instead of saying:

Think of every important failure and write a test for it.

You get:

Define what must remain true, then build an environment capable of finding sequences that make it false.

I think that distinction becomes even more important with AI.

If the implementation itself is increasingly generated, the scarce human contribution starts moving toward specifying the properties the implementation must not violate.

Money cannot disappear. Authorization cannot silently be bypassed. Acknowledged writes cannot vanish. One customer cannot observe another customer’s data.

The system needs enough information about what “correct” means that the simulated world can prove it wrong.

Don’t ask humans to predict every failure. Build a world capable of discovering them.

What If Code Isn’t the Main Control Surface?

StrongDM’s Software Factory is one of the more interesting examples I’ve seen of pushing this idea pretty far.

StrongDM describes an engineering process where humans aren’t writing the generated code and aren’t reviewing it in the traditional line-by-line sense either. Instead, correctness is established primarily by observing what the system actually does.

That’s a pretty aggressive constraint, and it doesn’t mean everyone should throw away code review tomorrow.

But the architecture of the idea is interesting.

If source inspection stops being your primary control surface, the engineering work doesn’t disappear.

It moves.

Now you need to get much better at describing desired behavior, defining properties that must hold, constructing realistic environments, generating failures, observing outcomes, and deciding whether the system actually did what you intended.

You didn’t remove engineering.

You moved the control surface up a level.

StrongDM’s Digital Twin Universe makes that idea concrete. Instead of tiny mocks, they build behavioral replicas of dependencies like Okta, Jira, Slack, Google Docs, Google Drive, and Google Sheets so software can interact with something that behaves more like the real environment.

That lets them run thousands of scenarios, inject failures, replay conditions deterministically, and exercise cases that would be expensive or dangerous against the real services.

The more I looked at that, the more I started thinking that the interesting artifact isn’t just the software anymore.

It’s the world around the software.

If implementation keeps getting cheaper, maybe it becomes rational to spend dramatically more effort constructing the world the implementation has to survive in.

Twin the dependencies.

Twin the infrastructure.

Twin the users.

Twin the economics.

Twin the failures.

Then put the system inside that world and see what happens.

Assertion → Scenario → Trajectory → History

Once you start building that world, another problem appears.

Suppose the requirement is:

The user should be able to complete checkout.

An agent can make that test pass while still building the wrong system. Maybe it bypasses fraud checks. Maybe it disables authorization. Maybe the user completes checkout perfectly while the business loses money on every transaction.

The assertion is green.

The system is wrong.

That’s reward hacking showing up inside software engineering.

This is why I like the progression:

Assertion → Scenario → Trajectory → History

An assertion asks whether some condition is true.

A scenario asks whether the user actually accomplished something.

A trajectory asks how the system accomplished it.

And a history asks what happens when thousands of those trajectories accumulate over time.

Each layer makes it harder to satisfy the letter of the requirement while violating what we actually meant.

Imagine the synthetic user successfully completes checkout. Great.

Now inspect the trajectory.

Did authentication work normally? Did payment flow through the intended service? Did retries behave correctly? Did latency remain acceptable? Did the system quietly route through some deprecated dependency? Did it bypass a security control to make the path easier?

The final outcome alone doesn’t tell you whether you should trust the system.

You need the path.

And even the trajectory only tells you about one interaction.

Systems accumulate.

So do engineering decisions.

Which brings us to history.

Deterministic Simulation, But for Engineering Decisions

Traditional deterministic simulation asks:

What happens if we explore lots of possible sequences of system events?

A longitudinal engineering simulator asks:

What happens if engineering decisions themselves become part of the sequence?

Suppose the agent chooses an architecture in simulated month one. It works beautifully, but changes infrastructure cost.

By month four, those costs start to matter, so the agent makes another decision. That reduces cost but introduces a reliability problem.

In month seven, the agent adds a service to fix the reliability issue. That works too, but now the system is more operationally complex.

Six months later, that complexity makes another product change substantially harder.

None of those decisions has to be obviously wrong.

That’s the point.

Decision N changes the environment in which decision N+1 happens.

The benchmark is no longer asking whether each patch is correct in isolation. It’s asking whether a series of locally reasonable decisions adds up to a healthy system.

A deterministic simulator can ask whether software survives its own state transitions.

Get Owen Zanzal’s stories in your inbox

Join Medium for free to get updates from this writer.

A longitudinal simulator can ask whether an engineer survives its own decisions.

Can you live with what you built?

Make the Benchmark Endogenous

This gives us a useful property for AI evaluation: the test becomes endogenous.

The exact evaluation isn’t fully specified before the agent starts. It emerges from inside the system.

The agent makes a decision. That decision changes performance, cost, architecture, user behavior, or operational complexity. The simulator generates the consequences, and those consequences create the next decision.

So the exact state the agent sees six months from now depends partly on what it did today.

There is no fixed question 37 waiting in a file somewhere.

Your answer to question 12 helped create question 37.

That matters for evaluation because the exact history being tested can only exist after the benchmark starts. It doesn’t eliminate everything a model might have learned during training, but it makes a precomputed answer key much less useful.

The world responds to the agent.

Run the Clock

This is where I think the benchmark gets really interesting.

Give the agent a system.

Give it users, infrastructure, telemetry, objectives, constraints, and an engineering budget.

Then run time.

A week.

A month.

A year.

Maybe three years.

While that happens, change the environment. Increase traffic. Change the user population. Deprecate a dependency. Change infrastructure pricing. Introduce a security incident. Change product requirements. Add a competitor.

Now the agent has to choose.

Build the feature?

Fix reliability?

Migrate the database?

Pay down technical debt?

Accept the cost for another quarter?

Stop doing something entirely?

Give it enough telemetry to make those decisions: errors, latency, infrastructure costs, retention, support volume, user behavior.

And make the signals disagree.

Maybe a feature increases engagement but costs too much to operate. Maybe a reliability change improves availability while making every request slower. Maybe an optimization helps power users while destroying onboarding.

Maybe every technical dashboard is green while users quietly stop coming back.

Now the problem isn’t:

Which metric should I maximize?

It’s:

What kind of system are we trying to maintain?

That’s where the evaluation stops looking like coding and starts looking like stewardship.

The Artifact Isn’t the Repository

At the end of a benchmark like this, I’m not sure the final repository is even the most interesting artifact.

I want the history.

What broke first? Which signal exposed it? What did the agent think was happening? What did it change? Did the intervention work? What else did that change break?

Did the agent recognize when an earlier decision had become a problem? Did it reverse bad decisions? When did technical debt accumulate? When did it choose to pay that debt down?

What happened to reliability, users, cost, and operational complexity?

What happened when two objectives directly conflicted?

That gives you something different:

an operating history.

And my suspicion is that this history could tell us much more about engineering ability than whether someone — or some model — can solve a collection of isolated implementation problems.

That’s a hypothesis.

But it’s one we can test.

A Three-Year Software Engineering Benchmark

The benchmark itself is almost annoyingly simple to describe:

Here is a system. Own it for three simulated years.

Users arrive.

Things fail.

Requirements change.

Infrastructure degrades.

The agent gets telemetry and decides what to investigate.

It proposes changes. Those changes alter the system. The world responds.

Then the next period begins.

Don’t score each patch independently.

Score the history.

What happened to user outcomes? Reliability? Cost? Security? Recovery behavior? Technical debt? Adaptability? Operational complexity?

And don’t give it one neat number to optimize.

Real systems don’t work like that.

A system that achieves perfect reliability by refusing every request is useless. A system growing rapidly while becoming economically impossible to operate isn’t healthy either.

You’re looking for something more subtle.

Stewardship.

From Coding to Stewardship

I think that word captures the shift better than “systems thinking.”

Stewardship assumes you’re responsible for something that has to keep living. You don’t get to optimize one moment in isolation.

You have to understand the state of the system now, the pressures acting on it, and how today’s intervention changes tomorrow’s options.

Can the agent distinguish a symptom from a root cause?

Can it recognize when an architecture that made sense in year one has become a bottleneck in year three?

Can it decide when technical debt is worth accepting, and when it needs to be paid down?

Can it improve reliability without making the product unusably slow?

Can it reduce costs without creating an operational nightmare?

Can it make hundreds of locally sensible decisions that add up to a globally healthy system?

That’s much closer to the job.

At that point, software starts looking a little less like laying bricks and a little more like city planning. You’re managing infrastructure, economics, incentives, maintenance, human behavior, and unintended consequences.

The implementation still matters.

But the difficult part is how everything interacts.

So, Don’t Learn to Code?

The title is deliberately provocative.

Programming still matters. Understanding software still matters. Architecture matters. Databases matter. Networks matter. Security matters. Debugging definitely matters.

I’m not arguing that everyone should stop understanding how computers work because the autocomplete got better.

I’m arguing that the abstraction we should aspire to operate at is moving.

If AI continues making implementation cheaper, then implementation becomes less of the scarce resource.

The scarce resource becomes knowing what should exist, which properties must hold, what you’re actually optimizing for, and how to construct a world capable of proving you wrong.

It becomes knowing how to separate signal from noise, how to recognize when a local improvement creates a global problem, and how today’s change alters tomorrow’s choices.

Maybe the future software engineer spends less time asking:

How do I implement this?

And more time asking:

What would have to be true for me to trust this system?

Then you build the world that can answer that question.

Specify the system. Define its invariants. Build its world. Instrument it. Simulate the users. Simulate the dependencies. Generate the failures.

Break it. Replay it. Understand what happened. Change the system. Measure the consequences.

Then keep going.

Because the real challenge isn’t producing the code.

It’s building a world that forces you to confront the questions you didn’t know to ask, and then living with the answers.

Run the clock.

--

--