agentleFS
patfornapatforna/core-skills/skills/software-design/SKILL.md

software-design

Skill0 starsChanged 5 months agoMITView on GitHub

Software design principles (Ousterhout-style). Use when making module/interface design decisions, reviewing code for complexity, or deciding where to put functionality. TRIGGER when: designing APIs, reviewing module boundaries, deciding function signatures, or user asks about complexity management. DO NOT TRIGGER when: purely mechanical refactoring with no design decisions.

9 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

24 rules. 0 are common, 24 are distinctive, and 0 are very specific to this project.

  • 0

    Common

    Most of the 5 edited versions we read keep these.

  • 24

    Distinctive

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  • could be automatic: a setting could enforce it
  1. Distinctive:

    If the interface complexity approaches the implementation complexity, the module is shallow.

    line 28
  2. Distinctive:

    If both pieces have similar signatures and similar levels of abstraction, the split made things worse -- merge them back.

    line 33
  3. Distinctive:

    Do NOT merge if the pieces are independently useful and combining would create a multi-purpose module with no clear single abstraction.

    line 41
  4. Distinctive:

    Every module should encapsulate design decisions (data structures, algorithms, serialization formats, protocols) that no other module needs …

    line 45
  5. Distinctive:

    Test for information leakage: If two modules both depend on the same design decision (e.g., a file format, a protocol, an ordering), …

    line 47
  6. Distinctive:

    Test for temporal decomposition: If your module boundaries mirror the order of operations (read, then process, then write) rather than …

    line 49
  7. Distinctive:

    Test for overexposure: If the caller must understand implementation details to use the module correctly, the interface is leaking.

    line 51
  8. Distinctive:

    When designing a module, make the interface general-purpose and the usage special-purpose -- even if there is currently only one caller.

    line 55
  9. Distinctive:

    Decorator/wrapper that mirrors the underlying interface: If the decorator's methods mostly forward to identically-named methods on the …

    line 71
  10. Distinctive:

    When you have a choice about where to handle complexity, push it into the implementation rather than the interface.

    line 75
  11. Distinctive:

    When NOT to apply: If callers genuinely need the exception information to make decisions, the exception must be exposed even though it adds …

    line 97
  12. Distinctive:

    Before committing to any significant design decision, sketch at least two radically different approaches.

    line 101
  13. Distinctive:

    Write comments that describe things that are not obvious from the code.

    line 112
  14. Distinctive:

    If the interface comment must describe implementation details to be complete, the method is shallow -- redesign it.

    line 122
  15. Distinctive:

    Place before major blocks to describe what the block does and why, not how.

    line 131
  16. Distinctive:

    Place the authoritative documentation in the most central location.

    line 137
  17. Distinctive:

    Add short references ("See designNotes: Zombies") in other modules.

    line 138
  18. Distinctive:

    Never duplicate cross-module documentation -- it will become stale.

    line 139
  19. Distinctive:

    Consistency: Use the same name for the same concept everywhere.

    line 146
  20. Distinctive:

    Hard to name = design smell: If you can't find a simple, precise name, the underlying concept may not have a clean design.

    line 150
  21. Distinctive:

    Follow existing conventions in the codebase, even if you would do it differently.

    line 154
  22. Distinctive:

    Before introducing a new convention, verify that no existing convention covers the same situation.

    line 156
  23. Distinctive:

    Do not force dissimilar things into the same pattern.

    line 158
  24. Distinctive:

    Use this checklist during code review or when evaluating your own design decisions.

    line 162

“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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Software Design -- Complexity Management

You make design decisions by minimizing complexity. Complexity is anything that makes code hard to understand or modify. Apply these rules during implementation and code review.

Complexity Diagnostic

Complexity has three symptoms. When you detect any of these, the design needs work:

  1. Change amplification: A single logical change requires edits in many places. Look for the same knowledge encoded in multiple locations.
  2. Cognitive load: Understanding a piece of code requires accumulating a large amount of context. Count how many things a developer must hold in mind.
  3. Unknown unknowns: It is unclear what code must be modified or what information is relevant. This is the worst form -- it causes bugs that aren't caught until production.

Two root causes produce these symptoms: dependencies (code that cannot be understood or modified in isolation) and obscurity (important information is not obvious).

Deep Modules

Design every module to be deep: simple interface, powerful implementation. The interface is the cost a caller pays; the implementation is the value they receive.

Decision procedure for module depth:

  1. Count the elements in the module's interface: methods, parameters, exceptions, configuration, implicit behavioral contracts.
  2. Estimate the implementation complexity hidden behind that interface.
  3. If the interface complexity approaches the implementation complexity, the module is shallow. Restructure it.

When splitting a function/class into two pieces:

  • Each resulting piece must provide a different abstraction that is independently useful.
  • If both pieces have similar signatures and similar levels of abstraction, the split made things worse -- merge them back.
  • Splitting a method only to make it shorter, without creating a cleaner abstraction, produces shallow modules.

When merging pieces together:

  • Merge when pieces share information that would otherwise leak between them.
  • Merge when combining simplifies the caller's job (one call instead of two coordinated calls).
  • Merge when it eliminates code duplication.
  • Do NOT merge if the pieces are independently useful and combining would create a multi-purpose module with no clear single abstraction.

Information Hiding

Every module should encapsulate design decisions (data structures, algorithms, serialization formats, protocols) that no other module needs to see.

Test for information leakage: If two modules both depend on the same design decision (e.g., a file format, a protocol, an ordering), information is leaking. Fix by consolidating that knowledge into one module.

Test for temporal decomposition: If your module boundaries mirror the order of operations (read, then process, then write) rather than grouping by shared knowledge, you have temporal decomposition. Restructure so that all code dealing with the same information lives together, regardless of when it executes.

Test for overexposure: If the caller must understand implementation details to use the module correctly, the interface is leaking. Shrink the interface by hiding those details.

Generality: the "Somewhat General-Purpose" Sweet Spot

When designing a module, make the interface general-purpose and the usage special-purpose -- even if there is currently only one caller.

Three questions to test your interface:

  1. "What is the simplest interface that covers all my current needs?" -- If the interface has methods used by only one caller, those methods are probably too special-purpose.
  2. "In how many situations will this method be used?" -- If only one situation, it is likely too specific. Generalize the method and let the caller supply the specifics.
  3. "Is this API easy to use for my current needs?" -- If a caller must write boilerplate to bridge between the general-purpose interface and its current need, the interface is too general.

Different Layer, Different Abstraction

Each layer in a call chain must provide a fundamentally different abstraction from adjacent layers. Detect violations with these red flags:

Pass-through method: A method that does little except call another method with a similar or identical signature. Fix by: (a) eliminating it, (b) redistributing functionality so each layer does something distinct, or (c) merging the two layers.

Pass-through variable: A variable threaded through many methods solely so a deeply nested function can access it. Fix by: storing it in a shared context object, or a module-level/class-level attribute that the deep function can access directly.

Decorator/wrapper that mirrors the underlying interface: If the decorator's methods mostly forward to identically-named methods on the wrapped object, the decorator is a shallow layer. Only use decorators when they add substantial new behavior distinct from the underlying class.

Pull Complexity Downward

When you have a choice about where to handle complexity, push it into the implementation rather than the interface. A module with a complex implementation but simple interface is better than the reverse.

Decision procedure:

  1. Can the module handle the complexity internally without exposing it to callers? If yes, do it -- even if it makes the implementation harder.
  2. Is the current design forcing callers to provide configuration or make decisions that the module could figure out on its own? If yes, add a reasonable default and let the module decide.
  3. Would handling it internally require knowledge the module doesn't have? Then the complexity genuinely belongs in the caller.

Configuration parameters are a red flag. Each configuration parameter is complexity pushed to the caller. Before adding one, ask: can the module compute a reasonable value automatically? If yes, do that instead.

Define Errors Out of Existence

Exceptions are a major source of complexity. Reduce the number of places exceptions must be handled using four techniques, in order of preference:

  1. Define errors out of existence: Redefine the operation's semantics so the "error" condition is simply part of normal behavior. Example: a delete operation that succeeds even if the target doesn't exist (its contract is "ensure target doesn't exist," not "remove existing target"). Example: a substring that clamps out-of-range indices instead of throwing.
  2. Exception masking: Handle the exception at a low level so higher levels never see it. Example: a network layer that transparently retransmits lost packets. This is pulling complexity downward.
  3. Exception aggregation: Instead of catching each exception individually at the point of origin, let exceptions propagate upward and catch them with a single handler at a higher level. Example: a web server that catches all request-processing exceptions in one top-level handler that returns an error response.
  4. Just crash: For errors that are rare, hard to handle, and offer no meaningful recovery (e.g., out of memory, corrupted internal state), abort with a clear diagnostic message.

When NOT to apply: If callers genuinely need the exception information to make decisions, the exception must be exposed even though it adds complexity. Only define away or mask exceptions when the information is not needed outside the module.

Design It Twice

Before committing to any significant design decision, sketch at least two radically different approaches. Compare them on:

  • Interface simplicity (fewer concepts for callers to learn)
  • Generality (how many situations does each approach handle?)
  • Performance
  • Ease of implementation

Pick the best, or combine strengths from multiple alternatives. This applies to: interface design, implementation strategy, module decomposition, and data structure choice.

Comments

Write comments that describe things that are not obvious from the code. Use different words from the name of the entity being documented.

Interface comments (every public class, method, and function):

  • First sentence: what the method does from the caller's perspective (the abstraction).
  • Each parameter: what it means, its constraints, its units, whether null is permitted.
  • Return value: what it represents, edge cases.
  • Side effects: any state changes beyond the return value.
  • Exceptions: what can be thrown and when.
  • Preconditions: what must be true before calling.
  • If the interface comment must describe implementation details to be complete, the method is shallow -- redesign it.

Data structure / field comments (every instance variable and important field):

  • Describe what the variable represents (nouns), not how it is manipulated (verbs).
  • State units, boundary conditions (inclusive/exclusive), null semantics, invariants.

Implementation comments (inside method bodies):

  • Place before major blocks to describe what the block does and why, not how.
  • For loops, describe what each iteration does at a higher level than the code.
  • For tricky code, explain the reasoning or link to an issue.

Cross-module comments (for design decisions spanning multiple modules):

  • Place the authoritative documentation in the most central location.
  • Add short references ("See designNotes: Zombies") in other modules.
  • Never duplicate cross-module documentation -- it will become stale.

Naming

Choose names that create a clear image of the underlying entity.

  • Precision test: "If someone sees this name in isolation, will they correctly guess what it refers to?" If not, choose a more specific name.
  • Consistency: Use the same name for the same concept everywhere. Never reuse a name for a different concept. Ensure all variables with a given name have the same behavior.
  • Scope-proportional length: i is fine for a 3-line loop. A variable used across a large span of code needs a descriptive name.
  • Boolean names should be predicates: is_valid, has_items, cursor_visible -- not status, flag, blink.
  • Avoid filler words: Drop Object, Field, Data, type prefixes, class-name prefixes from variable names unless they add genuine disambiguation.
  • Hard to name = design smell: If you can't find a simple, precise name, the underlying concept may not have a clean design. Consider restructuring.

Consistency

Follow existing conventions in the codebase, even if you would do it differently.

  • Before introducing a new convention, verify that no existing convention covers the same situation.
  • Having a "better idea" is not sufficient reason to introduce inconsistency. The value of consistency almost always exceeds the value of a marginally better approach used in only some places.
  • Do not force dissimilar things into the same pattern. Consistency only helps when "if it looks like an X, it really is an X."

Red Flags Checklist

Use this checklist during code review or when evaluating your own design decisions.

Red FlagDiagnostic Question
Shallow ModuleIs the interface complex relative to the functionality provided?
Information LeakageDoes the same design decision appear in multiple modules?
Temporal DecompositionAre module boundaries based on operation order rather than information grouping?
OverexposureMust callers know implementation details to use the module?
Pass-Through MethodDoes this method do little except call another method with a similar signature?
Pass-Through VariableIs a variable threaded through many methods just to reach a deeply nested consumer?
RepetitionDoes the same code/pattern appear in multiple places, suggesting a missing abstraction?
Special-General MixtureDoes general-purpose code contain special-case logic, or vice versa?
Conjoined MethodsMust you read another method to understand this one?
Comment Repeats CodeCould someone write this comment just by reading the code next to it?
Implementation Contaminates InterfaceDoes the interface comment describe implementation details callers don't need?
Vague NameIs the name broad enough to refer to many different things?
Hard to Pick NameIs it difficult to find a simple, precise name? (Underlying concept may not be clean.)
Hard to DescribeIs it difficult to write a simple, complete comment? (Design may need restructuring.)
Nonobvious CodeCan the meaning and behavior be understood with a quick reading?

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.