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.