A mature codebase rarely announces that it needs refactoring. It makes a small change feel oddly expensive. A new route requires touching three modules whose names no longer describe their responsibilities. A dependency update turns into archaeology. An incident fix needs a second person on the call because nobody is certain which branch of the conditional protects the customer path.
That is the moment I find the bonsai metaphor useful, with one important correction: the job is not to make code look pretty. The job is to preserve a living system while shaping it so the next change has somewhere safe to go.
The metaphor is useful because growth is the problem
Bonsai is not a miniature tree that happens by accident. Its shape comes from repeated, deliberate interventions: pruning, wiring, repotting, and deciding which growth to keep. The tree stays alive, but it does not stay untouched.
Software has the same tension. A healthy service keeps changing because product requirements, dependencies, traffic, and operators change. Left alone, the code can still compile and serve requests while its internal shape becomes costly to understand. The first symptom is not necessarily a defect. Often it is hesitation: the team starts treating a local edit as if it might wake something elsewhere.
Martin Fowler's definition adds the part that is easy to lose: refactoring changes internal structure to make software easier to understand and cheaper to modify, without changing observable behavior. That excludes feature work, a migration that changes a contract, and a rewrite dressed up as cleanup. Those can all be worthwhile. They simply need different planning and different evidence.
Calling everything “refactoring” is how a small maintenance task acquires an unlimited blast radius.
A bonsai is not a weekly haircut
The weak version of the metaphor produces cosmetic work: rename a few variables, reorder imports, split a long file, declare victory. Those changes can help, but they do not automatically make future changes safer.
A better question is: what recurring change does this shape make expensive?
If every new HTTP endpoint requires copying authorization checks, the useful work might be extracting an explicit policy boundary. If a feature flag has spread through handlers, workers, and templates, the issue is probably lifecycle ownership rather than the length of any one function. If an on-call engineer cannot identify the request path during an outage, improving observability or module boundaries may matter more than satisfying a linter.
Refactoring earns its keep when a real next change has exposed a structural constraint.
The shears are tests, not confidence
There is a dangerous part of the gardening analogy. Pruning a branch is visible. A behavior regression may be invisible until production traffic finds it.
That is why tests are not an optional polish step around refactoring. They are the evidence that the system still behaves as intended after an internal change. The refactoring literature repeatedly emphasizes small, safe mechanics. In a 2020 case study on an oversized class, Clare Sudbery describes keeping the code compiling and the tests running at every step, with coverage in place before the refactor starts.
For a service, I would want the narrowest test that protects the behavior being moved, plus the checks that make the deployment credible:
- a unit or component test for the rule being extracted;
- a contract or integration test where the code crosses a boundary; and
- where proportionate, a post-deployment synthetic probe or smoke check for the important path.
The exact stack differs. If the available evidence cannot support the claim that the intended behavior stayed stable, describe the change more modestly or add the missing check.
This also gives a useful stop condition. When a proposed cleanup cannot be protected by a reasonably scoped test, the first task may be characterization: document and test what the system does today, including the awkward behavior nobody wants to rediscover during an incident. Only then is the pruning safe.
AI makes overgrowth faster
AI-assisted coding changes the economics here. It can produce a working branch, a duplicate helper, or another configuration path in seconds. That is useful when the team is deliberate. It also means structural debt can arrive at a rate that manual coding rarely achieved.
A 2025 paper, The Shift from Writing to Pruning Software, proposes a bonsai-inspired interface for AI-generated code. Its core observation is sensible: when generation becomes cheap, the human task shifts toward navigating, evaluating, and reshaping a larger body of code. The paper proposes an interface; it is not evidence that the approach improves production reliability. Still, the direction matches a practical concern: generation can reduce the cost of adding code without reducing the cost of owning it.
The response should not be “never use AI.” It should be to give generated code the same ownership questions as handwritten code:
- What responsibility does this new code own?
- Which existing path does it duplicate or bypass?
- What behavior proves it belongs here?
- If it needs to change later, where would an engineer look first?
If those questions have no good answer, the code is not ready just because it passes the current build.
An infrastructure engineer prunes more than application code
For an infrastructure or platform engineer, the tree extends beyond a service repository: delivery pipelines; infrastructure definitions and environment overlays; identity, network, and secret boundaries; runtime configuration; observability; and the runbooks and recovery paths that connect them.
The same failure mode appears at every layer: a workaround becomes a pattern, a copied pipeline step diverges, an exception disappears into an overlay, or an alert keeps firing because no one owns the decision it is meant to support. Declarative infrastructure can accumulate structural debt just as application code can.
That makes refactoring part of the platform engineer's job, not a side activity reserved for developers. The target is not maximal abstraction. It is a platform whose next user, deploy, incident, and security change can be made with a clear owner, a small review surface, and evidence.
A practical sweep across the field looks like this:
- Platform and developer experience: turn tribal deployment knowledge into paved paths, understandable templates, and documented interfaces. Remove a platform abstraction when it only hides the system without removing work.
- CI/CD and GitOps: consolidate duplicated release logic, make promotion and rollback paths explicit, and keep generated or rendered configuration reviewable. A pipeline refactor is incomplete if an operator cannot tell what will reach production.
- Cloud and infrastructure as code: separate reusable modules from environment-specific intent, name ownership boundaries, and avoid copy-pasted resources that drift independently. Treat plans and policy checks as evidence, while remembering that they do not prove the live system is unchanged.
- Kubernetes and runtime operations: simplify workload, ingress, Gateway, policy, and configuration overlays while preserving the routing and rollout contract. Rendered manifests and a bounded runtime check are stronger evidence than a tidy directory tree.
- Security and identity: reduce broad permissions, clarify trust boundaries, remove obsolete access paths, and make secret rotation and break-glass procedures testable. A cleaner IAM policy is not safer unless the required workload still functions and the unwanted path is actually closed.
- Networking and reliability: replace undocumented routing exceptions with explicit contracts; preserve DNS, TLS, timeout, retry, and failover behavior as you simplify. For a customer-facing path, verification must extend beyond a syntax check.
- Observability and incident response: prune dashboards, alerts, and runbooks that no longer map to an operational decision. Keep the signals that let an engineer answer what changed, who is affected, and how to roll back.
- Cost and capacity: remove idle or duplicated resources only after measuring dependency and demand. Cost reduction is a production change, not janitorial work.
None of these areas needs a giant “platform cleanup” programme to improve. The valuable unit is still a small, explainable structural change backed by the evidence appropriate to that layer: an IaC plan, a rendered manifest diff, a policy evaluation, a synthetic request, a rollout observation, or a recovery exercise. The appropriate checks vary by layer, but each should support a specific preservation claim.
Prune on the path of work
A bonsai owner does not reshape the entire tree every time they notice one awkward branch. The same restraint matters in production code.
I prefer refactoring that is adjacent to a real change: improve a boundary before extending it, remove duplication when the second copy appears, name a concept when the team needs to discuss it, and leave unrelated cleanups alone. This keeps the diff reviewable and lets the pull request tell a coherent story.
It also makes verification more honest. A narrowly scoped refactor can state what it preserved, which tests exercised that claim, and what it deliberately did not touch. A broad “cleanup” commit usually cannot.
There are times to fund a larger structural effort. Repeated incidents, a blocked security upgrade, an untestable critical path, or a migration that keeps failing for the same architectural reason are all stronger signals than aesthetic discomfort. Even then, split the work into behavior-preserving slices where possible. The system does not care that the end state looked tidy in a diagram if the path there breaks customers.
A small operating loop
Before calling a change refactoring, I would write down four things:
- The behavior to preserve. Describe it from the caller or user perspective, not from the internal class name.
- The structural pressure. Name the repeated cost: duplicated policy, hidden dependency, impossible test setup, ambiguous ownership, or something equally concrete.
- The evidence. Identify the test or probe that should remain green before and after the change.
- The boundary. State what is not being redesigned in this pull request.
Then make one structural move, run the evidence, and review the diff as if you were the next person debugging it at an inconvenient hour.
That is less romantic than trimming a miniature tree. It is also more useful. Refactoring is routine maintenance of the structure through which future changes must travel.
Limits of the metaphor
Bonsai suggests a single careful owner and a stable aesthetic goal. Real systems have many owners, competing requirements, and behavior that must change. Refactoring will not replace migration planning, incident response, performance work, or product decisions. A green test suite also cannot prove every production property, especially across external dependencies and real traffic.
So use the image lightly. Keep the discipline.
Preserve the behavior you can state. Improve the structure that is making real work harder. Keep the change small enough to understand. And do not put the shears down until you have evidence that the tree still lives.
Sources and further reading
- Martin Fowler, Refactoring. Fowler defines refactoring as changing internal structure to make software easier to understand and cheaper to modify without changing observable behavior.
- Clare Sudbery, Refactoring: This class is too large (2020). A practical case study of incremental refactoring, covering tests, compiling at every step, and small commits.
- Ulam Labs, Why is refactoring your code important? (2020). A useful example of the bonsai analogy, though this article intentionally avoids treating cleanliness or security as automatic outcomes of refactoring.
- The Shift from Writing to Pruning Software: A Bonsai-Inspired IDE for Reshaping AI Generated Code, arXiv:2503.02833 (2025). A research proposal about navigating and reshaping generated code, not a production-effectiveness study.