The Pragmatic Programmer After the Memory Wall
What still holds when agents write code quickly, hardware punishes indirection, and cloud control planes fail at global scale.
I do not think The Pragmatic Programmer has aged out. I think the environment around it has become less forgiving.
The book is not really about tools. It is about engineering posture: take responsibility, keep systems easier to change, make feedback loops tight, test what can fail, and refuse to live with broken windows. That posture still holds. What changed is the failure surface.
In 2026, a code agent can produce a week of mediocre abstraction before lunch. A laptop-class chip can execute billions of instructions per second and still spend the hot path waiting on scattered memory. A global control plane can replicate bad state across regions faster than a human can open the incident channel.
That does not make pragmatism obsolete. It makes the old advice more literal.
ETC Has a Physical Layer
The book’s central design rule is ETC: easier to change. That rule is still the right one. The mistake is treating “easier to change” as a synonym for “more abstract.”
That is how codebases end up with a hierarchy for every noun, an interface for every class, and a dependency-injected trail of breadcrumbs between one integer and the next. It feels civilized in review. It often benchmarks like a pile of receipts.
Modern CPUs do not run class diagrams. They fetch cache lines.
Intel’s Lunar Lake design puts more attention on power, memory proximity, and a memory-side cache. Apple’s M5 pushes unified memory bandwidth to 153 GB/s. Those are not licenses to ignore locality. They are evidence that silicon vendors are spending real die area and packaging complexity to hide the cost of moving data.
Software can still defeat all of it with pointer chasing.
The common agent failure is not exotic. Ask for a particle update loop, a pricing pass, a simulation tick, or a ranking transform, and the default output often looks like this:
struct Particle {
float x;
float y;
float z;
float vx;
float vy;
float vz;
void step(float dt) {
x += vx * dt;
y += vy * dt;
z += vz * dt;
}
};
std::vector<Particle> particles;That shape is fine until the loop matters. Then every iteration drags fields through memory as an object bundle, whether the CPU needs all of them or not.
For a hot loop, I want the data shaped around the access pattern:
struct ParticleBlock {
std::vector<float> x;
std::vector<float> y;
std::vector<float> z;
std::vector<float> vx;
std::vector<float> vy;
std::vector<float> vz;
void step(float dt) {
for (size_t i = 0; i < x.size(); ++i) {
x[i] += vx[i] * dt;
y[i] += vy[i] * dt;
z[i] += vz[i] * dt;
}
}
};
This is not an argument for flattening the whole application into arrays. It is an argument for performance tiers.
Business policy can afford indirection. Hot loops, storage engines, serialization paths, rendering, compression, matching, ranking, and simulation usually cannot. In those places, ETC means the future maintainer can find the data flow, predict the memory access, and benchmark the change without spelunking through ceremony.
The pragmatic move is not “object-oriented” or “data-oriented.” The pragmatic move is knowing which part of the system is paying rent to the cache hierarchy.
Big-O Is Missing the Invoice
Big-O is not wrong. It is incomplete.
It throws away constants because that is what makes asymptotic reasoning useful. The machine puts those constants back with interest. A branch mispredict, a cold cache line, a TLB miss, a failed prefetch, and a recursive call frame all live outside the clean little expression.
That is why production sorting implementations use hybrids. The high-level algorithm carries the asymptotic guarantee. The small-partition fallback respects the machine.
For tiny contiguous ranges, insertion sort can beat a theoretically superior algorithm because it walks memory in a boring pattern. Boring is a feature. The hardware can prefetch it. The branch predictor can learn it. The compiler can see it.
The lesson is not “always use insertion sort.” That would be a cargo cult with a better haircut. The lesson is that the crossover point belongs to the benchmark, not the blog post.
This is the update I would write into the margin of the book: estimate first, then measure at the physical boundary. If the path is CPU-bound, measure cache misses. If it is I/O-bound, measure queueing and tail latency. If it is distributed, measure retries, coordination, and blast radius.
An asymptotic proof is the start of the conversation. A profile is where the machine gets a vote.
Agents Make Broken Windows Cheap
The book’s broken-window rule gets more important when code is cheap.
A human usually leaves a broken window one commit at a time: a vague name, a duplicated branch, a swallowed exception, a test skipped because the release is late. An agent can stamp out the same damage across twenty files with a confidence that feels suspiciously like authority.
The dangerous part is not that agent-generated code is bad. The dangerous part is that it is plausible.
It compiles. It uses the local framework. It names things in the house style. It may even come with tests. Then the system gets a new retry loop without jitter, a cache without invalidation, a transaction boundary around network I/O, or a catch-all handler that turns a real fault into quiet data corruption.
That changes the human job.
I do not review agent output as prose. I review it as an untrusted patch from a fast junior engineer with perfect typing and no memory of production.
The review checklist is blunt:
What invariant does this code claim to preserve?
Where does it fail closed, fail open, or fail loud?
What resource does it acquire, and where is that resource released?
What happens under retry, cancellation, partial failure, and duplicate delivery?
Which test would have failed before the fix?
Which benchmark proves the abstraction did not move the bottleneck?
That is not anti-agent. It is pro-accountability.
I would also keep an agent audit trail for serious systems. Not a theatrical “AI disclosure” badge. Useful metadata: model family, tool permissions, prompt or task summary, human approver, files touched, tests run, and any ignored failures.
The point is not blame. The point is defect analysis. If one model or workflow repeatedly creates the same kind of race, leak, or injection bug, I want that pattern visible.
Pride of ownership still means what it meant in the book. I signed the change. I own the blast radius.
Paranoia Survived Contact With Reality
The strongest part of The Pragmatic Programmer is its distrust of happy paths.
That distrust looked almost old-fashioned during the era of framework optimism. It does not look old-fashioned after the 2025 cloud incidents.
AWS published a summary of the October 19-20, 2025 DynamoDB disruption in us-east-1. The root trigger was a latent race condition in DynamoDB’s automated DNS management system. A stale DNS plan was applied, cleanup removed the active regional endpoint addresses, and DynamoDB endpoint resolution failed. The primary DynamoDB disruption recovered in hours, but EC2 instance launch recovery took far longer because dependent control-plane systems had entered congestive collapse.
That is the part worth studying. The first bug was a race. The real lesson was coupling.
A regional DNS management defect impaired a foundational datastore. Services that depended on that datastore could not make forward progress. Recovery created a herd effect. The system then needed throttling, restarts, and careful queue reduction before it could breathe again.
Google Cloud’s June 12, 2025 incident was smaller in duration but cleaner as a lesson. Google described a Service Control change for quota policy checks that lacked the right error handling and was not protected by a feature flag. A policy update inserted blank fields into regional Spanner tables. The metadata replicated globally within seconds. The null pointer path crashed Service Control binaries across regions and some restarting tasks created a herd effect on the underlying Spanner table.
The bug was not mysterious. The propagation path was.
This is pragmatic paranoia in production language:
State that can replicate globally needs staged propagation.
Automated deletion needs velocity limits.
Critical policy paths need feature flags that default off.
Control planes need failure isolation from the services they manage.
Restart loops need jitter and backoff before they become load generators.
Monitoring cannot depend entirely on the system it is meant to diagnose.
“Crash early” is still good advice inside a bounded fault domain. It is reckless advice when the crash loop can synchronize globally.
The rule I use is simple: crash local, recover global. Assert at the boundary where bad state enters. Quarantine the bad input. Keep the serving path degraded if safety allows it. Make the supervisor boring, bounded, and explicit.
The Merge Queue Is Now Architecture
The book treats automation as a professional baseline. That part now includes the merge queue.
When agents produce many small patches, a sequential queue becomes a throughput bottleneck and a correctness risk. A long queue encourages bigger batches. Bigger batches make failures harder to isolate. Harder isolation creates slower review. Slower review invites more automation to pile up behind it.
That is a feedback loop, not a tooling inconvenience.
Speculative CI is the right mental model. Test the next few queued changes against predicted future states of main. If patch A is expected to land, test B on top of A. If A fails, throw away the speculation and recompute. The trade is obvious: spend more compute to protect human attention and reduce queue latency.
The expensive part is deciding what to test.
I want deterministic test selection based on ownership and dependency graphs. I want property tests around parsers, protocol boundaries, money movement, authorization, and data structure invariants. I want replay of historical counterexamples before random generation burns a cluster. I want performance checks only where the patch touches a known hot path.
The starter kit is no longer just formatter, unit tests, and CI. For agent-heavy development, the starter kit is the operating system for trust.
What I Would Change In My Copy
I would not rewrite The Pragmatic Programmer. I would add margin notes.
Next to ETC, I would write: “Easier to change includes easier to profile.”
Next to DRY, I would write: “Agents duplicate intent more often than text.”
Next to Design by Contract, I would write: “Contracts are the spec the agent does not get to improvise.”
Next to Crash Early, I would write: “Only inside a fault domain with a supervisor.”
Next to Don’t Live With Broken Windows, I would write: “Machine-generated broken windows still count.”
Next to Pragmatic Teams, I would write: “The human team owns context. The agent owns nothing.”
That last one is the real update.
The industry keeps trying to turn software engineering into text generation. The book’s answer is still better: software engineering is judgment under constraint. Tools change the cost curve. They do not remove responsibility.
I want agents in the workflow. I also want cache-aware data structures, explicit invariants, boring release controls, and incident reports that name the coupling. The distinguished engineer in 2026 is not the person who types the most code. It is the person who can still see the system after the code becomes cheap.


