| name | architecture |
| description | Use this skill when organizing directories, designing namespace boundaries, or deciding what belongs at the top level. Covers Screaming Architecture, domain-as-interface seams (Ports & Adapters at the directory level), and implicit/explicit API pairs. |
Architecture Principles
When This Skill Applies
Use this skill whenever you are deciding where code belongs: introducing a new namespace, splitting an overgrown one, deciding what's public vs private at a directory boundary, or evaluating whether a layout reveals or obscures the system's purpose.
Principles for organizing code: directory layout, domain boundaries,
and public surfaces. The principles are domain-agnostic — apply them
to a healthcare system, an inventory tracker, a CLI tool, or any
other project.
Examples below use a fictional spaceship to ground each principle in
concrete code. The metaphor is incidental; substitute your own domain.
Only the names change.
Screaming Architecture
The directory layout should reveal what the system does, not what
frameworks it uses. Uncle Bob's principle (2011): a good architecture
"screams" its domain. Your top-level structure should convey "this is
a healthcare system" or "this is an inventory system" — not "this is a
Rails app" or "this is a Clojure CLI."
Imagine opening a spaceship codebase. Which layout tells you more?
ship/ ship/
bridge/ ← "the UI" life-support/ ← "breathing"
network/ ← "the pipe" propulsion/ ← "going places"
persistence/ ← "the DB" navigation/ ← "where are we"
controllers/ ← "wiring" weapons/ ← "pew pew"
models/ ← "data shapes" cargo/ ← "what we carry"
crew/ ← "who's aboard"
Left side: "I could be any app. What do I do?" Right side: "Oh, this
is a ship."
The lesson
Frameworks are implementation details. A bridge console, a
subspace radio, a tactical display — these are ways the ship is
delivered to its operator. The domain (life support, propulsion,
navigation) is what the ship does. Top-level structure should
privilege domain.
You can never eliminate the framework. But you can keep pushing it
toward the edges, where it belongs, and extracting the business logic
into pure, reusable components that don't know whether they're invoked
from a bridge button, a voice command, or a scripted autopilot.
Concrete example
Suppose set-oxygen-level has a bridge-console UI. A framework-first
layout looks like:
ship/
bridge/life-support.clj ← bridge command for life-support
life-support/controller.clj ← life-support logic
life-support/scrubber.clj ← O2 scrubber
The bridge command is a bridge concern with a life-support
payload. Problems: life-support code is split across two branches; top
level has "bridge" as a high-signal entry competing with real domain
names.
A domain-first layout:
ship/
life-support/
controller.clj
scrubber.clj
bridge/command.clj ← same file, now inside the domain
Life-support owns all its pieces. Top-level "bridge" shrinks by one
entry. The structure leans closer to "this is a ship of these
domains."
What still doesn't scream
ship.life-support.bridge.command still has "bridge" in its path. The
framework is visible — just one level deeper. A truer move exposes
life-support as a façade (see below) and lets the bridge adapter be a
thin translator that isn't architecturally prominent.
This is the pattern: each move pushes the framework a little further
from the center. Perfection isn't the goal; direction is.
Seams — domain-as-interface
Directories shouldn't just group files. Each top-level domain should
expose a stable public surface — a set of functions that describe
what the domain can do — while keeping implementation private and
swappable.
Ports & Adapters (Hexagonal Architecture) at the directory level:
ship.life-support is the port (public): set-oxygen,
set-pressure, status.
ship.life-support.scrubber is the adapter (private): chemical
scrubber today, bio-recycler tomorrow.
- Callers say
(life-support/set-oxygen 21) — not
(scrubber/set-o2 0.21).
Why this matters
- Decoupling. Callers don't know whether the scrubber is chemical
or biological. Swap the adapter without touching any caller.
- Testability. Swap a stub adapter for tests without redefining
every seam.
- Deferrable decisions. The "which storage?" or "which
transport?" question doesn't have to be answered on day one,
because the domain's shape doesn't depend on it.
- Discoverability. Newcomers find the public API in one place,
not by grepping consumers.
Starting state is rarely pure
Most projects begin with direct coupling — callers reach into
implementation namespaces (ship.life-support.scrubber) because
that's the easiest thing that works. Extracting façades is incremental
work: pick one domain at a time, let the system tell you when the next
seam is worth making explicit. You don't earn it all on day one, and
you don't have to.
Implicit / explicit API pairs
When a namespace exposes behavior through a dynamic var or implicit
context, provide two variants of each public function:
name — uses the implicit default (reads the dynamic var)
name- — takes the implementation as the first argument
(defn slurp- [fs path] ...) ; explicit
(defn slurp [path] (slurp- *env/fs* path)) ; implicit
Call sites choose whichever they need:
(slurp "/log/status.txt") ; normal case — uses default fs
(slurp- staging-fs "/log/status.txt") ; dry-run against an alt fs
(copy-tree! src-fs dst-fs "/quarters") ; requires two fs instances
Why not overload by arity?
Tempting to write (defn slurp ([path] ...) ([fs path] ...)).
Problem: call sites read ambiguously — (slurp a b) could be a
two-arg read against the default fs or an explicit-fs read. The
name- suffix makes intent visible at every call.
When the pattern earns its place
Apply when:
- The namespace wraps a protocol/interface that has multiple
implementations (real, in-memory, mock)
- Production code sometimes needs to reach around the default
(dry-run, staging, multi-tenant, test isolation)
- Explicitness beats convenience at the boundary
Skip when:
- The namespace has one implementation and no injection story
- Dynamic var indirection isn't yet worth the ceremony
Inspiration
Adapted from c3kit.bucket.api
— ffind vs ffind-, count vs count-, tx vs tx-. Same shape
throughout the library; predictable naming at every call site.
How this doc grows
Add a principle when the codebase earns it. Every principle comes
with:
- The idea, stated once in plain language
- Why it matters (the tradeoff the principle resolves)
- At least one concrete example from the code
- What it looks like when you've moved closer, even if not perfect
Don't add aspirational principles the code doesn't yet demonstrate.
Keep this file honest to what's actually in the tree.