← All skills
software-design-philosophy
Use when managing software complexity: module design, deep vs. shallow modules, information hiding, over-engineering, or strategic vs. tactical programming tradeoffs.
About this skill
Use when managing software complexity: module design, deep vs. shallow modules, information hiding, over-engineering, or strategic vs. tactical programming tradeoffs.
SKILL.md
software-design-philosophy.SKILL.mdPlace it in your Syntic skills folder. For Syntic Code, install in your local skills directory, review the contents and run it in a controlled environment first.
---
name: software-design-philosophy
description: Use when managing software complexity: module design, deep vs. shallow modules, information hiding, over-engineering, or strategic vs. tactical programming tradeoffs.
category: Design
version: 1.0.0
tools: []
---
# A Philosophy of Software Design Framework
A practical framework for managing the fundamental challenge of software engineering: complexity. Apply these principles when designing modules, reviewing APIs, refactoring code, or advising on architecture decisions.
## Core Principle
**The greatest limitation in writing software is our ability to understand the systems we are creating.** Complexity is the enemy: it makes systems hard to understand, hard to modify, and a source of bugs. Evaluate every design decision by asking "Does this increase or decrease the overall complexity of the system?" — the goal is not zero complexity, but minimizing unnecessary complexity and concentrating the necessary kind where it can be managed.
## Scoring
**Goal: 10/10.** Score a design by counting how many of the eight Quick Diagnostic rows it satisfies (≈1.25 points each). Bands: **9-10** = all eight pass (deep modules, no leakage, interface comments capture design intent, design improvement routine); **6-8** = 5-6 pass (mostly deep, one or two leaks or undocumented abstractions); **3-5** = 2-4 pass (classitis, temporal decomposition, comments that only restate code); **≤2** = 0-1 pass (tactical-tornado: shallow modules, pervasive leakage, no design intent recorded). State the score, the failed rows, and the fix for each.
## The Software Design Framework
### 1. Complexity and Its Causes
Complexity is anything about a system's structure that makes it hard to understand and modify. Three symptoms — **change amplification** (a simple change requires edits in many places), **cognitive load** (a developer must hold too much in mind), and **unknown unknowns** (unclear what must change or what's relevant — the worst symptom) — stem from two causes: dependencies and obscurity. Complexity is incremental, accumulating from hundreds of small decisions ("death by a thousand cuts"), so every decision matters.
**Code applications:** centralize shared knowledge (extract color constants instead of hardcoding `#ff0000` in 20 files); hide detail to cut cognitive load (`open(path)` instead of requiring buffer size, encoding, lock mode); make dependencies explicit via types/interfaces; name precisely (`numBytesReceived` not `n`) to fight obscurity.
### 2. Deep vs Shallow Modules
The best modules are **deep**: powerful functionality behind a simple interface. **Shallow modules** have complex interfaces relative to their functionality — they add complexity rather than hiding it. The interface is the *cost* a module imposes; the implementation is the benefit, so a method harder to learn than to re-implement is net-negative. Depth = functionality provided / interface complexity imposed (Unix file I/O is deep; thin Java I/O wrappers are shallow). **Classitis** is the disease of too many small, shallow classes, each adding cognitive load without adding depth. Small methods aren't inherently good — depth matters more than size.
**Code applications:** hide complexity behind a simple API (`file.read(path)` hides disk blocks, caching, buffering, encoding); merge related shallow classes (`RequestParser` + `RequestValidator` + `RequestProcessor` → one `RequestHandler`); favor fewer parameters and sensible defaults over 15-parameter constructors.
### 3. Information Hiding and Leakage
Each module should encapsulate knowledge not needed by other modules. **Information leakage** — one design decision reflected in multiple modules — is a key red flag: a decision in one module can change there alone, while the same decision leaked into N modules turns one edit into N edits no compiler will remind you to make. **Temporal decomposition** causes leakage: splitting code by *when* things happen forces shared knowledge across phases — organize by knowledge instead. Back-door leakage through data formats, protocols, or shared assumptions is the subtlest form; decorators frequently leak by exposing the decorated interface. If two modules share knowledge, merge them or create a module that encapsulates it.
**Code applications:** centralize serialization (one module owns JSON encoding, not `json.dumps` everywhere); combine "read config" and "apply config" into one module instead of splitting by phase; abstract transport details (`MessageBus.send(event)` hides HTTP vs. gRPC vs. queue).
### 4. General-Purpose vs Special-Purpose Modules
Design modules that are "somewhat general-purpose": an interface general enough for multiple uses, with an implementation that handles current needs. Ask: "What is the simplest interface that covers all my current needs?" Counterintuitively, the general interface is usually the *simpler* one — special-case methods multiply as requirements grow, while one general method absorbs them. The trap runs the other way too: generality current needs don't demand is speculative complexity paid for a use case that may never arrive. Push complexity downward: lower-level modules handle hard cases so upper levels stay simple. Configuration parameters often represent a failure to decide — each one pushes complexity onto the caller.
**Code applications:** design for the concept, not one use case (`text.insert(position, string)` instead of `text.addBulletPoint()`); auto-detect behavior instead of adding parameters (file encoding, not an `encoding` param); one general method over many specific ones (`store(key, value, options)` instead of `storeUser()`, `storeProduct()`, `storeOrder()`).
### 5. Comments as Design Documentation
Comments should describe what is not obvious from the code: design intent, abstraction rationale, invariants, and assumptions. "Good code is self-documenting" is a myth beyond low-level detail — code records *what* it does, never why this approach over the alternatives or what it silently assumes. That rationale lives only in the author's head and is gone once they move on. Four comment types: interface comments (most important — they define the abstraction), data structure member comments, implementation comments, cross-module comments. Write comments first (comment-driven design) to clarify thinking before code; if a comment is hard to write, the design may be too complex.
**Code applications:** interface comment describes the abstraction, not the implementation ("Returns the widget closest to position, or null if none within threshold"); data structure comment explains invariants ("List sorted by priority descending; ties broken by insertion order"); implementation comment explains why, not what ("// Binary search: list always sorted, can hold 100k+ items").
### 6. Strategic vs Tactical Programming
**Tactical programming** gets features working quickly and accumulates complexity with each shortcut. **Strategic programming** invests 10-20% extra effort in good design, treating every change as an opportunity to improve structure — tactical speed is borrowed, while strategic investment compounds into systems that are faster to work with within months. The "tactical tornado" is a developer who ships fast but leaves wreckage — celebrated short-term, destructive long-term. Startups need strategic programming most, since early shortcuts compound into crippling debt as the team grows. Your primary job is a great design that happens to work, not working code that happens to have a design.
**Code applications:** resist quick-and-dirty fixes (no boolean parameter for "just this one special case"); refactor an awkward interface while adding a feature, not as a separate event; evaluate reviews on structure ("does this make the system simpler?"), not just correctness.
## Common Mistakes
| Mistake | Why It Fails | Fix |
|---------|-------------|-----|
| **Creating too many small classes** | Classitis adds interfaces without depth | Merge related shallow classes into deeper modules |
| **Splitting modules by temporal order** | "Read, then process, then write" forces shared knowledge across modules | Group code that shares knowledge into one module |
| **Exposing implementation in interfaces** | Callers depend on internals; changes propagate | Design interfaces around abstractions; hide formats and protocols |
| **Treating comments as optional** | Design intent and assumptions are lost | Write interface comments first; maintain with the code |
| **Configuration parameters for everything** | A parameter offloaded to the caller is a decision declined | Determine behavior automatically; provide sensible defaults |
| **Quick-and-dirty tactical fixes** | Shortcuts compound until the system is unworkable | Invest 10-20% extra; treat every change as a design opportunity |
| **Pass-through methods** | Forwards arguments, adds an interface but no functionality | Merge the pass-through into the caller or the callee |
## Quick Diagnostic
| Question | If No | Action |
|----------|-------|--------|
| Can you describe each module in one sentence? | Modules do too much or lack purpose | Split into coherent, describable responsibilities |
| Are interfaces simpler than implementations? | Modules are shallow — complexity leaks outward | Hide more; merge shallow classes into deeper ones |
| Can you change an implementation without affecting callers? | Information is leaking across boundaries | Encapsulate the leaked decision in one module |
| Is the interface general enough for current needs, no more? | Either special-cased or speculatively generic | Ask "simplest interface for all current needs?" |
| Do interface comments capture design intent? | Newcomers must reverse-engineer the abstraction | Write interface comments before or during implementation |
| Are configuration parameters minimized? | Callers carry decisions the module should make | Auto-detect behavior; supply sensible defaults |
| Is the team investing 10-20% in design, not just shipping features? | Tactical debt compounds silently | Budget refactoring time into every change |
| Are pass-through methods and pure delegation absent? | Extra interface layers with no added depth | Merge pass-throughs into caller or callee |
Based on John Ousterhout's *A Philosophy of Software Design*.
Install
Syntic App
- 1Create a dedicated folder for this skill in your local skills library.
- 2Place SKILL.md into that folder.
- 3Restart Syntic and invoke this skill on matching tasks.
Syntic Code (CLI)
- 1Save SKILL.md in your local Syntic Code skills directory.
- 2Keep related files in the same skill folder.
- 3Run in a safe environment and validate outputs.