Which trade-offs actually shifted
Most architectural advice survives unchanged. Four trade-offs genuinely moved, and knowing which matters more than any new principle.
There is a temptation to declare that everything about software design has changed. It has not. Cohesion, coupling, clear boundaries and good names are still what makes a system workable, for exactly the same reasons.
Four specific trade-offs have moved, though, and they moved because the cost of writing code fell while the cost of understanding it did not.
1. Duplication versus indirection
Shifted toward tolerating duplication. The classic argument for extracting a shared abstraction was partly that writing the second copy was expensive. It is not any more. Meanwhile the cost of indirection — following an abstraction across five files — is paid by every reader, human and agent, on every visit.
This does not license copy-paste everywhere. It shifts the threshold: three similar-but-not-identical implementations that each read top to bottom are now often better than one parameterised abstraction with five options, where they were previously a clear smell.
2. Explicit versus implicit
Shifted hard toward explicit. Convention-over-configuration frameworks rely on knowledge that exists nowhere in the repository. A human learns the convention once; an agent re-derives it every session from whatever is in context, and gets it wrong when the convention is unusual.
Explicit wiring costs lines and saves an entire category of confident wrong guesses. The same applies to magic: decorators that do three things, metaprogramming, implicit registration by file location.
3. Cleverness versus searchability
Shifted hard toward searchability. An agent finds code by grep. A dynamically constructed method name, a handler registered by convention, a string assembled at runtime — all of these are invisible to search, and therefore effectively invisible.
// invisible: grep for "handleOrderCreated" finds nothing
const handler = handlers[`handle${pascalCase(event.type)}`]
// findable
const handler = {
'order.created': handleOrderCreated,
'order.refunded': handleOrderRefunded,
}[event.type]4. Test coverage versus test quality
Shifted toward quality, sharply. Coverage was always a weak proxy. Now that generating tests is nearly free, coverage is trivially achievable and says almost nothing, while the value of tests that genuinely discriminate correct from incorrect has gone up — because they are what lets you delegate.
What did not shift
- Boundaries and cohesion. Still the main determinant of whether a system stays workable.
- Simplicity. Cheap generation makes complexity cheaper to produce and no cheaper to live with.
- Naming. If anything more important, since names are a large part of what an agent has to reason from.
- The cost of a bad data model. Unchanged and still the most expensive mistake available.
- Deleting code. Still the best refactor, and now more valuable because dead code becomes context.
The one-sentence summary
Optimise for a reader who is fast, tireless, has no memory between visits, and can only see what is in front of them. That description fits an agent, and it also fits a new engineer on their first week — which is why almost everything in this module is good design advice that happens to have become more urgent.
Exercise
Find the most clever piece of code in your system — dynamic dispatch, metaprogramming, convention-based registration. Try to locate its behaviour using only grep, as an agent would.
Time how long it takes. Then decide honestly whether the cleverness is paying for what it costs every reader, every time.
Worked solution
The candidate: an event handler system using convention-based registration by filename.
Question: "what happens when an order.refunded event arrives?" grep -r "order.refunded" . -> 3 hits, all in test fixtures and one JSON schema. No handler. The handler lived in src/events/order/refunded.ts and was registered by a loader that walked the directory and derived the event name from the path. The string "order.refunded" appeared nowhere in the handler. Time for me, knowing the system: 4 minutes. Time for an agent, no prior knowledge: it never found it. It reported that no handler existed and proposed writing one.
Three separate sessions in which an agent concluded a handler did not exist and proposed creating a duplicate. Two were caught in review. One was not, and shipped a second handler for order.created which ran alongside the first for nine days, double-sending a customer email.
// src/events/registry.ts - one file, explicit, greppable
export const HANDLERS = {
'order.created': handleOrderCreated,
'order.refunded': handleOrderRefunded,
'order.shipped': handleOrderShipped,
// ... 31 entries
} satisfies Record
The loader was deleted. 31 lines of explicit registration replaced 40
lines of directory-walking magic.
Bonus, unplanned: the `satisfies Record` made it a
compile error to have an event type with no handler. The old system
silently dropped unhandled events, which it turned out had been
happening for one event type for four months. Thirty-one lines of boring, repetitive, explicit code replaced a clever loader, made the system greppable, and surfaced a four-month silent bug through the type system. The duplication-versus-indirection trade-off and the explicit-versus-implicit one both pointed the same way, and the compile-time exhaustiveness was free once the mapping was a value rather than a convention.
Takeaways
- Four trade-offs shifted: duplication over indirection, explicit over implicit, searchable over clever, quality over coverage.
- Boundaries, cohesion, simplicity, naming and data modelling did not shift at all.
- Optimise for a reader who is fast, tireless, has no memory, and sees only what is in front of them.
- Explicit registration as a typed value gives you exhaustiveness checking for free.
Check yourself
Why is convention-based registration by filename particularly costly now?
Agents locate code by search. A mapping that exists only as a convention between a path and a string is invisible to every search, so the reasonable conclusion from the available evidence is that nothing handles that event — which leads directly to a duplicate being written.
A course by Pieter Zandbergen