An anonymous engineer at coldtake.dev on why LLMs work in greenfield projects and fall apart in legacy ones — and how he restructures the codebase so agents stop guessing. The failure has a specific shape: ask for a “job offer status” field in a four-year-old system and the model invents a fourth spelling of a concept that already exists three times, because the codebase itself never decided which one was real. “The model is not what needs upgrading. The code is not ready.”
The strategic/tactical split
- Borrowing Ousterhout’s terms and bending them: strategic work is deciding (reading the system, working out what must change and why); tactical work is carrying the decision into files.
- Deciding stayed expensive; typing collapsed. An LLM does the mechanical half of a cleanup at a cost that no longer resembles 2020 — so the split is about authorship: he decides, agents implement.
- His loop: he writes GitHub issues per repo; an AI system addresses them via skills (markdown procedures — “address an issue” runs the same way every time) and sub-agents (separate sessions with narrow jobs: implement, security review, spec review); PRs come back for his review.
DDD as the foundation
- Every repo carries a
.workflow.jsonmanifest whose domain block is the only registration needed: the project, its bounded contexts, where each context’s glossary lives, its subdomain type, and every edge to a neighboring context. - Each context has a
CONTEXT.md— the living glossary of precise meanings plus deliberately rejected synonyms. The context map (every context and edge in the portfolio) is derived: a generator walks the repos, unions the domain blocks, and emits a disposableCONTEXT-MAP.md. - Edges are declared twice, once from each side — and the duplication is the point. The generator cross-checks pairs against a pairing table (supplier declares
published-language, consumer declaresconformistoranticorruption-layer) and files disagreements asDDDissues with fingerprints, so re-runs update the same issue and fixed findings auto-close.
The worked example
- job-offer-box: a Rust backend (
hyperion) + a web frontend. The backend owns the product language — Job Offer, Profile, Resume — under the rule that whoever holds the durable state owns the term. The frontend owns only screen vocabulary (View Model, Filter State) and marks everything else[published], arriving verbatim as generated TypeScript from the backend’s OpenAPI document. - One edge is honestly labeled
unclassified: the web repo is conformist on write (accepts the backend’s shape) and an anticorruption layer on read (translates to its own shape) — “two patterns hold at once, so neither name alone is true.”
Where it goes next
- The context map settles the shape; the inside of any context is still ordinary code. Next step: migrate one context at a time to a real domain model (value objects, aggregates, domain services) so the codebase answers the questions the model was guessing at.