System Design
Principles for building reusable, maintainable coding systems. From "A Philosophy of Software Design" by John Ousterhout.
Core Principle: Fight Complexity
Complexity is the root cause of most software problems. It accumulates incrementally—each shortcut adds a little, until the system becomes unmaintainable.
Complexity defined: Anything that makes software hard to understand or modify.
Symptoms:
- •Change amplification: simple change requires many modifications
- •Cognitive load: how much you need to know to make a change
- •Unknown unknowns: not obvious what needs to change
Deep Modules
The most important design principle: make modules deep.
┌─────────────────────────────┐ │ Simple Interface │ ← Small surface area ├─────────────────────────────┤ │ │ │ │ │ Deep Implementation │ ← Lots of functionality │ │ │ │ └─────────────────────────────┘
Deep module: Simple interface, lots of functionality hidden behind it.
Shallow module: Complex interface relative to functionality provided. Red flag.
Examples
Deep: Unix file I/O - just 5 calls (open, read, write, lseek, close) hide enormous complexity (buffering, caching, device drivers, permissions, journaling).
Shallow: Java's file reading requires BufferedReader wrapping FileReader wrapping FileInputStream. Interface complexity matches implementation complexity.
Apply This
- •Prefer fewer methods that do more over many small methods
- •Hide implementation details aggressively
- •A module's interface should be much simpler than its implementation
- •If interface is as complex as implementation, reconsider the abstraction
Strategic vs Tactical Programming
Tactical: Get it working now. Each task adds small complexities. Debt accumulates.
Strategic: Invest time in good design. Slower initially, faster long-term.
Progress │ │ Strategic ────────────────→ │ / │ / │ / Tactical ─────────→ │ / ↘ (slows down) │ / └──┴─────────────────────────────────→ Time
Rule of thumb: Spend 10-20% of development time on design improvements.
Working Code Isn't Enough
"Working code" is not the goal. The goal is a great design that also works. If you're satisfied with "it works," you're programming tactically.
Information Hiding
Each module should encapsulate knowledge that other modules don't need.
Information leakage (red flag): Same knowledge appears in multiple places. If one changes, all must change.
Temporal decomposition (red flag): Splitting code based on when things happen rather than what information they use. Often causes leakage.
Apply This
- •Ask: "What knowledge does this module encapsulate?"
- •If the answer is "not much," the module is probably shallow
- •Group code by what it knows, not when it runs
- •Private by default; expose only what's necessary
Define Errors Out of Existence
Exceptions add complexity. The best way to handle them: design so they can't happen.
Instead of:
function deleteFile(path: string): void {
if (!exists(path)) throw new FileNotFoundError();
// delete...
}
Do:
function deleteFile(path: string): void {
// Just delete. If it doesn't exist, goal is achieved.
// No error to handle.
}
Apply This
- •Redefine semantics so errors become non-issues
- •Handle edge cases internally rather than exposing them
- •Fewer exceptions = simpler interface = deeper module
- •Ask: "Can I change the definition so this isn't an error?"
General-Purpose Modules
Somewhat general-purpose modules are deeper than special-purpose ones.
Not too general: Don't build a framework when you need a function.
Not too specific: Don't hardcode assumptions that limit reuse.
Sweet spot: Solve today's problem in a way that naturally handles tomorrow's.
Questions to Ask
- •What is the simplest interface that covers all current needs?
- •How many situations will this method be used in?
- •Is this API easy to use for my current needs?
Pull Complexity Downward
When complexity is unavoidable, put it in the implementation, not the interface.
Bad: Expose complexity to all callers. Good: Handle complexity once, internally.
It's more important for a module to have a simple interface than a simple implementation.
Example
Configuration: Instead of requiring callers to configure everything, provide sensible defaults. Handle the complexity of choosing defaults internally.
Design Twice
Before implementing, consider at least two different designs. Compare them.
Benefits:
- •Reveals assumptions you didn't know you were making
- •Often the second design is better
- •Even if first design wins, you understand why
Don't skip this: "I can't think of another approach" usually means you haven't tried hard enough.
Red Flags Summary
| Red Flag | Symptom |
|---|---|
| Shallow module | Interface complexity ≈ implementation complexity |
| Information leakage | Same knowledge in multiple modules |
| Temporal decomposition | Code split by time, not information |
| Overexposure | Too many methods/params in interface |
| Pass-through methods | Method does little except call another |
| Repetition | Same code pattern appears multiple times |
| Special-general mixture | General-purpose code mixed with special-purpose |
| Conjoined methods | Can't understand one without reading another |
| Comment repeats code | Comment says what code obviously does |
| Vague name | Name doesn't convey much information |
Applying to CLI/Tool Design
When building CLIs, plugins, or tools:
- •Deep commands: Few commands that do a lot, not many shallow ones
- •Sensible defaults: Work without configuration for common cases
- •Progressive disclosure: Simple usage first, advanced options available
- •Consistent interface: Same patterns across all commands
- •Error elimination: Design so common mistakes are impossible
Example: Good CLI Design
# Deep: one command handles the common case well swarm setup # Not shallow: doesn't require 10 flags for basic usage # Sensible defaults: picks reasonable models # Progressive: advanced users can customize later
Key Takeaways
- •Complexity is the enemy. Every design decision should reduce it.
- •Deep modules win. Simple interface, rich functionality.
- •Hide information. Each module owns specific knowledge.
- •Define errors away. Change semantics to eliminate edge cases.
- •Design twice. Always consider alternatives.
- •Strategic > tactical. Invest in design, not just working code.
- •Pull complexity down. Implementation absorbs complexity, interface stays simple.