Software Design Principles
A Philosophy of Software Design — Design Principles
This skill contains the principles, rules, and red flags extracted from John Ousterhout’s book. The central goal of all design is reducing complexity.
How to use this as a skill design reference: See Section 17 at the bottom — it maps APoSD principles directly to skill file decisions (when to split files, what to put in descriptions, how to structure instructions, etc.).
1. COMPLEXITY: THE ENEMY
Definition
Complexity is anything related to the structure of a software system that makes it hard to understand and modify.
The 3 symptoms of complexity
- Change amplification — A seemingly simple change requires modifications in many places.
- Cognitive load — The developer must accumulate a lot of information to complete a task.
- Unknown unknowns — It is not clear which code must be modified or what information is needed.
The 2 root causes
- Dependencies: code that cannot be understood or modified in isolation.
- Obscurity: important information that is not obvious.
Core rule
Complexity is incremental. There is no single catastrophic mistake; it accumulates across hundreds of small decisions. Adopt a “zero tolerance” philosophy toward complexity.
2. STRATEGIC vs. TACTICAL PROGRAMMING
Tactical programming ❌
- Primary goal: get something working as fast as possible.
- Small complexities and hacks are introduced to meet deadlines.
- Result: accumulated complexity that slows development long-term.
- The “tactical tornado”: a very fast programmer who leaves a trail of unmaintainable code.
Strategic programming ✅
- Primary goal: a great design that also happens to work.
- Invest 10–20% of your time in continuously improving the design.
- Result: slower at first, but in months it outpaces the tactical approach.
Rule
Working code isn’t enough. Your goal is not code that works — it’s code that works AND has an excellent design.
3. DEEP MODULES
Core principle
The best modules have simple interfaces and a lot of hidden functionality. Visualize each module as a rectangle where the top edge length is the interface (cost) and the area is the functionality (benefit).
Deep module ✅
- Small interface, complex implementation.
- Example: Unix I/O — 5 system calls (
open,read,write,lseek,close) hiding hundreds of thousands of lines of code. - Example: Garbage collector — zero interface, enormous hidden complexity.
Shallow module ❌ (Red Flag)
- The interface is nearly as complex as the implementation.
- Provides little help in managing complexity.
- Example: a one-line method that simply delegates to another method.
Classitis ❌
- Syndrome of “more classes is better.”
- Small classes that contribute little real functionality.
- Result: many interfaces that accumulate into enormous system-level complexity.
Rule
It is more important for a module to have a simple interface than a simple implementation. Implementation complexity is paid once; interface complexity is paid by every user.
4. INFORMATION HIDING
Principle
Each module should encapsulate design decisions. The knowledge embedded in the implementation should not appear in the interface.
Information leakage ❌ (Red Flag)
Occurs when a design decision is reflected in multiple modules — any change requires modifying all of them.
Common cause — Temporal decomposition: structuring code according to the order of execution rather than by the knowledge it encapsulates. If two steps share knowledge (e.g., reading and parsing a file), they belong together in one module.
Practical rules
- Don’t confuse “private variable” with “hidden information.” A public getter/setter exposes the information just as much as a public field.
- If you need to merge classes to better hide information, do it. Larger classes can be simpler.
- Design modules with smart defaults: “do the right thing” without requiring the user to ask.
Red Flag: Overexposure
If the API for a commonly used feature forces users to learn about rarely used features, there is a design problem.
5. GENERAL-PURPOSE vs. SPECIAL-PURPOSE INTERFACES
Rule
Implement modules in a “somewhat general-purpose” fashion: the functionality reflects current needs, but the interface should be general enough to support multiple uses.
Benefits of generality
- Simpler, deeper interfaces.
- Better information hiding (clean separation between layers).
- Fewer methods, each more powerful.
Questions to evaluate if your interface is too specialized
- What is the simplest interface that covers all my current needs?
- In how many situations will this method be used? (If only one → red flag of over-specialization.)
- Is this API easy to use for my current needs?
6. DIFFERENT LAYER, DIFFERENT ABSTRACTION
Principle
Each layer of the system must provide a different abstraction from adjacent layers. If adjacent layers have similar abstractions, the class decomposition probably has a problem.
Red Flag: Pass-Through Method
A method that does almost nothing except pass its arguments to another method with the same signature. Indicates confusion about the division of responsibility.
Solutions: Redistribute functionality, expose the lower-level class directly, or merge classes.
Red Flag: Pass-Through Variable
A variable passed down through a long chain of methods only because some deep method needs it. Solution: Use a context object that stores the application’s global state.
Decorators
Use with caution — they tend to be shallow and create many pass-through methods. Before creating a decorator, ask:
- Can I add this functionality directly to the underlying class?
- Can I merge it with an existing decorator?
- Can it be implemented independently without wrapping everything?
7. PULL COMPLEXITY DOWNWARDS
Principle
If a module must deal with unavoidable complexity, it is better for the module to absorb it internally than to push it onto its users.
Modules have more users than developers. It is better for the developer to suffer more so that users suffer less.
Excessive configuration ❌
Configuration parameters are a way to push complexity upward instead of down. Before exporting a parameter, ask: “Will users be able to determine a better value than we can?” If the answer is no, compute it internally.
8. BETTER TOGETHER OR BETTER APART?
When to bring code together
- If they share information.
- If they are always used together (bidirectional relationship).
- If combining them simplifies the interface.
- If it eliminates duplication.
When to separate code
- Separate general-purpose code from special-purpose code.
- Lower layers should be more general; upper layers more specialized.
On method length
- Length alone is not a sufficient reason to split a method.
- Split only if it produces cleaner abstractions.
- A long method with a simple signature that is easy to read is deep — that is good.
Red Flag: Conjoined Methods
If you cannot understand the implementation of one method without understanding the other, there is a problem.
Red Flag: Repetition
If the same piece of code appears over and over, you haven’t found the right abstraction.
Red Flag: Special-General Mixture
Special-purpose code is not cleanly separated from general-purpose code in the same abstraction.
9. DEFINE ERRORS OUT OF EXISTENCE
Principle
The best way to handle exceptions is to redefine the semantics of operations so there is no error case in the first place.
Example — Tcl unset: Instead of throwing an error if the variable doesn’t exist, define
unset as “ensure the variable no longer exists.” Result: there is never an error.
Example — Unix file deletion: Unix does not fail when deleting an open file; it marks it for deletion and removes it when all processes close it. Windows fails — more complex for the user.
Techniques for reducing exceptions
- Define errors out of existence — Change the semantics so there is no error.
- Exception masking — Handle the exception at a low level; higher levels never know it occurred (e.g., TCP resends lost packets transparently).
- Exception aggregation — Handle many exceptions in one place instead of scattered handlers.
- Just crash — For errors not worth handling (e.g., out of memory in most applications).
Red Flag: Too Many Exceptions
If your class throws many exceptions, its interface is complex and shallow. Exceptions are part of the interface cost.
10. DESIGN IT TWICE
Principle
For every important design decision, consider at least two radically different alternatives. Compare pros and cons before choosing.
Why it matters
- Your first idea is rarely the best.
- Comparing alternatives develops your design intuition.
- Identifies problems before implementation.
- Costs very little time relative to implementation time.
Apply at multiple levels
- When designing module interfaces.
- When designing internal implementation.
- When choosing UI features.
- When decomposing the system into major modules.
11. COMMENTS AND DOCUMENTATION
Why write comments
Comments capture information that was in the designer’s mind but cannot be represented in code:
- High-level abstractions.
- Rationale behind design decisions.
- Usage constraints.
- Informal interface behavior.
Without comments, the only abstractions are code declarations — insufficient.
What to comment (in order of importance)
- Class interface comments — description of the abstraction provided.
- Method interface comments — behavior, arguments, return value, side effects, exceptions, preconditions.
- Instance variable comments — all non-obvious variables.
- Implementation comments — only when the code is not obvious; describe what and why, not how.
- Cross-module comments — for decisions that affect multiple modules; use a
designNotesfile.
Rules for good comments
✅ DO:
- Use different words from those in the entity’s name.
- For variables: document what it represents, not how it is manipulated. Think nouns.
- For methods: document all edge cases in the interface comment.
- Write comments first, before the implementation (as a design tool).
- Keep comments close to the code they describe.
- Prefer higher-level comments — they are easier to maintain.
❌ DON’T:
- Red Flag — Comment Repeats Code: if someone can write the comment just by reading the code, the comment adds no value.
- Red Flag — Implementation Docs Contaminate Interface: interface documentation must not describe implementation details.
- Don’t duplicate documentation — reference the canonical source.
- Don’t put important information only in the commit log.
Comments as a design tool
If a method or variable requires a long comment, it is a signal that you don’t have a good abstraction. If you cannot describe a method simply and completely, there is a design problem.
12. NAMING
Principle
A good name creates a precise mental image of what the entity is. It is a form of abstraction.
Rules
- Precision: Names must be specific enough that a reader understands what they are without reading documentation.
- Consistency: Use the same name for the same concept throughout the system. Never use the same name for two different concepts.
- Avoid generic names like
result,data,count,blockwithout additional context. - If it is hard to find a precise name → red flag that the entity does not have a clear design.
Naming red flags
- Vague Name: the name is broad enough to refer to many different things.
- Hard to Pick Name: if it is difficult to name something precisely, the underlying design may have problems.
13. OBVIOUS CODE
Principle
Obvious code can be read quickly with little effort, and the reader’s first guesses about its behavior are correct.
Techniques for making code more obvious
- Good names (see Section 12).
- Consistency: similar patterns → similar code.
- Strategic whitespace to separate logical blocks.
- Comments to compensate when the code cannot be made obvious.
Things that make code less obvious
- Event-driven programming — hard to follow control flow; document when each handler is invoked.
- Generic containers (e.g.,
Pair<Integer, Boolean>) — generic names obscure meaning. - Code that violates reader expectations — if the behavior is surprising, document it.
Red Flag: Nonobvious Code
If the behavior or meaning of code cannot be understood with a quick reading.
14. MODIFYING EXISTING CODE
Core rule
When modifying existing code, maintain the strategic mindset. Ask yourself: “Is this the best possible design given this change?”
- If you find design imperfections while modifying, fix them.
- Every modification should leave the system at least as clean as it was before.
- Continuously refactoring in small increments is more effective than occasional large refactors.
Maintaining comments
- Keep comments close to the code they describe.
- Before committing, scan the diffs to ensure documentation is still valid.
- Avoid duplicating documentation — reference the canonical source.
15. CONSISTENCY
Principle
A consistent system does similar things in similar ways and dissimilar things in different ways. It reduces cognitive load.
How to ensure consistency
- Document conventions (style guides, invariants).
- Automate enforcement with tools and linters.
- In code reviews, actively enforce consistency.
- “When in Rome”: when working in existing code, follow its conventions.
- Do not change established conventions without a strong reason and updating all affected code.
16. DESIGNING FOR PERFORMANCE
Principle
Clarity and performance are compatible. Simple code tends to be fast because it does no extraneous or redundant work.
Recommended approach
- Don’t optimize prematurely — know which operations are expensive (disk I/O, network, malloc, cache misses) and choose naturally efficient alternatives.
- Measure before modifying — never optimize by intuition.
- Design around the critical path — identify the minimum code needed for the common case and make it as simple as possible.
- Minimize special cases in the critical path.
17. APPLYING APoSD PRINCIPLES TO SKILL DESIGN
This section maps the book’s principles directly to decisions you face when writing Claude Code skills (SKILL.md files). Use it as a checklist when designing or reviewing a skill.
Skill description = module interface
The description field in the YAML frontmatter is the skill’s interface — the only thing visible
before Claude decides whether to load it.
- Deep module rule: Pack maximum signal into the description. It should tell Claude when to use the skill and what it enables, not just what it contains.
- Simple interface rule: The description must be scannable in seconds. One or two focused sentences beat a paragraph of caveats.
- Overexposure red flag: Don’t list every edge case in the description. Surface the 80% use case; handle the rest in the body.
SKILL.md body = implementation
The body is the implementation — hidden from the triggering decision, visible once loaded.
- Information hiding: Put “how to do it” in the body, not the description. The description answers “when and why”; the body answers “how”.
- Pull complexity downward: If the skill requires knowing something complex (a file format, a tool’s quirks, an API’s behavior), encode it in the body so Claude doesn’t have to figure it out each time.
- Comment what’s not obvious: Use callout blocks or inline notes to explain why a step is done a certain way, not just what to do.
File structure = module decomposition
Deciding what goes in SKILL.md vs. reference files mirrors the “better together or better apart” question.
| Keep in SKILL.md | Split into reference files |
|---|---|
| Steps always executed together | Domain-specific variants (AWS vs. GCP) |
| Shared context between steps | Large lookup tables or spec documents |
| Short enough to read in one pass | Content only needed for a subset of uses |
- Temporal decomposition red flag: Don’t split files by when they’re used (“setup.md”, “teardown.md”). Split by what knowledge they encapsulate (“aws.md”, “gcp.md”).
- Shallow module red flag: A reference file with two paragraphs isn’t worth a separate file. Merge it into the body unless it is genuinely a different abstraction.
Instructions = API design
Each numbered step or rule in your skill is part of its API to Claude.
- General-purpose rule: Write instructions in terms of outcomes and principles, not hyper-specific tool invocations that will break on version changes.
- Define errors out of existence: Anticipate common failure modes and fold the recovery into the normal instruction flow. Don’t leave Claude to figure out what to do when a tool returns an unexpected result.
- Design it twice: Before finalizing a skill, consider an alternative structure. Would the instructions be clearer if the skill were split? Merged? Reordered?
Skill description quality checklist
Apply this filter to the description field before shipping a skill:
- Does it name the concrete trigger contexts (not just “when relevant”)?
- Does it omit implementation details that belong in the body?
- Is it under ~60 words?
- Does it clarify when not to use it, if that ambiguity exists?
- Would you understand when to load this skill having never seen the body?
QUICK REFERENCE: PRINCIPLES & RED FLAGS
Official design principles (from the book)
- Complexity is incremental — sweat the small stuff.
- Working code isn’t enough.
- Make continual small investments to improve system design.
- Modules should be deep.
- Interfaces should make the most common usage as simple as possible.
- It is more important for a module to have a simple interface than a simple implementation.
- General-purpose modules are deeper.
- Separate general-purpose and special-purpose code.
- Different layers should have different abstractions.
- Pull complexity downward.
- Define errors (and special cases) out of existence.
- Design it twice.
- Comments should describe things not obvious from the code.
- Software should be designed for ease of reading, not ease of writing.
- The increments of software development should be abstractions, not features.
Red flags at a glance
| Red Flag | Signal |
|---|---|
| Shallow Module | Interface almost as complex as the implementation |
| Information Leakage | A design decision reflected in multiple modules |
| Temporal Decomposition | Structure based on execution order, not knowledge encapsulation |
| Overexposure | API forces users to know rarely-used features to use common ones |
| Pass-Through Method | Method only passes arguments to another with the same signature |
| Repetition | The same code appears over and over |
| Special-General Mixture | Special-purpose code mixed with general-purpose in the same abstraction |
| Conjoined Methods | Can’t understand one without understanding the other |
| Comment Repeats Code | Comment only restates what the code already says |
| Implementation Docs Contaminate Interface | Interface comment describes implementation details |
| Vague Name | Name is too broad to convey meaning |
| Hard to Pick Name | Difficulty naming something precisely hints at a design problem |
| Hard to Describe | Complete documentation requires a very long comment |
| Nonobvious Code | Behavior or meaning cannot be understood with a quick reading |