The application is ten years old and nobody had read it. Everybody had read the parts they worked on, which is a different thing — and the consequence was decisions being made twice, because the person making them did not know what already existed three directories away.
The symptom
three examples from one quarter:
a retry helper written in July, against one written
in 2021 that does the same thing with better
backoff
a proposal to add a feature flag system, in a
codebase that has had one since 2022
two hours spent working out how currency conversion
happens, in a class with a docblock explaining it
none of these is anybody's fault. the codebase is
88,000 lines and nobody has an index of it.Why it happens
Reading is unbillable and writing is not, so nobody schedules it. Knowledge of a codebase accumulates as a byproduct of changing it, which produces deep knowledge of the parts that change and none of the parts that do not — and the parts that do not change are where the surprises are.
The fix
The method
one week, one person, no changes permitted
by layer rather than by feature: domain first,
then application, then infrastructure, then HTTP
a notebook, in the repository, committed daily
a question list, separate, for things that could
not be answered by reading
the no-changes rule is the one that makes it work.
every time I wanted to fix something I wrote it down
instead, and the list was 41 items by Friday.Not changing anything is what keeps this to a week — a reading pass that becomes a refactoring pass takes a month and produces neither. The forty-one item list is the output, and about a third of it turned out to be wrong once the rest of the codebase had been read.
What the analyser found that reading did not
$ vendor/bin/phpstan analyse --level=10 --error-format=table
| ./bin/group-by-rule
142 a third-party interface returning an unshaped array
38 mixed crossing our own boundary
4 genuinely dynamic dispatch
$ ./bin/dead-code --coverage=coverage.xml --routes --grep
4,102 lines unreachable by any of the three checksand what reading found that the analyser did not:
two subsystems solving the same problem differently
a class named for what it did in 2018
a comment describing behaviour that was changed in
2022, still there, still wrong
a configuration value read in three places with
three different defaults
none of these are errors. all of them are why the
next change takes longer than it should.The tools find what is provably wrong and reading finds what is confusing, and confusion is the thing that costs time. The configuration value with three defaults is the clearest example — every one of them is valid code and the combination means nobody can say what the setting does.
Nine subsystems, two of which nobody could explain
orders, billing, catalogue, fulfilment, identity,
reporting, integrations, notifications
and: src/Sync/
1,880 lines, no tests, last substantive change 2021,
called by one scheduled job.
what it does: reconciles our order state against a
supplier's, and writes a discrepancy report nobody
reads.
what it should do: unclear. the person who wrote it
left in 2022 and the supplier's API has changed twice
since.A subsystem nobody can explain, with no tests, running nightly, writing a report nobody reads is not obviously safe to delete — it might be the only thing catching a class of error. Establishing that took a week of its own and the answer was that the discrepancies it reports had been zero for eighteen months because the query it runs has been broken since an API change.
The three patterns used inconsistently
fetching a related entity
a repository method 31 places
a lazy relation on the domain object 18
a direct query in the service 9
reporting a failure
a typed exception 88
a Result object 12
a nullable return 41
configuration
a constructor argument 60
config() at the point of use 22
a constant 14Three patterns for each of three problems, all of them defensible, none of them documented as the choice. Writing down which one wins — repository, typed exception, constructor argument — took ten minutes and is the single highest-value output of the week, because every future change now has a default.
The document
# The application, as it is
## Subsystems
Nine, listed with what each owns and what it depends on.
## Conventions that are actually followed
Where there are three patterns, which one wins and why.
## Things that surprised me
Twelve entries. The most useful section.
## Things nobody could explain
Two, with what we established and what remains open.
## Open questions
Eleven, each with who might know.
eighteen pages. read by:
the two other engineers, within a fortnight
a contractor onboarding in November, who said it
saved a week
nobody since
and it is already stale in three places, four months
later.The “things that surprised me” section is the one people actually read, which suggests the value is in the anomalies rather than in the structure — the structure is discoverable from the code and the anomalies are not. Being stale within four months is the expected outcome and does not make it worthless; it makes it a snapshot rather than documentation.
Verifying it worked
# the measurable outcome, such as it is
$ git log --oneline --since=2025-10 --grep='duplicate of' | wc -l
0 # was 3 in the previous quarter
$ ./bin/dead-code-removed
4,102 lines deleted across 6 pull requests
src/Sync/ removed entirely: 1,880 lines
# and the decision that took ten minutes
# "should we add a feature flag system?"
# → we have one. src/Support/Features. documented
# on page 7.A decision made in ten minutes that would have taken a day is the only concrete return anybody can point at, and it is a sample of one. The four thousand deleted lines are a more solid outcome and were a side effect rather than the goal.
What this costs
A week of one person’s time, and a document that is stale within a year. Neither of those is recoverable and the week is the honest cost — it produced no feature, no fix that anybody had asked for, and a document with three readers.
It also cannot be repeated usefully at the same interval. A second reading pass in twelve months would cover mostly the same ground, and the parts that changed are the parts people have been working on and already know. The right cadence is probably three years or a new person, and neither of those is a schedule anybody will keep.