One File, One Job: Design Patterns for LLM-Native Codebases

Four structural patterns that make your codebase safe for AI modification — not through documentation, but through architecture.

APRIL 2026  ·  7 MIN READ

In Part 1, I laid out the problem: LLMs are strangers to your codebase. Every session. The fix isn't better memory — it's architecture that makes forgetting safe.

Here are four patterns that do that. They're not theoretical. They're running in a production system I've operated for over a year — 75+ tools across three machines, modified by AI sessions daily.

Pattern 1: Single-responsibility files

Thirty files that each do one thing are better than five files with complex branching.

This sounds like basic software engineering — and it is. But the calculus changes when an LLM is your primary modifier. A human developer can hold "this file does three things, and here's how they interact" in their head while editing. An LLM can't. It sees the file, picks up on the local pattern, and makes changes that are locally correct and globally destructive.

The practical rule: if you're adding a code path to an existing file, ask whether it should be a new file instead. The answer is almost always yes.

A task tracker and a commitment tracker sound similar enough to live in the same file. They both have add/list/complete commands. But a task has an owner, a due date, and a status. A commitment has a counterparty, a deadline, and a verification step. The moment you put both in one file, every AI session that touches task logic might accidentally affect commitment logic — because the session doesn't know the distinction matters.

Separate files. Separate concerns. The overhead of more files is nothing compared to the cost of a session that doesn't understand the file it's editing.

What this looks like in practice

In the system I run, every tool is a single shell script or a small group of files in its own directory. task_ctl.sh does tasks. commit_ctl.sh does commitments. crm_sync.sh does CRM synchronization. They share nothing except a common data directory convention.

When an AI session needs to add a new command to the task tracker, it opens one file. There are no hidden interactions. The session can't accidentally break commitment tracking because commitment tracking doesn't exist in that file.

75 tools sounds like a lot of files. It is. It's also 75 tools that can each be modified independently without understanding the other 74.

Pattern 2: Config-driven behavior

New behavior should mean a new config file, not a new code path.

This is where most AI-modified codebases go wrong. You build a tool that handles one case. Then you need a variant. The AI's instinct — and honestly, most developers' instinct — is to add an if/else branch. "If it's this variant, do this slightly different thing."

That works for the second variant. By the fifth variant, you have a function with nested conditionals that no session can fully understand.

The alternative: externalize the variation into config. The tool reads a config file that says "here are the variants, here's what's different about each one." Adding a new variant means creating a new config entry, not modifying execution logic.

What this looks like in practice

I run a call intelligence pipeline that extracts structured data from meeting transcripts. Different meetings need different extraction — a sales call needs pipeline movement and next steps, a team check-in needs metrics and blockers.

The naive implementation: one extraction function with a big switch statement for each meeting type.

The actual implementation: profile configs. Each profile is a JSON file that declares what to extract, what fields matter, and where to route the results. Adding a new meeting type means dropping a new JSON file in the profiles directory. The extraction engine doesn't change.

profiles/
  sales_call.json      # extract: pipeline, actions, next_steps
  team_checkin.json    # extract: metrics, blockers, decisions
  investor_update.json # extract: kpis, asks, commitments

An AI session that needs to add investor meeting support creates one file. It doesn't touch the extraction engine. It can't break sales call processing because it never opens that code.

Pattern 3: Dispatcher layers

The moment a second variant appears, build a router. Never let a tool silently become two tools.

This is the pattern that would have saved the LinkedIn automator I described in Part 1. Instead of each session adding another branch to a growing handler, there should have been a dispatcher from the moment the second DOM variant appeared.

A dispatcher does two things: it detects which variant you're looking at, and it routes to the correct handler. The detection logic lives in one place. Each handler lives in its own file. Adding a new variant means writing a new handler and registering it — not modifying existing handlers.

This is the strategy pattern, and experienced developers know it. What's different in AI-assisted development is the timing. You need to build the dispatcher earlier than you think — at variant two, not variant five. Because the pace of AI development means you'll have five variants before your refactoring instincts fire.

The rule of two

If a tool handles one case, it's a tool. If it handles two cases, it needs a dispatcher. Not eventually. Now.

This feels premature. It's not. The cost of building a dispatcher for two variants is small. The cost of untangling five variants from a monolithic handler — when neither you nor the AI remembers why each branch exists — is enormous. I've paid that cost. More than once.

Pattern 4: Contracts registries

Declare what each tool owns, and make every session read the declarations before modifying anything.

This is the least obvious pattern and possibly the most important one. A contracts registry is a file that says, for each tool in your system:

  • OWNS: What data this tool is authoritative for. Don't write to these paths directly.
  • INTERFACE: How to interact with this tool. Use these commands only.
  • PRODUCES: What data other tools can read from this tool.
  • CONSUMES: What data this tool reads from other tools.
## crm_sync

OWNS:
  - /data/crm/contacts.json
  - /data/crm/sync_log.ndjson

INTERFACE:
  crm_sync.sh sync              # Pull latest from external CRM
  crm_sync.sh add-touch <id>    # Log a contact touch
  crm_sync.sh lookup <query>    # Search contacts

PRODUCES:
  - contacts.json (read by: call_intel, cascade, sms_handler)

CONSUMES:
  - call digests (from: call_intel)

When an AI session opens your codebase and needs to log a CRM touch, it reads the contracts registry and sees: "use crm_sync.sh add-touch, don't write to contacts.json directly." Without this, the session might reasonably decide to append directly to the JSON file — and break the sync cadence, create duplicates, or corrupt the format.

The contracts registry is a forcing function. It makes the right thing easy to find and the wrong thing require deliberately ignoring documented boundaries.

Why this works better than comments

Comments are local. They tell you about the line you're looking at. A contracts registry is global — it tells you about the system. When an AI session is about to build a new integration, it doesn't need to read every tool's source code. It reads one file and knows: here's what exists, here's how to talk to it, here's what not to touch.

I maintain a contracts registry for every tool that stores state or exposes an interface. The rule is simple: if you're building something new and it overlaps with a registered tool, you must use that tool's public interface. No reaching into internals.

These patterns compound

Any one of these patterns helps. All four together create something qualitatively different — a codebase where the structure itself teaches each new session how to work correctly.

Single-responsibility files mean changes are isolated. Config-driven behavior means new variants don't require code changes. Dispatcher layers prevent accidental complexity accumulation. Contracts registries prevent integration boundary violations.

An AI session opening this codebase doesn't need to understand the whole system. It opens one file, reads the relevant contract, adds a config entry or a new handler, and leaves. The architecture constrains it toward correct behavior — not through memory or documentation, but through structure.

In Part 3, I'll go deeper on the interchange formats — how tools actually pass data to each other, why append-only event logs solve half your integration problems, and what manifest-driven dispatch looks like in practice.