Craft of code
Software complexity
Complexity shows up as change amplification, cognitive load and unknown unknowns. The next stranger's change is the cost that matters.
Software spends most of its life being changed by people who did not write it. The feature you ship this week will be edited next quarter by a teammate who has never opened the file, under a deadline, at 11pm, with half the original context gone. Maintainable code is not "pretty" code. It is code that keeps the cost of that next change low.
The enemy is complexity: anything that makes the system hard to understand or hard to modify. John Ousterhout's framing is useful because it separates three symptoms that feel different but share a root cause.
The big idea
Concept
Complexity (in Ousterhout's sense) shows up as change amplification (one logical change requires edits in many places), cognitive load (how much you must hold in your head to work safely), and unknown unknowns (you cannot tell what you need to know until something breaks).
Fighting complexity is the day job of craft. Clever one-liners, premature abstractions, and "clever" frameworks often increase it. Simple interfaces over rich behavior tend to decrease it.
Treat complexity like a budget you spend on purpose. A payment path can afford more structure than a one-off admin script. Ask on every non-trivial PR:
- Does this change increase the number of places a future edit must touch?
- Does a new reader need a map of five files to understand one user action?
- Did I introduce a name (
Manager,Util,Helper) that attracts unrelated logic?
If yes, spend an extra hour now extracting or renaming. That hour is cheaper than the next incident's three hours of archaeology.
Beck's Tidy First? framing helps under deadline pressure: separate the structural commit that makes the change easy from the behavioral commit that delivers the feature. Reviewers can approve a rename with confidence; they can scrutinize the behavior diff without the noise of moves.
Walking through a smell table
| Smell | What goes wrong | Prefer |
|---|---|---|
| God class / god file | Everything depends on it; merges conflict constantly | Split by responsibility along change boundaries |
| Anemic domain + fat controllers | Business rules scattered in HTTP handlers | Domain functions/modules with clear inputs/outputs |
| Premature framework | Team learns the framework instead of the problem | Boring code until a second real use case appears |
| Clever one-liners | Cognitive load spikes; hard to debug | Straightforward control flow |
| Shotgun surgery | One feature edit sprays across the tree | Deep module owning that feature's rules |
| Leaky abstractions | Callers still need low-level knowledge | Hide retries, encoding, persistence details |
"Clean code" checklists that obsess over line counts without looking at change amplification produce shallow modules. Measure success by how calmly the next person can ship a related change.
A weekly habit: leave one name or boundary clearer than you found it. When a bug takes more than an hour to understand, add a comment or test that encodes the invariant. Once a month, skim a module you own for shotgun-surgery hotspots. Prefer deleting dead code to decorating it.
Common mistakes
- Optimizing for "pretty" instead of the next change.
- Measuring success by line count. Shallow files can still amplify change.
- Mixing tidy commits with behavior commits so reviewers cannot see either.
Check yourself
A one-line product change forces edits in eight files. Which complexity symptom is that?
Change amplification.
You cannot tell whether a refund path already handles duplicate webhooks until production double-pays. Which symptom?
Unknown unknowns — the invariant was not in the interface, a test, or a comment.
Key takeaways
- Optimize for the next change by a stranger.
- The three symptoms: change amplification, cognitive load, unknown unknowns.
- Spend complexity budget on purpose. Separate tidy commits from behavior commits.
- Line-count checklists that ignore change amplification produce shallow modules.
Further learning
Next, the main weapon against complexity: deep modules.
Related
Writing code others can maintain
Managing complexity with deep modules, clear naming, honest comments, consistent style, and error strategies that shrink the blast radius of change.
Deep modules
A deep module hides a lot of complexity behind a small interface. Shallow pass-through layers add names without adding leverage.
Refactoring and legacy code
Change code you did not write without breaking it. Start here for characterization, seams, tiny steps, and incremental migrations.