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

  1. Change amplification — A seemingly simple change requires modifications in many places.
  2. Cognitive load — The developer must accumulate a lot of information to complete a task.
  3. Unknown unknowns — It is not clear which code must be modified or what information is needed.

The 2 root causes

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 ❌

Strategic programming ✅

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 ✅

Shallow module ❌ (Red Flag)

Classitis ❌

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

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

Questions to evaluate if your interface is too specialized

  1. What is the simplest interface that covers all my current needs?
  2. In how many situations will this method be used? (If only one → red flag of over-specialization.)
  3. 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:


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

When to separate code

On method length

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

  1. Define errors out of existence — Change the semantics so there is no error.
  2. Exception masking — Handle the exception at a low level; higher levels never know it occurred (e.g., TCP resends lost packets transparently).
  3. Exception aggregation — Handle many exceptions in one place instead of scattered handlers.
  4. 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

Apply at multiple levels


11. COMMENTS AND DOCUMENTATION

Why write comments

Comments capture information that was in the designer’s mind but cannot be represented in code:

Without comments, the only abstractions are code declarations — insufficient.

What to comment (in order of importance)

  1. Class interface comments — description of the abstraction provided.
  2. Method interface comments — behavior, arguments, return value, side effects, exceptions, preconditions.
  3. Instance variable comments — all non-obvious variables.
  4. Implementation comments — only when the code is not obvious; describe what and why, not how.
  5. Cross-module comments — for decisions that affect multiple modules; use a designNotes file.

Rules for good comments

✅ DO:

❌ DON’T:

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

Naming red flags


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

Things that make code less obvious

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?”

Maintaining comments


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


16. DESIGNING FOR PERFORMANCE

Principle

Clarity and performance are compatible. Simple code tends to be fast because it does no extraneous or redundant work.

  1. Don’t optimize prematurely — know which operations are expensive (disk I/O, network, malloc, cache misses) and choose naturally efficient alternatives.
  2. Measure before modifying — never optimize by intuition.
  3. Design around the critical path — identify the minimum code needed for the common case and make it as simple as possible.
  4. 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.

SKILL.md body = implementation

The body is the implementation — hidden from the triggering decision, visible once loaded.

File structure = module decomposition

Deciding what goes in SKILL.md vs. reference files mirrors the “better together or better apart” question.

Keep in SKILL.mdSplit into reference files
Steps always executed togetherDomain-specific variants (AWS vs. GCP)
Shared context between stepsLarge lookup tables or spec documents
Short enough to read in one passContent only needed for a subset of uses

Instructions = API design

Each numbered step or rule in your skill is part of its API to Claude.

Skill description quality checklist

Apply this filter to the description field before shipping a skill:


QUICK REFERENCE: PRINCIPLES & RED FLAGS

Official design principles (from the book)

  1. Complexity is incremental — sweat the small stuff.
  2. Working code isn’t enough.
  3. Make continual small investments to improve system design.
  4. Modules should be deep.
  5. Interfaces should make the most common usage as simple as possible.
  6. It is more important for a module to have a simple interface than a simple implementation.
  7. General-purpose modules are deeper.
  8. Separate general-purpose and special-purpose code.
  9. Different layers should have different abstractions.
  10. Pull complexity downward.
  11. Define errors (and special cases) out of existence.
  12. Design it twice.
  13. Comments should describe things not obvious from the code.
  14. Software should be designed for ease of reading, not ease of writing.
  15. The increments of software development should be abstractions, not features.

Red flags at a glance

Red FlagSignal
Shallow ModuleInterface almost as complex as the implementation
Information LeakageA design decision reflected in multiple modules
Temporal DecompositionStructure based on execution order, not knowledge encapsulation
OverexposureAPI forces users to know rarely-used features to use common ones
Pass-Through MethodMethod only passes arguments to another with the same signature
RepetitionThe same code appears over and over
Special-General MixtureSpecial-purpose code mixed with general-purpose in the same abstraction
Conjoined MethodsCan’t understand one without understanding the other
Comment Repeats CodeComment only restates what the code already says
Implementation Docs Contaminate InterfaceInterface comment describes implementation details
Vague NameName is too broad to convey meaning
Hard to Pick NameDifficulty naming something precisely hints at a design problem
Hard to DescribeComplete documentation requires a very long comment
Nonobvious CodeBehavior or meaning cannot be understood with a quick reading