One task, one concern, one or two files, one test, one command. The rule I use to know when to stop splitting isn't a line count - it's whether anything still has to be looked up. 👋 I'm Anton - a software engineer working mostly in PHP/Symfony and Go, currently carving a live PHP monolith into Go services. Part 1 of this series was about what an executor must know and must not know. This part is about the other half of the same problem: how small the unit of work has to get before that's even possible. Notes: github.com/brilliant-almazov. As before: these are my habits on one codebase, not advice for yours. The unit I split work in four steps, and the last one is the only one with a definition: task "the service should take its runtime from the platform" └── stage "stop hand-rolling what the platform already gives us" └── sub-stage "message consumption" └── iteration one concern: 1–2 files + their test An iteration is one concern. In practice that is one code file, sometimes two of the same shape, plus the test that covers it. If it doesn't fit in one pass, it becomes two iterations. It does not become one long iteration. Real sizes from the last few stages: a stage of 13 iterations (moving off hand-written runtime), a stage of 12 (a skeleton generator), and a stage of 40 - that one converted forty-four copies of four read shapes onto a single generic core, one file per iteration. Forty iterations sounds absurd until you look at what each one is: change one file, move its test, run one package's tests. Fifteen to thirty-five minutes each. 13 iterations 12 iterations 40 iterations ──────────── ──────────── ──────────── moving off a skeleton 44 copies of four hand-written generator read shapes onto runtime one generic core (one file each) The tell Here's the only reliable signal I've found that an iteration is still too big: The executor had to look something up. Not "it took long". Not "the diff was big". If anything had to be found - the type to embed, the name of an error, which package the helper lives in, how the neighbour did it - then the iteration was carrying an unstated dependency, and the split was wrong. That reframes a whole class of complaints. When an iteration comes back with a clarifying question, the question is not the executor's failure. It's a defect in how I cut the work. So the fix is never "answer it" - the fix is add the fact and cut smaller. NOT the tell the tell ──────────────────────────── ───────────────────────────────── it took long anything had to be found the diff was large · the type to embed · the name of an error · which package a helper is in · how the neighbour did it │ ▼ the cut was wrong, not the executor What that rules out Iterations that sound reasonable and are not: "Walk the path and fix every step where the value is lost." This is a search with a fix attached. Do the walk first, then write one iteration per broken step. "Find where it breaks." Investigation. Not an iteration. "Figure out how it's done here and follow it." Reconnaissance delegated. "By analogy with the neighbouring domain." Copying an unnamed template. Each of those has the same shape: it makes the executor discover something before it can act. The discovery is the expensive part, and it happens on every executor, every time. Order The order of iterations follows data dependency: whatever produces something comes before whatever consumes it. Tables and schema go in the first iteration of a chain; the core goes before its users. Beyond that the order is free. Two iterations that touch neither the same files nor each other's output can run at the same time. That's the whole reason for splitting by concern rather than by "feature": concerns don't overlap in files, so they parallelise without merge pain. The dependency table for a 13-iteration stage looked like this - a plain list, not a graph anyone has to reconstruct: # Iteration Depends on 01 register the consumer in the daemon 05, 07 02 message headers on publish — 03 typed subscription 02 04 delivery options and terminal errors 03 05 dedup on the shared cache 04 06 entity cache on the shared core — 09 bulk insert on the shared preparer — Everything with a dash starts immediately. Everything else waits for exactly one named thing. Acceptance, per iteration An iteration that can't be checked isn't finished, it's abandoned. So each one carries a command that runs with no substitution and covers only its package: go test ./internal/repository/order/... -race -count=1 Not the whole suite. The full pass - formatting, vet, linter, everything with -race, structure checks - runs once, at the end of the stage, by me. Running it per iteration is the single most expensive habit I've had to break, and it feels responsible the entire time you're doing it. What it costs Forty iterations means forty written tasks. On a stage where the code itself is two days, the writing is most of a day, and it happens before anything runs. That trade only pays when the work is repetitive and the conventions are rigid - the same mechanical change across many files, in a codebase where "the right shape" is already decided. On exploratory work it's the wrong tool: you can't specify what you haven't found yet. The other cost is discipline. A stage of forty iterations is forty chances to say "this one's small, I'll just do it myself while I'm here" - and each time you do, the stage stops being resumable, because the state is now partly in your head. The one conclusion Split until nothing has to be found. If the executor is looking for something, the iteration is still too big - and that's a fact about the task I wrote, not about who executed it. Working with agents - Part 2. Next: what a task looks like when nothing is left to interpret - and the list of phrases I've banned from my own writing because every one of them is a hole.