Reducing Your Tolerance for Complexity

engineering

I have been working on a CI pipeline with Codex, and at some point the pipeline started doing quite a lot.

Health checks appeared. Contracts were added around them. More validation followed. Then the tests needed to prove that all of this machinery behaved correctly.

The strange part was that none of it looked obviously bad.

Taken individually, most of the changes made sense. The implementation was coherent, the tests passed, and I could explain why each piece worked (sometimes annoyingly well).

But the more I reviewed it, the less interested I became in does this work?

The better question was: does this belong here?

A health check can be perfectly reasonable engineering. That does not mean CI should own it. Maybe it belongs in an E2E test. Maybe it only becomes meaningful after deployment. Maybe there is a much cheaper check we can run earlier.

And sometimes, after removing enough surrounding machinery, the problem does not need to be solved at all.

This is not an argument that health checks never belong in CI (an integration job may legitimately start a service and verify that it is ready). The point is that I had stopped examining the boundary itself. I was reviewing the implementation before I had properly reviewed the responsibility.

That experience has made me much less tolerant of complexity.

Not because complexity is inherently bad. Some problems are genuinely complicated, and pretending otherwise usually makes things worse.

What has changed is how cheap it has become to create complexity that looks completely reasonable.

AI made plausible complexity cheap

There used to be a natural cost between having an idea and turning that idea into software.

If I wanted another abstraction, another validation layer, or another workflow, somebody had to write it. The implementation effort did not guarantee good judgment, but it created friction. A questionable idea had at least some time to die before becoming architecture.

Coding agents remove much of that friction.

I can describe a concern and get back the types, tests, error handling, configuration, and supporting code a few minutes later. The result can look surprisingly mature.

That is mostly a good thing (I am not asking to have boilerplate back).

But the economics become strange when the cost of producing code falls much faster than the cost of understanding it.

An agent can generate a 10,000-line PR.

I still have the same brain I had before agents.

Reviewing that PR means understanding what changed, which assumptions it introduced, how those pieces interact, and whether I am willing to take responsibility for all of it. Passing tests help, but tests can only prove the behavior we asked them to prove. They cannot tell me whether the abstraction should exist in the first place.

Fred Brooks drew a useful distinction in No Silver Bullet between the essential work of software—fashioning the conceptual structure of the system—and the accidental work involved in representing that structure as code. He argued that much of the real difficulty lives in specification and design, not simply in producing the representation.

AI is remarkably good at reducing the cost of that representation.

It does not automatically reduce the cost of deciding what the representation should mean.

That changes what I need to pay attention to.

The bottleneck is moving upstream.

I am becoming less impressed by how much code an agent can produce. The more useful question is how much code I can confidently keep.

The hard part is knowing what belongs

Bad code is relatively easy to reject.

The harder case is code that looks like good engineering.

The abstraction is clean. The interface makes sense. The failure modes have been considered. Everything has tests.

And yet the system would be better without it.

This is why I have been thinking about complexity less as something that arrives through obviously bad decisions and more as something that accumulates through reasonable ones.

A problem appears, so we add something to handle it. That addition creates another state we need to account for. Something else now needs configuration. Eventually the system contains considerably more machinery than anyone explicitly intended to build.

Every decision can make sense locally while the system becomes incoherent globally.

I have been writing simplicity decays by default in my notes for this reason.

Simple systems do not remain simple because nobody touches them. They remain simple because someone keeps defending their boundaries.

Almost every new piece of complexity has an advocate. There is a concrete problem today that makes the addition useful.

Deletion has a much harder job.

To remove something, I need enough context to understand why it was introduced, whether that reason still exists, and whether something else quietly depends on it. Complexity can therefore accumulate one reasonable decision at a time while simplification requires somebody to reconstruct the history afterward.

Agents accelerate the first process enormously.

This is also making me appreciate something experienced engineers seem to develop over time: the ability to look at a perfectly functional design and feel that something is off.

People sometimes call this engineering “taste” or say that senior engineers can smell a bad design.

I used to find that description slightly unsatisfying. If the code works, the abstraction is clean, and the tests pass, what exactly are you smelling?

I think a lot of it is trajectory.

An experienced engineer has seen the clean abstraction that eventually needed exceptions. The temporary compatibility layer that somehow became permanent. The responsibility that slowly migrated into the wrong part of the system.

They are not only evaluating the code as it exists today.

They have seen enough similar systems to imagine where it tends to go.

That instinct is not infallible (experience can become superstition surprisingly easily after all), but learning to unpack it is increasingly valuable when an agent can turn a weak architectural decision into convincing code almost instantly.

Rich Hickey's Simple Made Easy is useful here. His distinction between simple and easy feels increasingly relevant as implementation gets cheaper. An agent can make something remarkably easy to add without making the resulting system simple to reason about.

Generating senior-looking code and making senior-level decisions are not the same skill.

AI just makes the gap easier to hide.

Simplicity follows clarity

My first reaction to all of this was fairly predictable: simplify more.

Delete more code. Reject more abstractions. Keep changes smaller.

Those are useful habits, but they are downstream of the harder problem.

Before I can decide what is unnecessary, I need to know what is necessary.

That was what finally made the CI work easier to reason about.

“Make the pipeline simpler” was not a useful constraint.

What is CI responsible for? was.

Once I had a clearer answer to that question, some decisions became almost boring.

If a check only makes sense against a deployed service, CI is probably not its natural home. If something can fail cheaply before commit, I do not necessarily need an elaborate CI mechanism for it. If a behavior belongs to the system as a whole, perhaps E2E is where I should prove it.

There is no universal allocation for these checks. The right boundary depends on the system.

The important part is having the boundary.

Clarity creates the ability to exclude.

Without a clear responsibility, almost every addition can justify itself. Another check makes the system safer. Another abstraction gives us flexibility. Another fallback protects against something that might happen later.

The solution space keeps expanding because we never decided where it should stop.

This is why I am becoming suspicious of “make it simple” as an engineering principle on its own.

Simple toward what?

Complexity is sometimes exactly the right trade. Reliability can require it. Scale can require it. Isolation can require it. A difficult domain can simply contain essential complexity that we do not get to wish away. Brooks made essentially this point: some of software's complexity belongs to the conceptual problem itself.

What I want is a higher bar for the complexity I choose to carry.

What does it buy?

What responsibility does it serve?

What happens if I remove it?

And, increasingly, does it belong here at all?

I started this work thinking that reducing my tolerance for complexity meant getting better at simplifying systems after they became complicated.

I think the more useful skill happens earlier.

The clearer I become about the problem, its boundaries, and what I am deliberately choosing not to solve, the less unnecessary complexity has somewhere to hide.

Simplicity follows clarity.