What Makes a Rewrite?
Starting again gives us a chance to use what we’ve learned. Deciding whether to do it means understanding what we can improve, what we might lose, and what it takes to keep the next system worth building on.
The part of a rewrite I find appealing is the chance to use what we now understand. Some decisions only reveal their cost after we have built on them for a while. Starting again gives us room to reconsider them. It also gives us room to underestimate everything the existing system is doing for us.
I care most about what happens when we need to change something. The implementation might be small, but understanding its effects and checking that existing behaviour still holds can take much longer. If each change requires us to rediscover the same things, I want to know what would make that work easier next time.
That is the cost I care about. I can live with code I don’t particularly like. I find it harder to accept spending more time on each change while becoming less confident in the result. The system still runs. That doesn’t tell us whether we can keep building on it.
It is tempting to start again. I understand the appeal: choose the boundaries deliberately, make the assumptions explicit, give the next change somewhere sensible to go. But wanting those things doesn’t tell us whether a rewrite will deliver them. We can spend too long getting there, postpone more valuable work, or rebuild the same dependencies in a language we enjoy more.
And if we do get the design right, we still have to maintain it. That part belongs in the decision too.
The old system contains both knowledge and mistakes
Joel Spolsky’s argument against rewrites rests partly on the knowledge embedded in existing code. An odd condition may be a fix for a real failure that took weeks to understand. Replacing the code can mean rediscovering that failure. His 2000 essay uses Netscape as its central warning, but the argument is useful without accepting a single technical decision as a complete explanation of the company’s decline. Things You Should Never Do, Part I.
The awkward part is that code can outlive the explanation for it. We can see what a condition does without knowing which failure put it there, or whether that failure is still possible. Keeping it costs something. Removing it asks us to make a judgement with missing information.
Peter Naur’s Programming as Theory Building offers a useful way to think about this. He argues that programming develops an understanding held by the programmers, including how the implementation relates to the world and how it can be changed. Source and documentation do not fully transfer that understanding. His examples describe teams struggling to extend systems even when substantial documentation was available. Naur, 1985.
The advantage of understanding your own code
This helps explain why rewriting can feel so productive. The team is building an implementation and an understanding together. In the old system, it had to reconstruct someone else’s reasoning before making a change. In the new one, the reasoning is recent and the people who made the decisions are available.
I wouldn’t credit all of that ease to the architecture yet. Of course the new system makes sense to the people who just designed it. I want to see what happens when someone else has to extend it. Can they find the relevant behaviour, understand the constraints, and make a contained change? Or does every useful change still need one of the original authors in the room?
That does not make rebuilding an illegitimate way to learn. Recovering understanding can be valuable enough to justify replacing a bounded component. But it changes what we should measure. Finishing the code is insufficient if understanding remains concentrated in the two people who wrote it.
A bad implementation and a wrong model need different work
Consider a hypothetical booking system built around one assumption: a reservation belongs to one property. Over time, the product begins selling itineraries with several properties, partial cancellations, and independently confirmed suppliers.
At first, another table and a few conditionals may be enough. Later, every operation has to reconstruct an itinerary from several reservations. Payment allocation, cancellation, support, and reporting each develop a slightly different interpretation of the same relationship.
The difficulty now comes from the model. The application is repeatedly translating between what it stores and what the business means. Tidying individual functions might reduce local complexity without removing that translation.
This is different from a well-fitting model hidden inside badly organised code. If reservations still belong to one property and the main problem is that cancellation logic is scattered across controllers, consolidating that logic could address the constraint directly. Replacing the application would buy much more change than the problem requires.
Dropbox’s account of Nucleus gives a concrete example of a model-level limitation. Its old sync engine represented moves as a delete and an add, allowing a partial operation to leave a file apparently missing elsewhere. The replacement introduced different primitives and consistency guarantees. Dropbox described a four-year effort, undertaken after substantial incremental work had failed to resolve the underlying limitations. Rewriting the heart of our sync engine.
Finding the actual replacement boundary
The useful question is how many assumptions must move together. An awkward function is cheap to replace. A concept encoded in database keys, external integrations, reports, and operating procedures has a much larger boundary than its class definition suggests.
Even then, “the model is wrong” does not imply “replace everything.” The booking system might introduce an itinerary model while preserving the existing reservation implementation underneath it. Whether that works depends on the operations the new model needs to own. If it must enforce guarantees that the old reservation code can independently violate, wrapping the old implementation may preserve the very constraint being removed.
This is what I would want someone to show me before asking for a rewrite: the assumption that prevents the required behaviour, and how replacing this particular part removes it. I may agree that the code is difficult. I still need to know why this amount of replacement is necessary.
Separate repositories do not establish independence
In this example, the deployment boundary is more revealing than the repository boundary. If the parts must be released together, then their separate codebases do not give us independent delivery. A rewrite that reproduces those dependencies in a newer stack could leave that limitation untouched.
Before deciding how much to replace, I would want to distinguish dependencies that are necessary for the business operation from dependencies created by the implementation. Does a change require coordinated releases because an interface breaks, because several services interpret the same data, or because we cannot verify compatibility? Each explanation calls for a different intervention.
The answer affects the destination. We might need boundaries that allow independent deployment. We might be better served by consolidating parts that genuinely change together. We might first need enough automated verification to discover whether some of the coordination is precaution rather than necessity. I don’t see a reason to preserve the service count just because we already have it.
Feature parity can preserve the problem
“It will do everything the current system does” sounds reassuring. It is also a fairly large commitment to make before we have established what the current system actually does, and how much of it anyone still needs.
Cartwright, Horn, and Lewis describe a logistics replacement that spent substantial effort recreating support for package types the business intended to stop handling. Their discussion of feature parity also identifies cases where preserving behaviour is appropriate, including fixed calculations and workflows used by highly trained operators. The issue is deciding what requires continuity and what deserves reconsideration. Feature Parity.
In the booking example, support staff may use a spreadsheet to reconcile split payments because the application cannot represent them. Reproducing the old screens does not reproduce the whole working system. The spreadsheet and the judgement of the staff are part of how the business currently functions.
A replacement that eliminates that manual work could be worthwhile even if its screens look nearly identical. Conversely, a new interface might increase the staff’s workload while making the engineering team’s code easier to understand.
Who can authorise simplification?
Removing behaviour requires someone with the authority to accept the consequences. An engineer cannot safely decide that an obscure export is unnecessary because it appears in few access logs. It may be used once a year and be essential when it is used. Once a year may be exactly as often as it needs to work.
There is also a funding problem. If a team must promise complete parity to get a rewrite approved, it may lose the permission to simplify the product. Later, scope cuts look like broken commitments. Decisions about retiring features, changing workflows, and accepting incompatibilities need to be part of the proposal, with the people affected involved early enough to change it.
The comparison is usually unfair
An existing system is observed under actual conditions: interrupted engineers, incomplete requirements, historical data, support obligations, and customers using it in unexpected ways. The replacement is often assessed through a prototype with clean inputs and a protected team.
I would be careful about calling that an improvement yet. The prototype can tell us whether a new representation handles partial cancellation without contradictory states. That is useful evidence. It hasn’t had to carry the rest of the product.
What a protected team changes
There is another confounding factor: rewrites receive attention. A team may be given stronger ownership, time to write tests, permission to remove obsolete features, and direct access to domain experts. If delivery improves, how much came from the new implementation, and how much came from those changes?
Give the maintenance option an actual budget before comparing it with the rewrite. If only one option gets uninterrupted time, tests, and permission to remove old behaviour, we have already influenced the result.
The InsighTD survey collected 653 valid responses across six countries. Practitioners most frequently cited time pressure or deadlines as a cause of technical debt; common reported effects included delays, low maintainability, and rework. This does not prove what caused a particular codebase’s problems. It does make a purely technical explanation incomplete unless the team has examined its delivery conditions. Ramač and colleagues, 2021.
For a concrete comparison, give a bounded part of the existing system sustained attention. Improve its tests, clarify ownership, and attempt one representative change. The result can reduce uncertainty in either direction. If the work becomes tractable, a broad rewrite needs a stronger case. If the attempt exposes a constraint that survives those improvements, the replacement proposal has better evidence.
Opportunity cost depends on when value arrives
Counting engineer-months is useful, but it treats time as if every month were interchangeable. A delayed feature can lose most of its value if it misses a customer’s purchasing window. A delayed maintenance improvement may cost roughly the same amount each month. A missed support deadline can create a much sharper change in cost.
Suppose a rewrite takes six months and saves the team two engineer-months of maintenance per month afterwards. That sounds attractive. But if the same team could spend two months removing the largest recurring problem, the rewrite must be compared with the savings from that smaller intervention, including the four months in which it would already be helping.
Shipping value and retiring cost
Benefits can arrive at different stages too. A faster read path might save infrastructure cost as soon as some traffic moves. A licensing bill may disappear only when the final dependent workload leaves. Moving 90% of functionality does not necessarily remove 90% of operating cost.
This makes the last part of a migration economically different from the first. Early progress can deliver visible improvements while leaving the expensive shared dependency intact. If the difficult remainder is continually deferred, the organisation pays for the replacement without receiving the retirement benefit used to justify it.
I would want several dates here. When can users benefit? When does maintenance get cheaper? When can we actually turn the old infrastructure off? When is the team free to work on something else? Calling all of that “completion” makes an estimate easier to present and harder to use.
When one wrong assumption affects the whole estimate
The uncertainty is also connected. A misunderstood data model can delay implementation, increase reconciliation work, and extend parallel operation at the same time. Treating these as unrelated small contingencies understates a bad outcome. A useful pessimistic scenario follows one plausible failure through all the work it affects.
I don’t need the estimate to pretend it is exact. I need to know which assumption makes the investment worthwhile. We can calculate salaries very precisely and still have no good basis for saying the replacement will make development twice as fast.
The old system keeps moving
A rewrite has to catch a system that may still be changing. Each feature delivered in the old implementation can create more work in the new one. Freezing the old product removes some of that moving target, but creates opportunity cost for the business and frustration for its users.
There is no universally correct allocation. A team needs an explicit rule for which changes go into the old system, which wait, and which can be delivered through the replacement. Without that rule, product decisions become repeated negotiations between two implementations.
The people both systems need
The people we need most may already be doing four jobs: maintaining the old system, explaining it, reviewing the replacement, and investigating why the two disagree. Adding engineers gives us more people who can write code. It doesn’t necessarily give those few people any more time.
Separating the teams can protect focus while making knowledge transfer harder. Keeping one team responsible for both preserves context while exposing it to every interruption. Either arrangement needs to account for that cost. Assuming that one group can “keep the lights on” cheaply often means assigning it work whose difficulty is discovered only when it fails.
Status matters as well. If the new system is treated as the interesting work and the old one as an obligation, the people protecting current revenue can end up with less influence over the design that will replace it. Their objections may look like resistance when they are evidence about behaviour the replacement has not yet encountered.
The decision process needs to make those objections testable. A specific incompatibility can become a migration experiment or an acceptance criterion. Neither enthusiasm for the replacement nor familiarity with the old system should settle it on its own.
Language choice changes which work the team must do
A language can remove real constraints. Discord’s Read States rewrite is a useful example because the reported problem was specific: latency spikes associated with garbage collection in a large cache. The team tried tuning and cache changes before replacing the service with Rust, and reported improvements in latency, CPU, and memory. This was a small service ported in 2019 using older Go versions, not evidence that an arbitrary Go application should be rewritten today. Discord’s engineering account.
I like having a technical reason that specific. Better control over allocation matters if allocation is causing the problem. It may do very little for an endpoint that spends its time waiting on the database. Liking the language is fine; I would rather say that honestly than turn the preference into a performance claim we haven’t measured.
What the type system cannot decide
Correctness arguments need the same care. A type system can help encode distinctions between a pending booking and a confirmed one. It cannot decide whether confirmation means the supplier accepted the request, payment settled, or both. If that meaning is disputed, changing languages may make the disputed model more explicit without making it correct.
There are cases where that explicitness is itself valuable. Forcing the team to settle a distinction can remove ambiguity that the previous implementation tolerated. But similar clarification might be possible in the current language. The proposal needs to separate what requires a new language from what requires more deliberate modelling.
The cost beyond the language
The ecosystem can outweigh the language in either direction. A mature library may eliminate a large maintenance obligation. A missing integration may create one. Build and deployment support, profiling, debugging, dependency updates, and available reviewers all affect the cost of ownership. Benchmark results capture only part of that work.
Team familiarity matters, but it should not become a permanent veto on learning. A language that fits an important long-term constraint may justify training and a slower start. The relevant question is whether the organisation will fund that transition and retain enough shared competence to support the result. The person who pushed for the language being productive in it doesn’t answer that question.
I would prefer an experiment that includes an ordinary maintainer changing the prototype, a realistic failure being diagnosed, and the required integrations being exercised. That tests more of the proposed operating model than a successful implementation by its strongest advocate.
AI can change the cost without resolving the reason
The recent AI-assisted ports make this decision more interesting. If producing the replacement becomes much cheaper, some projects that were previously unrealistic deserve another look. I don’t think the value of a rewrite depends on how much code someone typed by hand.
But part of the appeal we discussed earlier was the understanding built along the way. Automating the implementation does not automatically give the maintainers that understanding. If we replace code we struggle to reason about with different code we also struggle to reason about, I have trouble calling that progress towards maintainability. We may have removed a technical limitation while making the next change harder to judge.
A deliberate port preserves something worth keeping
Bun’s July 2026 account describes a mechanical Zig-to-Rust port intended to retain its architecture and behaviour while gaining stronger memory-management guarantees. Jarred Sumner describes explicit translation guidance, the existing language-independent test suite, and separate AI reviewers looking for defects. He monitored the workflows and adjusted them when they went wrong. This was not a claim that humans had reviewed every generated line. Keeping the result recognisable to the team was an explicit concern. Rewriting Bun in Rust.
Flow’s team made the distinction even more explicitly in its June 2026 account: a largely line-by-line OCaml-to-Rust port, with redesign deferred and the shared-memory system a major exception. They required original code beside the translation during review, rejected excessive departures, and reported a final human review. They also acknowledged carrying old design mistakes forward. Flow’s OCaml to Rust Port.
That restraint makes sense to me. If we already understand the model and want a different implementation language, preserving the structure gives us something to compare against. We don’t have to rediscover the architecture at the same time as we learn how the new language expresses it. Carrying an existing flaw forward can be an acceptable choice when fixing it would turn one migration into two different problems.
It still leaves work to do. Similar-looking code in two languages can have different behaviour, and familiarity with the original does not establish competence in the replacement. I would treat correspondence as an aid to investigation and review, not a guarantee of correctness. These accounts describe ways to constrain the work; they do not establish how easy either codebase will be to maintain years later.
The understanding still has to belong to the team
I wouldn’t require every maintainer to understand every line. That would rule out plenty of systems written entirely by people. I would want the team to understand the parts it changes, where the important guarantees are enforced, and how to investigate a failure that the existing tests never anticipated.
Being able to generate another patch is not the same as being able to judge it. An AI explanation can help, but I want someone to be able to check that explanation against the implementation, the requirements, and an observable result. Otherwise the same process supplies both the change and our reason for trusting it.
For me, this changes how we should describe the savings. The first working version might arrive sooner while review, learning, and diagnosis still require substantial time. If that work gets deferred until the first incident, it has not disappeared from the cost of the rewrite.
I would include a maintenance exercise before declaring the result ready: have someone other than the person directing the port make a meaningful change and investigate a failure. Let them use AI. The useful evidence is whether they can explain why the fix is correct, what it might affect, and how they checked it. That tests the capability we will need after the migration team moves on.
If AI makes the translation affordable and the team can still own the result, that is a real opportunity. If it produces a system nobody can confidently change, then it has missed one of the main reasons I would want to rewrite in the first place.
Incremental replacement has a price and a limit
The Strangler Fig approach replaces responsibilities gradually, allowing investment and returns to occur in stages. That can make a large replacement easier to adjust as the team learns. Martin Fowler’s account.
The bit I would spend more time on is the system halfway through. We can draw a neat boundary around some code and still be unable to move its responsibility independently.
Return to the booking example. If one itinerary can be changed by both old and new implementations, which one owns cancellation? Can a partial cancellation update payment allocation atomically? Can an old background job overwrite the result using its original assumptions? These are properties of the combined system, even if each implementation is internally correct.
Temporary adapters and synchronisation mechanisms can address some of this. Cartwright, Horn, and Lewis describe such components as transitional architecture, built to support displacement and intended to be removed. Their purpose includes buying safer migration paths, so assessing them only as code that will later be discarded misses part of their value. Transitional Architecture.
When coexistence becomes the constraint
But the temporary arrangement must remain cheaper or safer than the alternatives. If every step requires both systems to understand both models, the migration may add complexity faster than it removes responsibility. A different cut might work better: migrate a complete customer cohort, product line, or workflow so one implementation owns its decisions end to end.
Sometimes that separation is not available at an acceptable cost. A small, well-understood subsystem may be easier to replace in one controlled cutover than to support through months of coexistence. The size that matters is the responsibility and state being transferred, including its external dependencies.
Incremental work can also stall because the organisation never funds the final extraction. A useful stage should either deliver a benefit that justifies remaining in that state or have a credible commitment to the next step. Otherwise “mostly complete” can become the arrangement everyone has to maintain.
Rollback stops being simple when state changes
For a read-only calculation, running old and new implementations on the same inputs can reveal discrepancies without changing the world. Stateful workflows are harder. Executing both paths may send two notifications, reserve inventory twice, or create conflicting records.
Comparisons need to account for those side effects. Replayed traffic also reflects the workloads captured in the sample; it may miss rare sequences, long-lived records, or behaviour during partial failure. Passing the comparison establishes evidence about the exercised cases, not general equivalence.
Dropbox’s account of testing Nucleus describes deterministic simulation and injected filesystem and network failures. It is useful because the test strategy was concerned with sequences and failure conditions, beyond normal successful operations. Testing sync at Dropbox.
The point where data cannot go back
In the booking system, a more consequential boundary appears when the replacement writes an itinerary that cannot be represented faithfully as independent reservations. At that point, directing traffic back to the old application may produce an incorrect interpretation of valid new data.
The team then needs a choice made in advance: restrict new behaviour until rollback is no longer required, maintain a reversible representation, or accept a transition after which recovery proceeds forward. Each has costs. Holding back new behaviour delays some of the rewrite’s value; preserving reversibility adds work; committing to forward recovery requires confidence in repair procedures.
Rollback needs an answer about the data. I want to know what the old application will do with a record written by the new one. A feature flag can choose which implementation runs next. It cannot undo a message another system has already acted on.
Staying put can be rational, including when the code is unpleasant
Maintenance cost depends on what the system is asked to do. A difficult component that rarely changes may be cheaper to retain than a well-designed replacement. A similar component on the path of every planned feature can impose a much larger cost.
This is why code quality alone is an incomplete investment signal. Look at expected change, operational exposure, and remaining useful life. A system approaching retirement may justify containment and support. A system becoming central to the next product direction may justify a much larger intervention before the workload arrives.
What waiting buys and what it costs
Deferral can buy information. The product direction may become clearer, a dependency may become easier to replace, or a small experiment may reveal that the proposed design does not solve the problem. Waiting has value when it reduces uncertainty without closing important options.
Waiting can also make those options worse. More integrations may adopt the old data model. The people who understand it may leave. A team can become so occupied by recurring failures that it has no capacity left to change the cause. In those circumstances, the benefit of starting earlier includes having enough organisational capacity to complete the work.
The timing decision therefore needs a view of how both maintenance cost and replacement cost change if the team waits. They need not rise together. Nor does either necessarily rise at all.
Declining a rewrite still leaves a maintenance decision
I can accept that a rewrite is too expensive. What I struggle with is treating that as the end of the conversation. We still intend to change the system. We still need some confidence that those changes work. Where is the time for that coming from?
Without automated tests, confidence has to be rebuilt around each change through other means. That takes time, but it can also make structural improvements harder to attempt. The changes that might reduce future effort require us to modify code we already struggle to verify. A newer framework does not remove that problem by itself; upgrading safely depends on knowing which behaviour must survive.
I would want to start accumulating evidence that remains useful whichever direction we take: tests around important behaviour, a map of the dependencies that force coordinated deployments, and a realistic assessment of the supported upgrade paths. They could make continued maintenance less expensive, or give a replacement a safer starting point.
I keep coming back to the idea of the system eventually toppling over. It makes the cost sound like something we will recognise when it arrives. I’m not sure we will. The application could stay up, the tickets could keep closing, and the team could quietly become less willing to touch anything beyond the immediate fix.
A rewrite gets an estimate and a decision. The hours spent checking the same behaviour, coordinating another release, and rediscovering the same dependency disappear into ordinary tickets. We are spending that time whether we call it an investment or not. Recording it would at least let us compare the options honestly.
That still doesn’t prove a rewrite would pay for itself. I would settle for a concrete answer: if we cannot replace it, what can we improve, and what would make us reconsider? Approving a rewrite that nobody has time to finish is no better.
The features that never reach the roadmap
There is a less visible consequence of staying: the product may adapt to the constraint. Difficult features are excluded before reaching the roadmap, so delivery metrics look acceptable. Counting only completed changes misses capabilities the organisation has stopped considering.
Those forgone capabilities still need scrutiny. “We could build anything afterwards” is not a benefit estimate. Identify the actual opportunity, its likely demand, and the constraint preventing it. Otherwise an unlimited future becomes a convenient justification for an unlimited project.
The rewrite has to survive ordinary development
Suppose we get approval. We finish the rewrite, move the users over, and retire the old system. The boundaries make sense. The tests pass. Changes are easier.
I would still want to know what keeps it that way.
The next deadline doesn’t care how carefully we designed it. Neither does a requirement we didn’t anticipate. The design has to remain workable when its original authors aren’t reviewing every change.
Research on architecture erosion supports treating this as more than a code-quality problem. Li and colleagues’ systematic mapping study examined 73 studies and identified both technical and non-technical causes, consequences for quality and evolution, and a range of detection and mitigation approaches. The authors also called for more empirical work on industrial practice. This is evidence that erosion has several causes, not a prediction that every rewrite will deteriorate at the same rate. Understanding Software Architecture Erosion, 2022.
My concern is what happens when the replacement’s maintenance assumptions turn out to be unrealistic. A design might require every contributor to understand the whole dependency graph, or every cross-component change to receive a senior architect’s review. That may work during a focused rewrite and fail once several teams are delivering ordinary features. The recurring effort needed to preserve the design belongs in the original cost estimate.
Translate the philosophy into properties you can check
“Keep the services independent” is an intention. Supporting an old client while a new service version is deployed is a behaviour that can be exercised. The latter gives the team evidence about whether the former remains true.
Different promises require different checks. A rule against importing another module’s internal code can be checked statically. Independent deployment needs compatibility testing across versions. A business invariant, such as never allocating more money than a payment contains, needs checks at the point where state changes, with tests that exercise retries and concurrent operations. No single architecture test establishes all three.
The enforcement point matters. If only one application path validates a rule but a background job can write the same state directly, the invariant depends on conventions outside that validation. Conversely, putting a rule at a shared boundary can reduce how many callers must remember it. The team should be able to identify where a guarantee is enforced and which paths can bypass it.
The intent also needs an explanation. A dependency check can reject an import without telling a future maintainer why that direction was prohibited. A short decision record should connect the restriction to its purpose, the alternatives considered, and the circumstances in which it should be revisited. Otherwise the check may survive after its rationale has stopped applying.
Passing the architecture checks is not enough
Shopify’s 2024 Packwerk retrospective is useful precisely because it complicates the case for enforcement. The team reported that eliminating static dependency violations did not make a package runnable in isolation: booting and testing it separately revealed dependencies the tool had not detected. Some fixes also added indirection that made code harder to understand. The team eventually added isolated execution checks to CI. This was modularisation work, not a study of rewrite outcomes, but it illustrates the gap between passing a structural check and obtaining the property the check was meant to protect. A Packwerk Retrospective.
This is why I want to test what the architecture promises, not stop at whether we followed its rules. If the promise is isolated operation, run the component in isolation. If it is contained change, examine whether representative features still require coordinated edits. A falling violation count can be useful evidence, but it cannot answer a different question merely because it is easy to measure.
Exceptions need a decision, not just a waiver
Suppose a deadline leads to one direct read of another component’s tables. The immediate change may be small and reasonable. Once other code depends on the shape and meaning of those tables, changing them requires coordination. Several individually defensible exceptions can remove the independence the rewrite was funded to create.
Prohibiting every exception is not a workable answer either. An urgent fix may justify a temporary compromise. What matters is recording which property was weakened, who owns the consequence, and what event or date will trigger review. If nobody owns the follow-up and nothing triggers a review, I would treat it as part of the architecture. Calling it temporary doesn’t give it an end date.
Repeated exceptions can also be evidence against the boundary. If routine product work constantly crosses it, the team should investigate whether the responsibilities were divided incorrectly. Preserving the original diagram at any cost can create adapters and indirection that make the system harder to understand. The purpose of the boundary must remain open to scrutiny.
If the team cannot sustain the design
There are at least three possibilities to distinguish: the safeguards are missing, the team has no capacity to act on them, or the design requires more coordination than the organisation can reasonably provide. Adding a check helps the first. It can merely create an ignored queue of violations in the second. In the third, reducing the number of boundaries may be the more maintainable choice.
“Easy to improve” is what I want from the design. But someone has to explain the constraints in reviews, help new maintainers work within them, update dependencies, and keep the tests useful. I don’t think we can claim to value maintainability while leaving all of that to whatever time happens to remain.
If those conditions cannot be supported, the rewrite’s benefits may be shorter-lived than its proposal assumes. That does not erase a real improvement already delivered: removing an unsupported dependency or resolving a correctness limitation can remain valuable. But the economic case changes if lower maintenance costs last for one year rather than five. The cost of preserving the design belongs in the cost of the rewrite.
The useful decision is the next commitment
I don’t think the first decision has to settle the whole rewrite. It should settle what we are willing to do next, and what we need to learn from it.
For the booking system, I would start by testing whether the itinerary model can support a real cancellation workflow, including historical reservations and supplier failures. I would also test whether the old system can accommodate that model with a contained change. Both experiments inform the choice; neither needs to become a promise to rebuild the product.
The decision after that work should name what has been learned, what remains uncertain, and what the next stage makes harder to reverse. A successful demonstration of the new model may justify migrating one workflow. It does not automatically justify changing the reporting platform or rewriting the user interface.
Reassessing after the investment has begun
The same discipline applies late in the project. Money already spent does not make completion worthwhile. But cancellation is not free either: the team may already have migrated customers, introduced obligations, or removed an old capability. Compare the remaining paths from the system that actually exists now. Continuing purely to defend the original decision is as weak as cancelling purely because the original estimate was wrong.
After launch, measure whether representative changes became easier, which operating costs actually disappeared, and whether the promised capabilities reached users. Some overhead will have been deliberately purchased for safety during migration; distinguish that from overhead the team must now carry indefinitely.
The change I would pay most attention to is the first substantial one made by someone outside the rewrite team. Let them find the behaviour, work out the constraints, and extend it. See where they get stuck.
That is what I want good architecture to make possible. Someone should be able to build on what we left them without first having to make the case for throwing it away.
A note on the evidence
The company accounts cited here explain decisions under particular workloads, staffing, and business conditions. They are useful for examining mechanisms, but they are selected reports, not a representative sample of rewrite outcomes. A successful migration cannot show us what would have happened had the same team spent the same time improving the original system. The Bun and Flow accounts describe recent AI-assisted ports, not long-term studies of maintainer understanding. My concern about understanding and my suggested maintenance exercise are judgements about what I would want to verify, rather than measured outcomes from those projects.
Naur provides a conceptual account of programming knowledge. InsighTD provides survey evidence about practitioners’ experiences of technical debt. Neither establishes that rewriting improves productivity. The architecture-erosion review maps existing research, and the Packwerk retrospective reports one team’s experience; the maintenance proposals here are my synthesis of that evidence. The coupled-service and booking examples are hypothetical illustrations, not accounts of a particular organisation. The comparisons and decision criteria are my interpretation of the sources rather than a validated prediction method.