The Fast Way or The Right Way: Do We Really Need A Strategy?

AI agents write code fast. Very fast. “The fast way” has never been more seductive. Get the feature working. Ship it. Move on. Every shortcut feels justified. There’s a deadline. The workaround is “temporary.” So you add a few annotations and look the other way: @SuppressWarnings “GodClass”, “TightCoupling”, “TooManyParameters”. The technical debt will be paid back “later”. Business loves it. Arguments about code that’s “harder to change in the future” rarely land. The future is someone else’s problem. John K. Ousterhout has a name for this. In his book A Philosophy of Software Design, he calls it tactical programming. And he argues it is quietly destroying your codebase - one suppressed warning at a time.
But shortcuts compound. What began as a pragmatic workaround becomes, over months and years, a system that takes weeks to modify safely. The system has become complex.
Ousterhout defines complexity simply: anything that makes a system harder to understand or modify.
It shows up in three ways.
- Change amplification. You need to fix one thing. You end up touching twelve files. Each change reveals another change. A one-line fix becomes a two-day excavation.
- Cognitive load. Nobody can hold the whole system in their head anymore. Every task requires reading old code for an hour before writing a single line. New developers take months to become productive. Senior developers become bottlenecks - because they are the only ones who remember why things are the way they are.
- Unknown unknowns. The worst kind. You don’t know what you don’t know. You make a change that looks correct. It breaks something three modules away. Nobody saw it coming - because nobody could. This is where tactical programming leads. Not in a single catastrophic moment. Gradually, then suddenly.
What is Strategic Programming?
Strategic programming starts from a different premise: your primary goal is to produce a great design that also happens to work, not working code that happens to have some design.
It does not mean over-engineering. It does not mean Big Design Up Front. It means treating every feature, every change, every pull request as a chance to leave the system slightly better than you found it. The cost is real but small. Ousterhout estimates strategic programming takes roughly 10–15% more time upfront. That is one extra hour in a ten-hour week.
So what does it look like in practice? Here are the key principles Ousterhout builds his case around.
Deep Modules
Deep modules is a main tool of Strategic Programming. A module - whether a class, a function, a service, or a library - has two faces: its interface and its implementation. The interface is what users of the module must know. The implementation is what it actually does. Ousterhout’s insight is that the best modules have a simple, narrow interface concealing a rich, complex implementation.
Deep module:
public interface GeneralLedger {
String post(JournalEntry entry);
Deep module caller: One line - caller knows nothing
String id = ledger.post(entry);
Shallow module: Three separate interfaces the caller must know about and manage together
public interface EntryValidator {
void validateBalance(List<EntryLine> lines);
void validateAccountsExist(List<EntryLine> lines, Map<String, AccountType> chart);
void validatePeriodOpen(LocalDate date, Set<YearMonth> closedPeriods);
}
public interface AuditLog {
String append(JournalEntry entry);
}
public interface BalanceUpdater {
void apply(List<EntryLine> lines,
Map<String, BigDecimal> runningBalances,
Map<String, AccountType> chart);
}
Shallow module caller:
validator.validateBalance(entry.lines());
validator.validateAccountsExist(entry.lines(), chart);
validator.validatePeriodOpen(entry.date(), closedPeriods);
String id = auditLog.append(entry);
balanceUpdater.apply(entry.lines(), runningBalances, chart);
The caller now owns the internal state (chart, runningBalances, closedPeriods), must know the correct ordering of operations, and must remember to call all three steps every time. If you forget validatePeriodOpen, the system silently posts to a closed period with no error.
The Danger of Over-Decomposition
Small classes and short functions are not inherently good. Decomposition should be driven by abstraction, not by line counts. The right question when splitting a method is not “is this too long?” but “does splitting this create a meaningful new abstraction, or does it just make the logic harder to follow?”
Information Hiding and Information Leakage
Central to deep module design is the concept of information hiding: each module should know as little as possible about the internals of the others it works with. When two modules share knowledge of an internal detail - a particular data format, an implementation assumption, a specific ordering of operations - that knowledge has leaked. Now a change to one module requires a change to the other. The modules are no longer truly independent; they are tightly coupled through hidden shared state.
A module is loosely coupled when it depends only on the narrow public interface of another module - not on its internal structure, implementation details, or internal state. A change inside one module should not force changes in others.
The question to ask when designing an interface is: what is the minimum interface callers need to know to use interface correctly? Everything else should be hidden.
Comments as Design: Writing Code for Humans
Comments are not optional, and they are not just for complex code. They are a first-class part of good design. Code tells you What a system does. Well-written comments tell you Why.
Principle of designing twice
Before committing to an implementation, sketch two or three genuinely different approaches to the problem. Not minor variations - different abstractions, different decompositions, different ways of thinking about the problem entirely. Then compare them honestly. The first solution that comes to mind is rarely the best one.
Team work
Individual developers can practice strategic programming, but its full benefits require team-level commitment. Schedule pressure is real. Strategic design is not overriding deadlines. At the same time, teams should be honest about the cost of tactical decisions and build in time to address them - not in some hypothetical future sprint, but continuously, as part of normal development.
Complexity is the root cause of most software failures. Simplicity is a discipline, not a talent. Technical excellence it is not clever architecture. It is not elaborate patterns. It is one question, asked at every step: will the next developer find this easier or harder than I did? And in the age of AI - that question matters more, not less. An AI agent is only as good as the system it writes against. Point it at clean interfaces, hidden information, and meaningful abstractions: it produces good code fast. Point it at a tangled, tactical codebase: it produces more tangle, faster. AI does not fix complexity. It amplifies whatever is already there. The fast way was always an illusion. Now it just moves quicker.
Sources: