How to Rewrite a Legacy System: The Ship of Theseus

Introduction

Recently faced with many issues of maintaining legacy code, I stumbled upon this blog: Understand Legacy Code🔗, documenting the dilemmas and solutions in rewriting legacy systems. Taking my own project as an example, I found that things were more complicated than I initially imagined during the project upgrade assessment!

  • Framework Version Upgrade: What happened to backward compatibility? Once the version changes, the code style and architecture all need to change, and the old version’s framework will be permanently abandoned.
  • Missing Documentation: Due to the rush, there was no time to document, and the “learning” accumulated becomes a pain every team member must go through, needing to digest a large amount of learning debt before getting up to speed on the project.
  • Deeply Rooted Anti-Patterns: Incorrect development patterns have become ingrained in the culture of the entire project, and existing anti-patterns can easily lead to the occurrence of the Broken Windows Theory🔗.

In short, reaching a point of no return, perhaps the only remaining high-risk option is to rewrite, because…

  • The coupling between programs is too high; all the functionality is related each other…
  • No tests or difficult to test…
  • Rewriting is cheaper than refactoring…
  • The project can no longer afford changes…

Thus, you embark on the path of project rewriting.

Traps of Rewriting a Project

(Story below is from: The Ship of Theseus to NOT rewrite a legacy system from scratch🔗)

  1. You discuss with management about the strategy of stopping new features for some time, while you rewrite the existing app.

  2. You estimate the rewrite will take 6 months to cover what the existing app does.

  3. A few months in, a nasty bug is discovered and ABSOLUTELY needs to be fixed in the old code. So you patch the old code and the new one too.

  4. A few months later, a new feature has been sold to the client. It HAS TO BE implemented in the old code—the new version is not ready yet! You need to go back to the old code but also add a TODO to implement this in the new version.

  5. After 5 months, you realize the project will be late. The old app was doing way more things than expected. You start hustling more.

  6. After 7 months, you start testing the new version. QA raises up a lot of things that should be fixed.

  7. After 9 months, the business can’t stand “not developing features” anymore. Leadership is not happy with the situation, you are tired. You start making changes to the old, painful code while trying to keep up with the rewrite.

  8. Eventually, you end up with the 2 systems in production. The long-term goal is to get rid of the old one, but the new one is not ready yet. Every feature needs to be implemented twice.

The story illustrates how rewriting leads to two completely independent systems, highlighting the heavy burden and challenges that rewriting can bring in this example.

Ship of Theseus Pattern

If the ship’s parts are gradually replaced as they rot, is it still the same ship after all parts have been replaced? Without discussing philosophy, the perspective of this approach lies in the gradual replacement of rewritten segments, representing faster feedback, less work (at least less psychological burden), and avoiding loss of functionality!

  • Let the new code act as a proxy for the old code, where users using the new system are simply directed to the old system.
  • Gradually implement the behaviors of the old code into the new code.
  • Gradually phase out the old code, replacing it with the new code.

Conclusion

Our concept of rewriting is often “tear everything down and redo it,” but this approach is overly idealistic, and in some projects that require rewriting at scale, such an approach may lead the project into an abyss. Taking a step back to rethink and reassess the ultimate goal of rewriting, the Ship of Theseus pattern is a simple yet easily overlooked option.

Further Reading