software-design
Skill2 starsChanged 4 months agoMITView on GitHub
You MUST consult this skill when writing, reviewing, or designing code. Also trigger when code seems verbose or over-documented (unnecessary wrappers, comments restating the code), when design behavior is ambiguous (what should happen on empty input, which error to propagate), or when conventions are silently violated without explanation. NOT for debugging existing errors, language syntax questions, framework setup (CI/CD, database migrations), performance benchmarking, test quality criteria (see test-design), API surface conventions (see api-design), or abstract pattern theory discussions.
9 other projects use a version of this.
Adopted by
Most recently taken up by
- nohemi20231/design-skillstheir own version, first seen Nov 8, 2026
- usetemi/skillstheir own version, first seen Nov 8, 2026
- kasperjunge/skillstheir own version, first seen Nov 8, 2026
- ArthurZakirov/SystemSmiththeir own version, first seen Nov 1, 2026
- gustavovalverde/skillstheir own version, first seen Oct 6, 2026
- stupidZZ/skillstheir own version, first seen Jul 9, 2026
Other versions9 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 9
- stupidZZ/skills★ 9same name
- gustavovalverde/skills★ 8same name
- kasperjunge/skills★ 7same name
- usetemi/skills★ 6same name
- yinhunfeixue/software_development_skills★ 4same name
- ArthurZakirov/SystemSmith★ 0same name
- nohemi20231/design-skills★ 0same name
- patforna/core-skills★ 0same name
- suraciii/agent-skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository67 other files it gives its agents.
- AGENTS.mdAGENTS.md
- agent-architectureSkill
- agent-evaluationSkill
- agent-memorySkill
- agent-post-trainingSkill
- agent-self-evolutionSkill
- agent-tool-designSkill
- api-designSkill
- application-architectureSkill
- brainstormSkill
- code-reviewerSkill
- coding-agent-designSkill
- concurrency-designSkill
- context-engineeringSkill
- dockerSkill
- game-audioSkill
- game-designSkill
- game-narrativeSkill
- game-patternsSkill
- game-performanceSkill
- game-renderingSkill
- game-visualsSkill
- git-commitSkill
- git-prSkill
- godot-shaderSkill
- godotSkill
- gpu-rendering-architectureSkill
- grafanaSkill
- helmSkill
- homelab-monitoringSkill
- k3sSkill
- k8s-networkingSkill
- k8s-operationsSkill
- k8s-storageSkill
- k8s-workloadsSkill
- knowledge-baseSkill
- level-designSkill
- llm-inference-servingSkill
- loggingSkill
- logqlSkill
- love2d-fennelSkill
- love2dSkill
- ml-platform-architectureSkill
- ml-post-trainingSkill
- multi-agent-collaborationSkill
- nix-dendriticSkill
- nix-packagingSkill
- nixSkill
- odin-designSkill
- odin-gamedevSkill
- orchestratorSkill
- physical-ai-systemsSkill
- plannerSkill
- prd-to-storiesSkill
- prd-writingSkill
- promqlSkill
- pr-reviewerSkill
- python-designSkill
- rag-designSkill
- ray-data-pipelinesSkill
- The first 60 shown.
The file
Show its rules and text
We split this skill into the rules it gives an agent
13 rules. 0 are common, 12 are distinctive, and 1 is very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
12
Distinctive
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 24
Every piece of code exists to serve someone -- a caller, a user, a future maintainer.
- Distinctive: line 30
Prefer the smallest interface that serves the caller -- you can always add later, but removing is painful.
- Distinctive: line 39
Prefer the simplest mechanism: When a fix starts to need new options, override layers or patches to upstream code, look for a simpler …
- Distinctive: line 66
Let cut points emerge: Don't factor too early. Wait for natural factoring boundaries to reveal themselves through experience.
- Distinctive: line 109
Immutability: default to immutable data.
- Distinctive: line 119
Framework composition over ad-hoc: when you've chosen a framework, use its native composition mechanisms (dependency injection, middleware, …
- Distinctive: line 163
Check the primary source: before you state how code, config or a tool behaves, read its source or docs, or run it with unfiltered output.
- Distinctive: line 222
Ask at decision points: Ask when the answer cannot be inferred from the codebase's conventions, the language's standard behavior, or the …
- Distinctive: line 231
Conciseness: Every line of code and documentation should carry information.
- Distinctive: line 232
No docstrings that restate what the signature already says.
- Very specific: line 293
Result types force capacity handling at the right moment: When send() returns .mailbox_full or .pool_exhausted, the caller must handle it …
- Distinctive: line 315
Drop explicitly, not implicitly: When hardware limits are exceeded, the only engineering choice is *where* and *how* you drop work.
- Distinctive: line 331
When reviewing code or making design decisions:
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Software Design Principles
Favor small, focused, composable pieces that each do one thing well. This applies at every level -- functions, modules, APIs, systems.
See references/patterns.md for the full before/after catalog when you need a
concrete refactoring model for a specific pattern (bloated config, deep
inheritance, hidden side effects, etc.).
Start Minimal
Every piece of code exists to serve someone -- a caller, a user, a future maintainer. Write code that earns its place. If a function, class, or module doesn't serve a clear need, it shouldn't exist yet. Resist speculative abstractions.
Design from the caller's perspective: what does the consumer actually need? Prefer the smallest interface that serves the caller -- you can always add later, but removing is painful. When an interface feels bloated, ask: "Which callers use which parts?" If different callers use different subsets, that's a signal to split.
Scope to the request. Change what was asked. Unrequested edits, refactors, cross-references, merges or history rewrites go in your summary as suggestions, not into the diff.
Prefer the simplest mechanism. When a fix starts to need new options, override layers or patches to upstream code, look for a simpler approach that meets the goal (separate copies, another install path, the platform default), and name what the heavier option would add.
# Before: speculative abstraction nobody asked for
class DataPipelineOrchestrator:
def __init__(self, source_adapter, sink_adapter, transformer_chain,
retry_policy, metrics_collector, feature_flags): ...
def execute_with_hooks(self, pre_hooks=None, post_hooks=None): ...
def execute_async(self): ...
def execute_dry_run(self): ...
# After: solve the actual problem first
def process_records(records):
"""Filter and transform records. Extend when the need is real."""
return [transform(r) for r in records if is_valid(r)]Chesterton's Fence: Existing code already earned its place once -- before removing it, understand why it exists. What invariant does it protect? What edge case does it handle? What broke before it was added? Deleting code you don't understand is how you reintroduce bugs that were already fixed. Code that is demonstrably unreachable or has no callers can be removed without this investigation -- the evidence of disuse is itself the answer.
Let cut points emerge: Don't factor too early. Wait for natural factoring boundaries to reveal themselves through experience. The right abstraction announces itself: a narrow interface that hides genuine complexity, a pattern that repeats in three genuinely similar places, a boundary where two things change at different rates. Premature extraction creates abstractions that fit the first two cases but fight the third.
Layered interfaces: Design for the 80% use case first; make the 20% possible without making the 80% harder. A simple API for simple cases, with the full API available when needed. Don't force every caller to supply parameters they don't care about just because one caller does. "Highly configurable" is often the designer declining to make a decision — shipping the burden to every caller instead of doing the thinking once. Good defaults are respect for the caller's time; escape hatches are for the genuine minority who need something unusual.
# Before: every caller must handle the full complexity
def send_report(data, format, recipients, cc=None, subject=None,
template=None, retry_count=3, timeout=30): ...
# After: compose small pieces — simple callers use the composed version,
# power users use the building blocks directly
def build_report(data, format="pdf", template=None): ...
def deliver(report, recipients, cc=None, subject=None,
retry_count=3, timeout=30): ...
def send_report(data, recipients, format="pdf"):
"""Simple API: covers 80% of callers."""
report = build_report(data, format)
deliver(report, recipients)Compose Small Pieces
Build systems by composing small, focused pieces rather than extending large ones.
- Pure functions: take inputs, return outputs, do nothing else. Inherently testable and composable. When a function needs IO or state mutation, push that to the boundary -- keep core logic pure.
- Immutability: default to immutable data. Mutable state is the primary source of hard-to-reproduce bugs. Prefer returning new values over modifying existing ones.
- Composition over inheritance: favor delegation over class hierarchies.
Inheritance couples tightly and resists change. Pipelines
(
validate -> transform -> persist) are often clearer than inheritance chains. - Declarative over imperative: express what should happen, not how. Comprehensions and map/filter communicate intent. But when they become hard to read, a named loop is clearer -- don't force it.
- Framework composition over ad-hoc: when you've chosen a framework, use its native composition mechanisms (dependency injection, middleware, lifecycle hooks, fixtures) rather than closures, globals, or manual wiring. Ad-hoc patterns bypass the testability, discoverability, and lifecycle management the framework provides -- and surprise readers who know the framework.
# Before: logic tangled with side effects — hard to test, hard to reuse
def calculate_discount(user_id):
user = db.get_user(user_id) # side effect: DB read
discount = user.loyalty_years * 0.05
logger.info(f"Discount: {discount}") # side effect: logging
analytics.track("discount_calc") # side effect: network
return discount
# After: pure core, side effects at the boundary
def calculate_discount(loyalty_years):
return min(loyalty_years * 0.05, 0.30)
# Caller handles IO and side effects:
user = db.get_user(user_id)
discount = calculate_discount(user.loyalty_years)
logger.info(f"Discount: {discount}")
analytics.track("discount_calc")Be Honest and Thorough
Code should communicate its purpose and not promise more than it delivers.
- Self-explanatory: choose names that reveal intent. Comment the "why," not the "what." Use types as documentation. Structure code so readers discover things in the order they need them.
- Honest: APIs should do what their names suggest -- no hidden side effects,
no silent failures. A function called
get_usershould get a user, not also update a cache. Make failure modes visible. - Thorough: handle edge cases explicitly -- empty lists, missing keys, timeouts are expected, not exceptional. Validate at boundaries. Clean up resources.
- Longevity: choose boring technology. Depend on abstractions, not implementations. Wrap external dependencies so you can swap them. Write for the reader who comes after you.
- Check the primary source: before you state how code, config or a tool behaves, read its source or docs, or run it with unfiltered output. If you can't reach the source, say what you couldn't check instead of inferring it.
Parse, Don't Validate
Validation checks a condition and discards the result. Parsing checks a condition and returns a refined type that carries the proof forward. The difference is information preservation: a validator answers "is this valid?" and throws the knowledge away; a parser answers "is this valid?" and hands back a value that cannot be invalid.
Alexis King argues: prefer functions that return a refined type over
functions that return None or bool. Downstream code that receives a
refined type needs no re-checks; downstream code that receives a raw type
must re-validate at every use site.
Before (validates, discards proof — downstream must re-check):
validate_non_empty(items: list) -> None # raises on empty; caller still holds listAfter (parses, returns refined type — downstream has a guarantee):
parse_non_empty(items: list) -> NonEmpty # raises on empty; caller holds NonEmptyMake illegal states unrepresentable: pick data structures that rule out bad
states by construction. Parse at the boundary; once parsed, downstream code
never re-checks. In Python, enforcement is runtime via smart constructors —
see python-design for mechanics. See references/type-driven-design.md
for depth and references/engineering-judgment.md "Validate Fully at Load"
for the related frontier-of-uncertainty principle.
Locality of Behavior: The behavior of a code unit should be obvious by looking only at that unit. When you have to chase through five files to understand what a single function does, the behavior has been spread too thin. This complements honesty (don't hide what you do) but addresses structural proximity: don't spread a single behavior across distant files when it can live together. Co-locate the things that change together.
Expression complexity: Break complex conditionals into named intermediate variables. This makes the logic readable and makes the intermediate values inspectable in a debugger.
# Before: dense conditional — hard to read, hard to debug
if user.subscription_tier in ("pro", "enterprise") and not user.payment_overdue \
and (user.trial_days_remaining > 0 or user.has_paid_invoice):
grant_access()
# After: named intermediates — each step is readable and debuggable
is_paid_tier = user.subscription_tier in ("pro", "enterprise")
is_account_current = not user.payment_overdue
has_valid_access = user.trial_days_remaining > 0 or user.has_paid_invoice
if is_paid_tier and is_account_current and has_valid_access:
grant_access()Ask at decision points: Ask when the answer cannot be inferred from the codebase's conventions, the language's standard behavior, or the caller's stated intent. When a standard defensive default applies, apply it and note the assumption in a comment. Reserve asking for cases where reasonable engineers would disagree — the cost of asking is one question; the cost of guessing wrong is rework after the caller discovers the assumption. This applies to code review too: if a design choice isn't obvious from context, the author should document the reasoning, not leave reviewers to guess.
Conciseness: Every line of code and documentation should carry information. No docstrings that restate what the signature already says. No comments that narrate the code. No single-use abstractions (type aliases, helper functions, wrapper classes) that exist only to "be clean" but add indirection without simplifying. Remove documentation that adds no information beyond what the signature and code already express; keep documentation that explains the "why," not the "what" — and document the alternatives you rejected.
# Before: comment restates the code
x = x + 1 # increment x by 1
# Before: wrapper that adds nothing
def get_items():
return fetch_items()
# After: the code speaks for itself — comment only when the "why" isn't obvious
x = x + 1 # offset for 1-based indexing in the legacy APIComment when breaking a convention: Silence means the project's conventions apply. An inline comment means you thought about it and chose differently for a specific reason. This makes rule violations visible and reviewable — the alternative is silent deviation that erodes conventions over time.
try:
plugin.load(name)
# Catching broad Exception because the plugin loader must never crash the host process.
except Exception:
logger.exception("plugin failed to load")# Before: name hides side effects; failure is silent
def get_user(user_id):
user = db.query(user_id)
cache.set(user_id, user) # hidden side effect
analytics.track("user_fetched") # hidden side effect
if not user:
return None # silent failure
return user
# After: name matches behavior; failure is explicit
def get_user(user_id) -> User:
user = db.query(user_id)
if not user:
raise UserNotFoundError(user_id)
return user
# Caller owns the side effects:
user = get_user(user_id)
cache.set(user_id, user)Bounded Resources as API Honesty
An unbounded queue is a lie. It promises infinite capacity and delivers catastrophic failure — silently, minutes later, far from the code that caused it. A bounded resource with an explicit rejection is honest: "I am full. Decide what to do."
Result types force capacity handling at the right moment. When send()
returns .mailbox_full or .pool_exhausted, the caller must handle it at the
instant the system reaches capacity — not via a downstream timeout two minutes
later. This is the same principle as or_return for errors: make the failure
path visible and force a decision.
# Unbounded: caller never knows the queue is 50,000 items deep
queue.push(task)
# Bounded: caller handles capacity at the call site
match worker_pool.submit(task):
case .ok: proceed()
case .pool_full: shed_load(task) # explicit, immediate, recoverablePredictability beats brevity. Forcing callers to handle a capacity result on every operation is more verbose than dumping into an unbounded channel. Accept this trade-off. The system degrades gracefully, sheds excess load instantly, and recovers the moment pressure normalizes — instead of spending the next 20 minutes processing dead requests that were queued during the spike.
Drop explicitly, not implicitly. When hardware limits are exceeded, the only engineering choice is where and how you drop work. Implicit dropping (OOM crash, process kill) loses everything. Explicit dropping (immediate rejection, load shedding at the entry point) preserves everything else and gives the caller a chance to respond — log it, retry later, return a 503, or route to a fallback.
For the structural mechanisms that implement this principle — bounded mailboxes, ring buffers, back-pressure propagation, supervision trees — see
concurrency-design. This section covers the design principle; that skill covers the architecture.
Decision Checklist
When reviewing code or making design decisions:
- Does this earn its place? (Purpose and Usefulness, Minimalism)
- Is each piece focused on one thing? (Focused Interfaces)
- Can I understand this without running it in my head? (Pure Functions, Self-Explanatory Design)
- Does it do what it says? (Honesty, Locality of Behavior)
- What happens when things go wrong? (Thoroughness)
- Will this still make sense in a year? (Longevity Over Trend)
- Before I delete this -- do I know why it exists? (Chesterton's Fence)
- Am I factoring too early, or has the pattern proven itself? (Let cut points emerge)
- Is the common case simple, and the complex case possible? (Layered interfaces)
- Am I guessing at behavior, or did I confirm it? (Ask at decision points)
- Does every line carry information? (Conciseness)
- If I'm breaking a convention, did I say why? (Comment when breaking a convention)
- Do I actually understand what this does at runtime, or am I papering over it? (Engineering Judgment)
- Are my queues and pools bounded? Does the caller see rejection explicitly? (Bounded Resources as API Honesty)
- Does this function return a refined type, or does it validate and discard the proof? (Parse, Don't Validate)
- Can a caller understand correct usage from the signature alone, without reading the body? (Affordances — Design Lenses)
- What mental model does this interface present to a new caller? Is it accurate? (Conceptual Model — Design Lenses)
- Do names lead a new engineer to the right place on the first try? (Information Scent — Design Lenses)
- Can a competent engineer hold this interface in working memory in one sitting? (Cognitive Load — Design Lenses)
These are lenses, not laws. They sometimes conflict -- minimalism might suggest fewer types while thoroughness demands explicit error handling. Use judgment. The goal is software that is genuinely useful, easy to understand, and respectful of the humans who interact with it.
References
| Reference | When to read |
references/patterns.md | Full before/after catalog for specific refactoring patterns -- bloated config, deep inheritance, hidden side effects, immutability, declarative style, etc. Consult when you need a concrete model for a specific principle. |
references/engineering-judgment.md | Optimization judgment, understand-before-you-change, invariants, root-cause discipline, state calibration, activity-vs-output, and design-for-hard-cases. Consult when reasoning about runtime behavior, calibrating optimization effort, or applying principles with judgment. |
references/type-driven-design.md | Parse vs validate (information preservation), make illegal states unrepresentable, shotgun parsing antipattern, push the burden of proof upward. Consult when designing boundary types or eliminating redundant downstream checks. |
references/design-lenses.md | Affordances, signifiers, conceptual models, information scent, Gestalt principles, and structured design critique workflow. Consult when reviewing code for structural quality, when something feels off but you can't articulate why, or when naming and navigability are the concern. |
technical-writing skill | Docs, READMEs, specs and design docs: Diátaxis, BLUF, voice and revision rules. Load it when writing or reviewing documentation. |
references/module-patterns.md | Domain-aligned vs implementation-aligned structure, dependency direction as architectural constraint. Consult when organizing modules or reviewing project structure. |
references/cli-patterns.md | Progressive disclosure in commands, feedback contracts, constraint defaults for dangerous operations. Consult when designing or reviewing CLI tools. |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

