Design from the Leaves
Engineers copy patterns. Agents copy patterns. Whatever shape your leaf nodes take is your codebase's design language. Make it worth copying.
Most codebases have a surface where new features get added: command handlers, query handlers, CRUD endpoints, repository methods, validators. Small, similar units, replicated every time someone builds something. Call them leaves.
When an engineer adds another one, they open an existing leaf, copy it, rename a few things, and edit. This is not laziness. It is correct. The existing leaf is the most reliable spec of how a leaf should look here.
Agents do the same. Give one a vague task and the first thing it does is grep for similar examples, read one or two, use them as a template. With a smaller spotlight than a human, an agent leans on this even harder. Whatever it finds at the leaves is what it will produce.
So here is the principle: design your codebase from the leaves up.
The leaves are your design language
Look at a leaf node and ask: if every new feature produces another one of these, am I happy with the shape it is in?
Most leaf nodes I see in the wild are the wrong shape. Type annotations duplicated from the interface they implement. A constructor wiring up dependencies through a base class. A try/catch and a logger call identical to every other leaf. The actual thing this leaf does, the one or two interesting lines, is buried in ceremony.
The next engineer copies this. The agent copies this. The cycle continues.
The discipline is the opposite. A leaf should read as if it has no boilerplate. The smallest possible declaration of what makes this leaf different from every other leaf. Nothing more.
If a line of code appears in every command handler, it does not belong in any command handler. It belongs in the layer above.
Push complexity up the tree
The complexity does not vanish. It moves.
If the leaf is a single declarative call, something is doing the wiring. If there are no type annotations at the leaf, something is inferring them. If error handling, logging, transactions, and event publishing are not in the leaf, something is wrapping them around it.
That something is the layer above. It is heavier. It uses generics. It has a non-trivial type signature. It might be hard to read on first glance.
That is fine, because:
- It gets written once.
- It can be unit tested with the kind of rigour that pays off when something is used a hundred times.
- It rarely changes.
- Once it is right, agents and humans use it without reading it.
The trade: concentrate cognitive load in a few places that are read rarely, in exchange for shrinking it in every place that is read often. The framework gets harder to write. Every feature gets easier.
Push the types up too
The same principle applies to typing. This is where TypeScript earns its keep.
I work in strict mode. Every value is typed. But the leaves do not show it. No explicit type parameters, no : ReturnType<...> annotations, no manual as const ceremonies. The types are there in the sense that the compiler will reject any mistake. They are absent in the sense that you cannot see them in the source.
This is what a well-designed factory with inference gives you. You declare the shape once at the framework level, with generics connecting inputs and outputs. At the leaf, you write plain values. The compiler threads the types through.
A command handler looks like this:
export const createUser = defineCommandHandler({
parse: CreateUser.parse,
load: async command => ({
emailTaken: await users.existsByEmail(command.email),
}),
check: (state, command) => {
if (state.emailTaken) {
throw new EmailTaken(command.email)
}
},
execute: async (state, command) => {
await users.insert(command)
},
})
Four fields. No type annotations. Reads top to bottom.
Behind the scenes, defineCommandHandler is roughly:
type CommandHandler<C, S> = {
parse: (input: unknown) => C
load: (command: C) => Promise<S>
check: (state: S, command: C) => void
execute: (state: S, command: C) => Promise<void>
}
function defineCommandHandler<C, S>(handler: CommandHandler<C, S>) {
return handler
}
C is inferred from parse’s return type. S is inferred from what load returns. By the time check and execute run, the compiler already knows the shape of state and command. Full type safety, zero type ceremony.
A reader, human or agent, has to learn one thing: a command handler is a defineCommandHandler call with four fields. The state shape and command shape are inferred. The agent does not need the framework in its context to write a correct new handler. It needs one existing handler. The pattern is the spec.
Sketch the leaf first
I try to design the leaf shape before anything else exists. Before the framework. Before the wiring. Before any types are connected up.
When the shape has not been established yet, I write the sketch myself, or describe it precisely to an agent. Four fields. These names. This is what every new handler should look like when there are forty of them. The leaf is the artefact I am most opinionated about, because it is the one that gets repeated.
Then I hand the rest off. “Make defineCommandHandler such that all of these types infer through to the leaf.” The agent writes the generics. I do not particularly care what shape the framework code takes (conditional types, helper interfaces, mapped types), as long as the leaf reads the way I sketched it and the compiler refuses incorrect inputs.
As the agent iterates on the wiring, I question every line that ends up at the leaf. Why is that import there? Why does that field need an annotation? Why is state being passed explicitly when the framework should already know? Anything that looks like ceremony at the leaf gets pushed back up until the leaf is the bare declaration I sketched.
I am less precious about the framework’s internals. Not because I will not look at them again, but because they are cheap to change. The framework lives in one place. The leaves live in fifty. Refactoring a generic helper is a one-file change. Refactoring the leaf shape is a fifty-file change. The leaf is where care has to be invested up front, because it is the most expensive thing to revise later.
This inverts the usual review focus. Most reviews zoom in on the parts that look complicated. With this pattern, the complicated parts are the parts hardly anyone touches. The simple parts get multiplied. That is where the attention goes.
What the agent does next
Ask an agent to add a deleteUser handler.
If the leaf is full of ceremony, the agent reads the ceremony, understands each piece, and reproduces it. The boilerplate is load-bearing. Get any of it wrong and tests fail. The agent burns spotlight on plumbing.
If the leaf is the declarative form above, the agent reads createUser, recognises the four-field shape, and writes:
export const deleteUser = defineCommandHandler({
parse: DeleteUser.parse,
load: async command => ({
user: await users.find(command.id),
}),
check: (state, command) => {
if (!state.user) {
throw new UserNotFound(command.id)
}
},
execute: async (state, command) => {
await users.delete(command.id)
},
})
It does not need to understand defineCommandHandler. It does not need to understand transaction boundaries or the dispatcher. It needs to know what a delete does and copy a shape it has already seen. That is exactly what agents are best at.
Every new feature that fits the leaf-node pattern is a low-context, low-risk task. The framework absorbs the weight.
Layers all the way down
This is a multi layered story.
In a real application the same lift repeats at multiple scales. First, leaves into a generic defineCommandHandler. Then you notice every user-domain handler wires up the user repository the same way, and you lift again into a defineUserCommandHandler that pre-wires it. Then the user-domain framework itself shares transactions and event publishing with every other domain framework, and that lifts again.
At every layer, the question is the same: what ceremony is being repeated below me? Push it up. The result is a stack where each layer reads as a declaration in the language of the layer above. The deeper you go, the heavier the code is allowed to be. The closer to the surface, the closer it reads to pure intent.
Most feature work happens at the surface, and the surface is where complexity has been most aggressively starved.
The discipline
Designing from the leaves is harder than letting them grow organically. It means:
- Refusing to ship the first leaf until it is something you would be happy to see copied a hundred times.
- Treating every line that repeats across leaves as a defect, to be lifted into the layer above.
- Investing in generics and inference at the framework level so the leaf stays free of annotations.
- Writing the heavy framework code carefully and testing it well, because everything above it inherits its correctness.
More upfront work for fewer files. It pays back every time a new leaf is added, which in a healthy codebase is most of the work.
In the AI era the multiplier is even larger. The leaf is the unit of work an agent produces most often. The leaf is what it copies when given a vague instruction. The leaf is the part of your codebase that ends up replicated hundreds of times. Adding a new leaf should not demand reasoning about transactions, error handling, or how the framework wires itself together. None of that lives at the leaf. The agent matches a shape it has seen before, and the framework absorbs the rest. Whatever exists at the leaves is what the codebase keeps producing.
Make them worth copying.
Part of an ongoing series on the patterns that make AI-assisted development actually work. See also It’s (Still) All About Boundaries for the broader principle: keep the cognitive surface area small at every zoom level. Designing from the leaves is what that looks like at the smallest zoom.