Forty-four articles, all on the same subject: systems that already exist. PHP, Go, AWS, and in between the things that are not technology and decide the outcome anyway.
This text is not a table of contents, that sits at the bottom. It is an attempt to write down what ran through all of the articles without having been planned. Five sentences are left.
The most expensive part is never the code
It started with the question about price and came back in nearly every article afterwards: two systems of the same size differ by a factor of five in effort, and the difference never sits in the amount of code. It sits in what it costs to demonstrate that a change is correct.
More hangs off that than it first appears. Without that demonstration a tool like Rector is limited to the rules the language itself enforces. Without it a coding agent multiplies not the speed but the risk. Without it every revocation of a permission is a gamble and every shutdown of a feature a guess.
So building a net around the place being touched next is not preparatory work you can skip when things are urgent. It is the part that makes everything else cheaper, and the only one that cannot be negotiated away.
An appointment beats an opinion
The second sentence is not about technology, and it looked quite different in three separate articles.
"We should move to a newer version at some point" is an opinion and competes with features. "Maintenance ends on 31 December, the move needs three months of lead time, we start at the end of September" is an appointment and competes with nothing. Same thing, two different conversations.
Cleaning up permissions ran on the same mechanics: an exception with a justification grows back, an exception with a date does not. And with a feature nobody needs any more, an announced shutdown on a date is something else entirely from the same shutdown without the announcement, although technically nothing separates them.
So anyone trying to get a modernization through spends less time justifying it and more time dating it.
What nobody checks does not hold
The sentence that appeared most often across the articles, in contexts that have nothing to do with each other.
The tenant separation that is a claim after three months because no test pins it down. The four-statement rule for new caches, forgotten in two weeks without a place that enforces it. The handover that consists of a presentation without the one test. The brief to an agent whose boundary moves along with the work as soon as the agent may change the tests.
The conclusion is uncomfortable for documents and comfortable for developers: a rule is worth as much as the mechanism that checks it. Without one it is a statement of intent, and statements of intent hold until the next tight week.
Cutting smaller is almost always the answer
In the articles about attempts that failed, the cause was remarkably often the same, and it was never the tool.
Rector across the whole project instead of one directory. Observability everywhere instead of along one path. The replacement in one step instead of a series of blocks with a result each. The permissions all at once instead of starting with what demonstrably was never used.
The pattern behind it is always the same: the amount per step decides whether a fault can be attributed. Keep it small and every deviation is explainable. Make it large and every deviation is an investigation, and after two days everything gets thrown away.
What comes out of that is not slowness but the opposite. Twelve weeks in six blocks with a result each deliver something usable earlier than twelve weeks in one piece, and they survive an interruption.
Taking away is modernization
The fifth sentence was the one that surprised me most while writing, because it showed up in articles that were not supposed to be about it.
The cache with a two percent hit rate gets removed, not documented. Two services always deployed together are one and belong merged. The feature nobody has called in four years costs in every upgrade and every migration. The old path after a replacement belongs in the same task as the switch-over, not in a follow-up ticket.
Every one of those decisions makes the system smaller, and none of them can be presented as progress. Which is exactly why they do not happen on their own: the risk is distributed one-sidedly, and whoever takes something away has their name on the decision. It takes a procedure that can carry that, and it consists of measuring, announcing, switching off, waiting.
Two assessments that shifted while writing
Writing forty-four articles on one subject shifts a few assessments, and two of them belong here.
The organizational topics are not the garnish. I had planned the series technically and put the articles about ownership, handover, dates and scope in between as light relief. While writing it turned out that the technical articles regularly ended at an organizational question: who decides, who checks, who gets the alert. The code was rarely the bottleneck.
The difference between tools and procedures grew. Writing about Rector and about coding agents on the same subject made it clear that the interesting question is never "which tool" but "what checks the result". A deterministic tool needs a sample per rule. A non-deterministic one needs a net. The tools are interchangeable, the question is not.
Four entry points if you have a concrete problem
The series is not meant to be read through. Arriving here with a particular situation, you are best off starting at one of these four places.
The system needs rebuilding and nobody knows what it costs. Then it is about the five drivers of effort, the cut into blocks, and the second number missing from the conversation with management. The way in is What a modernization costs.
It runs, but every change is expensive. Then the order is: establish what is actually used, put a net around the place coming up next, and only then change something. The way in is Testing legacy code when there are no tests.
Operations are the problem, not the code. Then it is about delivery, observability, access and boundaries, and the cheapest single step is almost always deliverability. The way in is Blue/green and canary without Kubernetes.
Somebody is leaving, or somebody is arriving. Then it is about knowledge, access, and the one test that says whether a handover held. The way in is The handover day.
If you are facing a situation like that and are not sure where to start: write to me. Two sentences about the system are enough for an assessment of which of the four orders applies to you.
Every article in the series
The series runs in four alternating strands: existing systems and rebuilds, AWS and operations, backend and boundaries, and the topics that are not technology.
- Rector in a legacy project12 September 2026
- The handover day12 September 2026
- Blue/green and canary without Kubernetes12 September 2026
- Retrofitting multi-tenancy12 September 2026
- What a modernization costs12 September 2026
- Removing features nobody uses any more12 September 2026
- An end-of-life calendar for the whole stack12 September 2026
- The distributed monolith12 September 2026
- Cleaning up IAM in a grown AWS account12 September 2026
- Test data from production12 September 2026
- A coding agent in a legacy codebase12 September 2026
- Introducing OpenTelemetry in PHP and Go12 September 2026
- The cache as a debt12 September 2026
- Modernizing a server-rendered front end without an SPA12 September 2026
- Twelve weeks of modernization: a realistic plan12 September 2026
- Backups nobody has ever restored12 September 2026
- Configuration and secrets for Go services12 September 2026
- Ten holes that sit in every old PHP system12 September 2026
- Putting an API in front of the monolith12 September 2026
- MySQL 5.7 to 8.0: what breaks in the move12 September 2026
- Introducing PHPStan into legacy code step by step12 September 2026
- Modernizing with an external freelancer12 September 2026
- A PHP monolith on ECS Fargate12 September 2026
- From cron jobs to queues12 September 2026
- Maintenance mode: running a legacy system safely12 September 2026
- Learning Go as a PHP developer: five wrong assumptions12 September 2026
- Shopware 5 plugins in the migration to 612 September 2026
- Dual write and backfill: where migrations really tear12 September 2026
- Knowledge in one person is an architecture problem12 September 2026
- Terraform drift: when console and code diverge12 September 2026
- Laravel upgrades: where the pitfalls differ12 September 2026
- Building an anti-corruption layer between Go and PHP12 September 2026
- Three measurements before you refactor a single line12 September 2026
- Composer as archaeology12 September 2026
- Sessions, uploads, state: three chains to one server12 September 2026
- Feature flags in a monolith without framework support12 September 2026
- Lift and shift or replatforming12 September 2026
- Which service to pull out of the monolith first12 September 2026
- The business case for a modernization12 September 2026
- Working through Symfony deprecations without drowning in the log12 September 2026
- The real legacy is your database schema12 September 2026
- When deployment still runs over FTP12 September 2026
- Testing legacy code when there are no tests12 September 2026
- Technical due diligence: assessing a legacy system in five days12 September 2026
The series ends here, the subject does not. How I work on grown systems is described on its own page, and anyone who does not want to miss the next articles subscribes to the feed.

