Backend
Splitting Until It Fits
Anton Brilliantov Dev.to (EN Zone)
3 views
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.
Read original: https://dev.to/anton_brilliantov/splitting-until-it-fits-3100
← Previous
Your AI Coding Agent Will Run Whatever a Stranger's Repo Tells It To
Next →
You write one kind sentence. I donate a dime. A blockchain makes sure I do.
Related
Legacy System Modernization Without the Rewrite
Backend
0
DEV Community
Running ML models in the browser with ONNX Runtime Web: a practical guide
Backend
0
DEV Community
RV32I vs x86_32: How Branching Works Across Architectural Lines
Backend
0
DEV Community
Ratatui passo dopo passo - Introduzione
Backend
2
Dev.to (EN Zone)
Comments0
No comments yet — be the first