# Pony Software Design

> Disciplines for software design work. Load when designing APIs, type systems, features, or system boundaries. Counters the tendency to retrieve familiar patterns instead of discovering what the problem actually needs. Has full (8-persona) and lightweight (5-persona) modes.

- Skill: `ponylang/pony-software-design` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add ponylang/pony-software-design`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ponylang/pony-software-design/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ponylang (https://skillmd.com/u/ponylang)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ponylang/pony-software-design

---


# Software Design

Load this skill when doing design work — APIs, type systems, features, system
boundaries. The core problem it addresses: LLMs default to *retrieving*
familiar patterns from training data rather than *discovering* what a specific
problem needs. The result is designs that look right (they have the right nouns)
but weren't derived from the problem.

Design is the act of discovering what is needed. It's about finding surprising
affordances and avoiding candy-machine interfaces riddled with footguns.

Design is not a phase. It's a continuous loop: observe what you have, orient
against what you know, decide, act, then observe again. Every decision you make
reveals new information. That information might confirm prior decisions or
invalidate them. Either way, you have to look. "I already decided that" is never
a reason to skip re-evaluation — it was decided with less information than you
have now.

## Mode selection

The skill has two modes: **full** and **lightweight**. The orchestrator
selects the appropriate mode based on the criteria below and proceeds.
Report the mode choice when presenting results — the human can request
full mode if lightweight was used and they want deeper coverage.

**Full mode** is the default. Use it when:

- Defining new boundaries — a new subsystem, API surface, or type hierarchy
- Multiple ownership or abstraction decisions need to be made simultaneously
- There's genuine design space to explore — multiple viable approaches
- The design must be described from first principles, not as an extension of
  something that already exists

**Lightweight mode** is for bounded design work within established patterns:

- Adding a method to an existing API, a new variant in a type family, a new
  handler following established conventions
- The boundaries are already decided — you're filling in, not redrawing
- Consumer code patterns are already established by adjacent features
- The task can be described as "another X that does Y" where X already exists
  in the codebase

When in doubt, use full mode. Lightweight is appropriate when there's a
clear existing pattern being extended and the boundaries are already
decided.

## Process: full mode

Design work is where pattern-matching failures are most costly and hardest to
self-detect. A single agent applying design disciplines will still
pattern-match — the disciplines become post-hoc rationalizations for a
retrieved design rather than actual constraints on the exploration.

The full design process runs in two stages with a feedback loop. Load
`pony-ensemble` for the mechanical process; the personas defined in this skill
replace the generic attention focuses.

### Relationship to Ensemble Workflow

This skill uses the ensemble workflow with domain-specific customizations.
Stage 1 (design) runs as a standard ensemble with 3 personas. Stage 2
(evaluation) runs as a second ensemble with 5 personas, using the Stage 1
synthesis output as its input. The two-stage loop and finding categorization
(Rejection/Adjustment/Tension) are additions specific to this skill — the
base ensemble protocol handles agent spawning, triage, and synthesis
mechanics.

### Orchestrator pre-spawn: understand the problem

Before spawning design personas, the orchestrator must decompose the problem
statement. Ask: "What are we trying to accomplish? What pain point does this
address?"

If the problem statement implies a solution rather than stating a problem —
"add convenience wrapper methods for X" rather than "users struggle with X
because Y" — peel it back to the underlying pain point. If you can't
confidently identify the pain point, ask the human. This is not a failure;
it's the most valuable question the orchestrator can ask, because every
persona will anchor on whatever framing they receive.

Brief personas with the actual problem. The original problem statement can be
included as context, but the briefing should lead with the pain point: what the
user is struggling with and why. This gives personas room to explore different
solutions rather than anchoring on the solution implied by the problem
statement.

### Stage 1: Design

Three design personas explore the problem space in parallel. Each applies all
the disciplines below but enters the problem from a different direction. The
decorrelation comes from where they start, not what they know. Persona
definitions are in `personas/design/`.

| File | Focus |
|------|-------|
| `consumer-first.md` | Starts from usage code, derives API from what makes call sites clean |
| `skeptic.md` | Questions every abstraction, tries to subtract, proposes the smallest design |
| `principle-checker.md` | Hard verification of each design principle with evidence |

### Orchestrator post-return: evaluate exploration quality

When personas return, the orchestrator evaluates the quality of exploration
before forwarding outputs to synthesis. Check:

- **Did the personas explore meaningfully different approaches?** Read each
  persona's "Key decisions / alternatives considered" section. If a persona
  only considered one interpretation of the problem and designed for it, the
  exploration was insufficient.
- **Did they all anchor on the same narrow interpretation?** If all three
  personas produced designs based on the same framing of the problem — e.g.,
  all three designed wrapper methods — the ensemble failed to decorrelate.
  The different entry points weren't enough to overcome the anchoring effect
  of the problem statement.
- **Is the reasoning for rejecting alternatives sound?** A persona that
  considered a wild idea and rejected it with good reasoning explored
  genuinely. A persona that listed an alternative and dismissed it in one
  sentence didn't.

If exploration quality is poor — narrow interpretation, insufficient
alternatives, weak rejection reasoning — don't forward to synthesis. Refine
the briefing: restate the problem more broadly, point out the anchoring you
observed, and ask the human to clarify if needed. Then re-spawn.

### Stage 1 synthesis

Stage 1 synthesis produces a candidate design using the standard ensemble
synthesis process. The synthesis should pay special attention to:

- Where the consumer-first designer's sketches conflict with the skeptic's
  subtractions — the tension usually reveals the right boundary
- Where the principle checker found a violation that the others missed —
  this is the highest-value finding
- Whether all three converged on the same abstraction — convergence from
  different starting points is strong signal
- **When the skeptic says "no value" and redirects**: The skeptic is the only
  persona whose job includes saying "this shouldn't exist." When the skeptic
  concludes the proposed design doesn't earn its keep and redirects toward a
  different problem, this is not a minority position to be outvoted because
  the other two produced designs. The other two will *always* produce a
  design — that's their job. Evaluate the skeptic's case on its merits. If
  the skeptic is right that the design is a thin wrapper, the synthesis
  output should adopt the skeptic's redirect as the foundation: "the
  proposed design doesn't earn its keep — here's the actual problem worth
  solving" becomes the candidate, not a blended version of the thin wrapper.

### Stage 2: Evaluation

Five evaluation personas stress-test the candidate design in parallel. Their
input is the Integrated Result from Stage 1 synthesis — the candidate design
with its consumer sketches, type definitions, and boundary decisions. They
evaluate these design artifacts, not implementation code. Persona definitions
are in `personas/evaluation/`.

| File | Focus |
|------|-------|
| `security.md` | Trust boundaries, attack surfaces, resource bounds in the design |
| `performance.md` | Architectural bottlenecks, coordination points, data structure choices |
| `adversarial.md` | Concrete usage scenarios that lead to bad outcomes |
| `testability.md` | Whether the design is verifiable — observable effects, isolatable components |
| `wildcard.md` | What all the other personas missed |

For the wildcard persona specifically: include the identity statement (first
paragraph) from each of the other personas so the wildcard knows what
territory is already covered.

Before spawning evaluation personas, create a temporary directory for evidence
files (`~/tmp/design-eval-<timestamp>/`). Each persona writes its detailed
analysis to a file in this directory and returns a structured summary to the
orchestrator. The synthesizer works from summaries and digs into evidence
files only when it needs to examine a finding more closely. This prevents
context overload during synthesis.

Evaluation personas identify problems and assess impact — they do not
categorize their own findings as Rejection/Adjustment/Tension. Categorization
is the synthesis step's responsibility.

### Evaluation persona output format

Each evaluation persona produces two artifacts:

**Evidence file** — written to the path provided by the orchestrator. Contains
the full detailed analysis: every finding with complete evidence, full design
element excerpts, detailed reasoning, and complete pass/fail evaluations. This
is the authoritative record.

**Summary** (returned to orchestrator) — a structured summary for the
synthesizer to work from:

**Findings** — ordered by impact (Structural > Significant > Minor). Each:

- **Design element**: The type, boundary, API, or interaction being evaluated
- **Concern**: What the problem is
- **Impact**: Structural (requires rethinking the approach), significant
  (requires notable changes to the candidate), or minor (a local
  change; the rest of the candidate is unaffected)
- **Evidence**: Brief — full evidence is in the file
- **Suggested change**: Write it whenever you can. A concern you can name but
  cannot resolve is still a finding — say so, and say what blocks you.

Impact is what the finding costs the candidate: how much of it has to
change. It is not permission to leave the finding alone — report a minor
finding exactly as you report a structural one, and stage 2 synthesis categorizes
every finding, at every impact level.

The impact assessment helps the synthesizer with categorization without
pre-empting it. A persona's "structural" assessment is a strong signal toward
Rejection, but the synthesizer may disagree if it sees the concern addressed
by another persona's suggestion.

**Passes** — things checked that look correct. Brief.

**Uncertainties** — things the persona couldn't determine, and why.

Stage 2 synthesis works from the persona summaries and categorizes each
finding. Provide the paths to each persona's evidence file so the synthesizer
can dig in when it needs more context — when impact assessments conflict, when
a finding's summary is ambiguous, or when it needs to verify the evidence
supports the concern.

Stage 2 synthesis categorizes each finding:

- **Rejection**: A structural problem that invalidates the design direction.
  The candidate cannot be fixed by adjustment — the design personas need to
  rethink the approach. The rejection includes why the direction fails and
  what constraint it violates.
- **Adjustment**: A specific aspect that needs to change, but the overall
  direction is sound. Becomes a constraint for the next design iteration.
- **Tension**: A fundamental conflict that the personas cannot resolve — it
  requires human judgment. Collected and presented at the end.

### The loop

After stage 2 synthesis:

1. If there are only tensions (no rejections or adjustments), the loop
   terminates. Present the design with the tensions for human review.
2. If there are adjustments and/or rejections, feed them back to the design
   personas. Each design persona receives: the prior candidate design, the
   original problem statement, and the categorized findings. Rejections include
   the rationale for why the direction failed — design personas should explore
   a different approach, not patch the rejected one. Adjustments include the
   specific change needed and why — design personas should revise the candidate
   to incorporate them as constraints.
3. The design personas run again with this context, producing a revised
   candidate.
4. Evaluation runs again on the revised candidate. Evaluation personas run with
   fresh context (no knowledge of prior evaluations). The synthesis step
   receives the full history so it can track convergence.
5. Repeat until clean or until convergence failure is detected.

### Convergence failure

The orchestrator monitors the loop for signs that it isn't converging:

- The same evaluation concern keeps appearing across iterations, even after
  design revisions attempt to address it
- Rejections and adjustments are contradicting each other (fixing one
  evaluation concern breaks another)
- The design is growing more complex with each iteration rather than settling

When a convergence failure is detected, the orchestrator stops the loop and
escalates to the human: "Here's the fundamental tension — these concerns pull
in opposite directions, and we need you to decide which matters more." This is
not a failure of the process. Surfacing genuine tensions is one of its primary
outputs.

### Output

The final output includes:

- **Accepted design** (if one emerged): The candidate that passed evaluation,
  with consumer sketches, type definitions, and boundary decisions.
- **Rejected designs**: Each candidate that was rejected during the loop, with
  the rejection rationale. These are valuable — they document explored
  territory and why it didn't work.
- **Unresolved tensions**: Findings categorized as tensions that require human
  judgment.

If the loop terminated via convergence failure rather than a clean evaluation,
there is no accepted design — only the history of attempts, the rejections, and
the tensions.

## Process: lightweight mode

Lightweight mode uses fewer personas and a single pass. It keeps all three
design personas but reduces evaluation to two personas and drops the
feedback loop. Load `pony-ensemble` for the mechanical process.

### Orchestrator pre-spawn: understand the problem

Same as full mode. Before spawning personas, decompose the problem statement
to the underlying pain point. If the problem statement implies a solution,
peel it back or ask the human. Brief personas with the actual problem.

### Stage 1: Design

Three design personas explore the problem in parallel — the same as full
mode. The same disciplines apply; the decorrelation still comes from
different entry points.

| File | Focus |
|------|-------|
| `consumer-first.md` | Starts from usage code, derives API from what makes call sites clean |
| `skeptic.md` | Questions every abstraction, tries to subtract, proposes the smallest design |
| `principle-checker.md` | Hard verification of each design principle with evidence |

### Orchestrator post-return: evaluate exploration quality

Same as full mode. Evaluate whether personas explored meaningfully different
approaches before forwarding to synthesis. If all three anchored on the same
narrow interpretation, re-brief and re-spawn.

Stage 1 synthesis produces a candidate design using the standard ensemble
synthesis process. The same synthesis guidance as full mode applies:
consumer-first vs skeptic tensions reveal boundaries, principle-checker
violations the others missed are the highest-value findings, and
convergence from different starting points is strong signal.

### Stage 2: Evaluation

Two evaluation personas stress-test the candidate. The adversarial evaluator
always runs. The second slot is context-dependent — if the human specifies
which evaluator to use, use that. Otherwise the orchestrator picks
whichever lens is most relevant to the task:

| When | Pick |
|------|------|
| Design touches trust boundaries or external input | `security.md` |
| Design is on a hot path or introduces coordination points | `performance.md` |
| Design has complex state or will be hard to test in isolation | `testability.md` |

Pick whichever is closest — every design has some risk profile. If
multiple conditions apply, pick the most relevant one. If the reason
for picking a particular evaluator is a characteristic that also
appears in the full-mode selection criteria, that's a signal the task
warrants full mode — don't use the persona pick to compensate for a
wrong mode selection.

Before spawning evaluation personas, create a temporary directory for
evidence files (`~/tmp/design-eval-<timestamp>/`), same as full mode.
Each persona writes its detailed analysis to a file in this directory and
returns a structured summary. Evaluation personas use the same output
format as full mode (evidence file + summary with findings ordered by
impact, passes, uncertainties).

Stage 2 synthesis categorizes each finding as Rejection, Adjustment, or
Tension using the same scheme as full mode.

### No loop — single pass

Lightweight mode does not iterate. After Stage 2 synthesis:

- **Adjustments and tensions**: Present the design with findings to the human.
  Adjustments are expected to be small enough that the orchestrator or human
  can apply them directly. If adjustments collectively amount to redesigning
  rather than tweaking, that's the same escalation signal as high finding
  density — present it to the human.
- **Rejection**: The design direction is wrong. Present the rejected
  candidate, the rejection rationale, and all other findings to the human.
  The human decides what to do — escalate to full mode, fix it directly,
  rethink the problem statement, or something else. Lightweight doesn't
  prescribe the response; it presents the information.

If the review produces an unexpectedly high density of findings relative to
the change size, if a finding reveals the approach is fundamentally wrong,
or if a finding reveals the change touches more subsystems or has more
complex interactions than the mode selection assumed, the orchestrator
presents this to the human. The human decides what to do — the same options
apply.

### Output

The final output includes:

- **Candidate design**: With consumer sketches and type definitions.
- **Adjustments**: Specific changes needed, small enough to apply directly.
- **Tensions**: Conflicts requiring human judgment.
- **Rejection rationale**: If applicable — the structural finding and why
  the design direction was rejected.

## Design values

When principles conflict, these values set the priority. We value the left side over the right — but the right side still matters when the left isn't at stake.

**API safety over API minimality** — an error-prone API should be fixed even if the fix adds surface. Prefer solutions that don't expand the API, but never leave a footgun to preserve minimality.

**Correctness over performance** — never sacrifice correctness for speed. Get it right first, then optimize. A faster wrong answer is still wrong.

**Correctness over concision** — correct but verbose beats concise but wrong. Don't simplify code or APIs at the cost of correct behavior.

**Security over performance** — never skip validation at trust boundaries for speed. Optimize how you validate, not whether you validate. Security is correctness.

**Interface simplicity over implementation simplicity** — accept a harder implementation to give users a clean interface. The consumer's experience matters more than the implementer's convenience.

**Performance over interface simplicity** — runtime speed matters more than programmer convenience. It's acceptable to make things harder on the user to improve performance, but never at the cost of correctness.

**Simplicity over consistency** — don't force artificial consistency when it makes things harder to use. If two similar things genuinely need different interfaces, let them be different.

**Explicitness over implicitness** — when the language allows something to work by magic (implicit conversions, convention-based wiring, unnamed dependencies), prefer the version that states what's happening. The cost of a few extra characters is less than the cost of reconstructing hidden knowledge.

**Type safety over convenience** — use the type system to encode constraints even when it's more work. Distinct types for distinct semantics, validated wrappers over raw primitives, explicit error vocabularies over generic errors. "We could just use a String here" is almost always wrong.

**Changeability over predictive design** — make designs modular and replaceable so future needs can be accommodated, but don't add abstractions, extension points, or features for changes that haven't happened yet. Easy to modify beats designed for a specific predicted modification.

## The disciplines

These are the foundation each persona builds on. Every agent applies all of
them.

### Start from the problem, not the solution

State what problem the user has before proposing any types, traits, or APIs.
"The user needs X" comes before "here's a `SessionStore` trait." If you can't
articulate the problem without referencing your solution, you don't understand
the problem yet.

### Explore before committing

If you only have one idea, you don't have any. The first interpretation of a
problem statement is pattern retrieval — the LLM equivalent of "this looks like
a thing I've seen before." Design starts when you generate a second
interpretation that's genuinely different, not a variation on the first.

Before committing to a direction, explore the design space. Generate multiple
approaches internally — different framings of what the problem is asking for,
not just different implementations of the same framing. Include wild ideas.
They're valuable not because you'll use them, but because they reveal what
matters about the problem. An idea you reject teaches you something about why
you rejected it — that "why" is design knowledge.

Present your best idea, not your first idea. The output is the winner of an
internal competition. The ensemble output format has a "Key decisions:
alternatives considered" section — use it to document what you explored, what
you picked, why, and why you rejected the alternatives. This isn't bookkeeping;
it gives the orchestrator visibility into whether real exploration happened.

The problem statement is where exploration starts, not where it ends. "Add
convenience wrapper methods" is a hint about a pain point, not a design
specification. What pain? Why is the current approach painful? What would
"convenience" actually mean to the user? Different answers to those questions
lead to genuinely different designs — one of which might be "thin wrappers"
and another might be "a unified API that abstracts away the underlying
complexity." Both are valid interpretations of "convenience"; only exploration
reveals which one earns its keep.

### Sketch consumer code first

Before designing any API, write the code that *uses* it. The handler, the
call site, the configuration — the actual application code a user would write.
Not as an afterthought example, but as the first artifact. The consumer sketch
is the specification. It reveals:

- What the API actually needs to provide (and what it doesn't)
- Where type safety breaks down (runtime casts, stringly-typed maps)
- Whether the abstraction can actually serve its purpose (can middleware do
  async work? can the handler access typed data?)
- What the error paths look like from the consumer's perspective

If the consumer code is awkward, the API is wrong. Fix the API, not the
consumer code.

**When claiming consistency between two APIs** (e.g., "guards use the same API
as handlers"), write both consumer sketches side by side. If the method names,
signatures, or interaction patterns differ, the claim is false — address the
discrepancy before proceeding.

### Inventory before inventing

Before proposing a new type, trait, or abstraction, write down what already
exists that addresses the same need: in the codebase, in the language's stdlib,
in the ecosystem. If nothing exists, say so explicitly. If something exists,
start from it — extend, adapt, or compose it rather than building a parallel
structure.

On a greenfield project, "what already exists" means the language's built-in
types, stdlib, and idioms. A new type that duplicates what the language provides
is a smell.

This is not "reuse for reuse's sake." It's a forcing function against the
pattern-matching tendency to invent new abstractions when the problem doesn't
require them.

### Build up incrementally

Don't design the whole thing at once. Start with the smallest coherent piece.
Validate it with a consumer sketch. Then add the next piece and see if it fits.

At each step, ask:
- Does the new piece fit naturally with what's already there?
- Or is it fighting the existing design?
- If it's fighting, is the problem upstream? Would a different foundation make
  this piece fit naturally?

This is how you discover the shape of the problem. A big-bang design papers
over these tensions. Incremental exploration surfaces them while they're cheap
to fix.

### Every step changes what you can see

Every design decision is made with incomplete information. As you explore
further, the territory expands. At step B you could see one option and picked
it. At step D you can see two options that weren't visible from B. Step C might
provide evidence that the option you didn't pick is actually better. None of
this means B was wrong at the time — it means the landscape changed and you need
to look again.

The way you discover this is by constantly pushing on the design: "what if we
did it this other way?" "Does this conform to our principles?" "This doesn't
feel right — why?" These aren't idle questions. They're how you explore more of
the map. Each question might reveal new options, new evidence, or new
connections between decisions you thought were independent.

When the landscape changes, trace back through prior decisions. Not to check
whether they were "disproved" — that's too binary. Check whether the option
space has expanded. Maybe at step B you picked X because it was the only option
you could see. Now at step D you can see X and Y. Which is better given
everything you've learned? Maybe X is still right. Maybe Y is clearly better.
Maybe you need to explore further to tell. All three of those outcomes require
you to actually go back and look rather than assuming B is settled.

This is the core of the design loop. Skipping it is how you end up with designs
that look coherent on the surface but have quiet contradictions baked in — an
app-level-only registration model sitting next to a radix tree that already
knows the route hierarchy, because nobody went back to check whether "app-level
only" still made sense after discovering what the tree could do.

The cost of revisiting decisions is real but bounded. The cost of building on a
decision that should have been revisited compounds with every step forward.

### Question every abstraction

For each type, trait, or interface in your design, ask: is this here because the
problem requires it, or because other systems have it? "Sessions usually have a
SessionStore" is not a reason. "The framework needs to persist session data" is
a reason — but only if the framework actually needs to own that responsibility.

The strongest signal that you're importing rather than discovering: your design
has the same nouns as Rails/Phoenix/Express/Django and you're working in a
language with fundamentally different idioms.

### Name things precisely

Names are the primary interface of a design — they're how it communicates
intent to every future reader. A design that's structurally sound can still
fail in practice because a name suggested something different from what the
thing actually does, and the user built a wrong mental model from it.

This discipline earns its keep at boundaries: public API types, method names,
parameter names, module names — anywhere a user encounters a name and forms an
expectation about what it means. Internal names matter too, but the cost of a
misleading public name is much higher because it shapes how every consumer
understands the system.

At each design step, ask:

- **Does the name come from the problem domain or the solution domain?**
  Problem-domain names ("invoice," "route," "subscription") connect the code
  to what users already understand. Solution-domain names ("manager,"
  "handler," "processor") describe implementation roles that tell the user
  nothing about what the thing actually does. Prefer problem-domain names for
  types the user interacts with; reserve solution-domain names for internal
  machinery where they're the clearest description of the role. Note that some
  of these words become problem-domain vocabulary in specific contexts —
  "handler" is the natural term in web frameworks and event systems. The
  concern is with using them as generic suffixes that avoid naming what the
  thing actually handles or manages.
- **Does the name describe what this thing does, or just what category it
  belongs to?** "Validator" says it validates — but what?
  "InvoiceAmountValidator" says what it validates. "Store" says it stores —
  but a `UserStore` that also sends email notifications lies about its
  responsibilities. A name that categorizes without specifying is a name that
  lets scope creep in without anyone noticing.
- **Could the name mislead a user about what this thing does?** If someone
  reading only the name would form a wrong expectation about the behavior, the
  name is wrong. This includes names that are accurate but incomplete — a
  `Cache` that also writes through to the database is misleading because the
  name suggests read-only caching.
- **Are there names that sound similar but mean different things?** `Session`
  and `SessionState`, `Token` and `TokenData`, `Route` and `Router` — when
  names differ by a suffix or prefix, users will assume the relationship is
  systematic. If `SessionState` is not the state of a `Session`, the naming
  implies a relationship that doesn't exist.
- **Are there names that sound different but mean the same thing?** If the
  design uses `user` in one place and `account` in another for the same
  concept, readers will assume these are different things and spend effort
  trying to understand the distinction. One concept, one name — everywhere.
  This is about vocabulary consistency, not boundary-qualified variants like
  `UserInput` and `UserRecord` — those are distinct types that need distinct
  names per "Distinguish values with distinct semantics."

"Distinguish values with distinct semantics" ensures different concepts get
different types. This discipline ensures those types get names that communicate
what they actually are. A type can be correctly distinct and still misleading
if its name suggests the wrong thing.

When this discipline pushes for more precise names and the skeptic questions
whether the named concepts need to exist at all, surface the tension — both
are needed. Good names make abstractions easier to evaluate: a precisely named
type reveals its purpose, which either justifies or undermines its existence.

### Reason about ownership boundaries

For every capability in the design, ask: does the framework/library own this, or
does the user own this? The answer should come from analysis of the consumer
sketch, not from "frameworks usually own this."

The test: if you removed this from the framework and the user did it themselves,
would anything break? Would anything get worse? If the user can do it better
(more type-safe, more flexible, more natural in the language), the framework
shouldn't own it.

This discipline draws the line between framework and user — the external
boundary. For application-level design where the question is how to organize
*within* the user's code, see "Separate layers."

### Separate layers

"Reason about ownership boundaries" asks whether the framework or the user
should own a capability — the external boundary. This discipline asks the
internal question: within the application, does each piece of logic live in the
right layer?

Layered applications have three layers: domain logic (pure business rules,
zero infrastructure dependencies), orchestration (combines domain logic with
infrastructure — databases, caches, queues), and presentation (adapts
orchestration for a specific protocol — HTTP, GraphQL, CLI). This discipline
operationalizes that separation as design-time questions.

**Scope**: This discipline applies when the skill is used for application-level
design — systems with distinct domain, orchestration, and presentation concerns.
For library or API design where there's no application layering, this discipline
is not relevant. The ownership boundary discipline covers the framework/library
boundary instead. Note that "infrastructure" in the questions below means
infrastructure *relative to the application's domain* — a deployment tool's
domain vocabulary naturally includes container and orchestration concepts, and
"infrastructure" for that tool means the specific database or message queue it
uses to do its work, not the deployment concepts it operates on.

At each design step, ask:

- **Which layer does this belong to?** For every type, function, or interaction
  in the design, classify it: domain, orchestration, or presentation. If you
  can't classify it cleanly, the boundaries are blurred — which usually means
  domain logic has acquired an infrastructure dependency or presentation logic
  has absorbed business rules.
- **Does this domain type depend on any infrastructure?** Domain types should
  have zero infrastructure dependencies — no database clients, no HTTP types, no
  cache interfaces. If a domain type needs to talk to infrastructure, it belongs
  in orchestration, or the domain type needs to express its need through an
  interface that orchestration satisfies.
- **Is orchestration leaking into domain types?** The tell: a domain type that
  knows about connection pools, transaction boundaries, or retry policies.
  Domain logic defines what should happen; orchestration decides how to make it
  happen with real infrastructure.
- **Is domain logic leaking into presentation?** The tell: a request handler
  that contains business rules instead of delegating to orchestration. When
  business logic lives in the presentation layer, it can't be reused by a
  different presentation (a CLI that needs the same logic as the HTTP API).
- **Could you swap the presentation layer without touching domain logic?**
  HTTP → CLI, REST → GraphQL, synchronous → message-queue-driven. If swapping
  the presentation layer requires changes to domain types, those types have
  presentation concerns baked in. This is a thought experiment, not a
  requirement to actually build multiple presentations — it tests whether
  the boundary is clean.
- **Is the presentation layer coupled to orchestration internals?** The tell:
  a request handler that manages transaction boundaries, knows about caching
  strategies, or handles retry logic instead of delegating those concerns to
  orchestration. Presentation should call orchestration and receive results —
  it shouldn't know how orchestration coordinates infrastructure.
- **Are there domain concepts that only exist because of an infrastructure
  choice?** A `PaginatedResult` type in the domain layer exists because the
  database returns paginated results — that's infrastructure leaking into domain
  vocabulary. The domain might need "a bounded subset of results" but shouldn't
  know that the bound comes from database cursor limits.

The test for clean layer separation: can you describe the domain logic without
mentioning any infrastructure technology? If explaining a business rule requires
saying "database," "HTTP," "cache," or "queue," the rule has infrastructure
dependencies that should live in orchestration instead.

"Check cohesion" asks whether things belong together within a type. This
discipline asks a related but distinct question: do things belong together
within a *layer*? A type can be internally cohesive but live in the wrong
layer — a well-structured `UserService` that's cohesive in its
responsibilities but mixes domain rules with database queries. Cohesion
would call it fine (all methods serve "user management"); layer separation
would flag the infrastructure
dependency in domain logic.

"Reason about ownership boundaries" draws the line between framework and user.
This discipline draws lines within the user's code. The two work at different
scales but share a principle: clear boundaries make it obvious where each
concern lives and prevent responsibilities from migrating to the wrong side.

When this discipline pushes toward extracting infrastructure from domain types
and the skeptic questions whether the indirection is worth it, surface the
tension. For small applications or early-stage code, the cost of clean
separation may exceed the benefit — the answer depends on the scale and
expected evolution of the application. But be honest about the tradeoff:
coupling that's cheap
today gets expensive as the application grows, and untangling it later costs
more than separating it now. The skeptic's role here is to prevent ceremony —
extracting an interface that has exactly one implementation and no prospect of a
second is indirection without value. The discipline's role is to prevent
entanglement — domain logic that can't be tested, reused, or understood without
its infrastructure context. When both concerns are real, that's a tension worth
presenting to the human.

### Map state explicitly

State is where most design complexity hides. Boolean flags, nullable fields,
and implicit modes are all state — just state without a name. Unnamed state
can't be reasoned about systematically: you can't enumerate its transitions,
verify its invariants, or ask what happens when an input arrives in a state
you didn't consider.

This discipline earns its keep when a component has multiple interacting pieces
of state, or when state determines which operations are valid. A single flag
that independently tracks one condition doesn't need a state machine — it needs
a good name. For everything else, answer these questions:

- **What states can it be in?** Not just the happy-path states — include
  initialization, error recovery, shutdown, and any transitional states between
  them.
- **What are the transitions?** What event or action moves the system from one
  state to another? Are there transitions the design allows but shouldn't?
- **What invariants hold in each state?** What can the rest of the system
  assume about a component that's in state X? For invariants that span beyond
  individual states — cross-component relationships, ordering constraints — see
  "Articulate invariants."
- **What happens when an input arrives in a state where it doesn't apply?**
  The design should answer this explicitly — ignore, error, queue, crash —
  rather than leaving it undefined.
- **Can the type system encode the states?** If the language supports it
  (union types, sealed classes, enums, trait-based state machines), prefer
  type-level encoding over convention-level tracking. When the type system
  encodes the states, the compiler enforces completeness — every handler must
  account for every state.

The test for whether you've mapped state well: can someone new to the design
draw the state diagram from your description? If not, the state model isn't
explicit enough.

### Articulate invariants

Every design establishes contracts — things that must always be true for the
system to work correctly. Callers rely on ordering guarantees, components depend
on data relationships, operations assume structural properties hold. When these
contracts are implicit, nobody can verify them, nobody knows when they've been
violated, and future changes break them without anyone realizing what happened
unti

…(truncated)
