software-design · git:20260921.0e6bccc · 2026-09-21 · sha256 a14ff975fa807043

software-design git:20260921.0e6bcccA

Immutable. This exact content is served forever at /api/v1/blob/a14ff975fa807043.

---
name: software-design
description: >-
  Design judgment for modules, interfaces, and where information lives. Use
  when designing or restructuring a module or interface, deciding where an
  abstraction goes, evaluating a decomposition, placing rationale in comments
  or scoped docs, or comparing alternative designs for a larger change.
---

# Software Design

**Reduce the amount of information a developer must know, and make the
remaining information obvious.** Software complexity is whatever makes a
system difficult to understand or modify—especially when essential information
is hidden or distant. The goal is **information locality**: the information
needed to understand and modify a part of the system is colocated and easy to
find.

Design for the reader, and the reader is a fresh session: the next engineer, a
reviewer, or an agent with no memory of this conversation. Whatever the author
knew — the request, the rejected alternatives, the failure that shaped the
fix — survives only if it lands in the repository: code, names, comments,
contracts, tests, or the nearest scoped `AGENTS.md`. Judge a design by how
much a fresh reader must gather before they can change it safely.

## Deep modules

A deep module concentrates knowledge: a good abstraction replaces a large
implementation burden with a much smaller interface burden. Callers get
leverage — one implementation pays back across every call site — and
maintainers get locality — change, bugs, and verification concentrate in one
place. When designing an interface, ask: can it have fewer methods, simpler
parameters, more hidden inside?

- **The interface is the test surface.** Callers and tests use the same
  interface. Wanting to test past the interface usually means the module is
  the wrong shape.
- **The fresh-reader test.** Can someone use the module correctly from its
  interface alone, without opening the implementation or its siblings? If
  crucial behavior is discoverable only by reading the implementation, the
  interface is incomplete; state it in the interface's comment.

## Decomposition

More files, functions, layers, or services is not more modularity. Every
split adds a place a reader must find, load, and reconnect — so split only
when the piece can be understood from its interface without loading its
neighbors. When understanding one piece routinely requires reading three
siblings, the split scattered the logic instead of hiding it; recombine or
deepen instead.

Scattering applies to knowledge too: one invariant half-stated in two
documents, a policy duplicated across layers, a rationale narrated in both a
comment and a nearby `AGENTS.md`. Keep one canonical statement at the
narrowest durable place and link to it from everywhere else — two copies
drift.

There is no useful static threshold for size — lines of code must grow with
essential behavior. A long module that reads top to bottom in one place often
beats a short one that forwards to five others. Judge by the information a
reader must hold, not by static analysis.

## Volatility

How much change is coming to a part of the system is usually written down,
not guessed. The gap between the project's declared goal state and the current
implementation, its roadmap and scope decisions, and open issues all declare
where change is planned and where it is not.
Commit history is not a signal: frequent edits can mean a bad design
forcing rework, not a volatile domain.

An awkward design in an area with no declared change ahead is tolerable
debt. The same flaw on a path the roadmap keeps touching taxes every
future change. When nothing declares an area's future, assume ordinary
volatility rather than using stability as an excuse. Spend design effort
where declared change is coming.

## Where information lives

Code states what happens; it cannot state intent, rejected alternatives,
incident history, or which behavior callers may rely on. Put each fact at the
narrowest durable place a reader will actually encounter:

- **A type, schema, constraint, or lint** for anything an agent could
  otherwise silently violate — prose is advisory; mechanisms survive
  imperfect attention.
- **The interface's comment** for the contract: what callers may depend on.
- **An adjacent comment** for local rationale a reader cannot reconstruct
  from the code.
- **The nearest scoped `AGENTS.md`** for subsystem workflow and conventions.

## Comments

**A comment carries information that cannot be expressed in code.** The test
for any comment: does it help the next reader build an adequate theory of the
program? The author is inside the box, writing to a reader who stands outside
and cannot see what the author saw.

A comment that restates *what* the code does fails the test — it adds reading
without adding information, and it goes stale when the code moves. Write what
the code cannot say:

- **On an interface, what a caller would otherwise get wrong.** Imagine a
  caller who sees only the signature. What would they assume wrongly? What
  would they have to open the implementation to learn? Say that. Use a name
  or a type first when the language allows, and comment only what is left. If
  the signature says everything, write no comment. A widely used interface
  needs its full contract. A private helper with one nearby caller usually
  needs nothing. Do not repeat what the module's comment already says.
- **In an implementation, why — and whatever is not obvious.** The reason the
  code takes this shape (efficiency, a vendor quirk, an incident), the key
  idea of a non-obvious algorithm, the invariant that must not change
  casually, and where it is verified.
- **Key ideas and conclusions, stated explicitly.** Do not leave the reader
  to deduce what the author already knows.

A comment cannot — and need not — say everything. Comments about purpose and
reasoning stay true longer than comments that name the pieces currently in
place.