Software Rot: When Systems Become Legacy

· 4 min read
Software Rot: When Systems Become Legacy

Recently, I became a user of Revolut, an 8-year-old digital banking and financial services company. Initially offering just money transfer and exchange services in the UK, Revolut now provides various financial products for businesses and individuals across Europe, the US, and Japan.

Revolut’s services are much more convenient than a traditional European bank. Their web and mobile apps are highly responsive with intuitive user interfaces. Their support representatives are available 24/7 to answer questions. Revolut natively supports multi-currency accounts and cryptocurrency. I could go on and on about their features. There are still some limitations, though, so using a Revolut account and a traditional bank account is best, but their apps are exceptional nonetheless.

Instead of rambling about how many bank services are outdated, I want to focus on the striking differences between Revolut’s apps and a “traditional bank.” I won’t name any specific banks, but chances are, your experience with your bank’s websites or apps could be better if you’re in Europe or the US.

Legacy Systems in Traditional Banking

The slow crawl of digital banking adoption in the US and Europe boils down to one colossal roadblock: legacy systems. For decades, banks have built their technology stacks on outdated foundations. Now, they need to modernize the antiquated systems.

A system gains “legacy” status when it can no longer meet an organization’s needs or today’s engineering standards. But legacy systems still stay for a few reasons. Often, they are too complex and costly to modernize or replace. Or they still provide some crucial business operations. Legacy systems create plenty of headaches. They can be hard to maintain, integrate with new technologies, or scale up to meet growing demands.

Outdated systems often result from technical debt accumulated over a long time. This debt piles up through suboptimal decisions, shortcuts, or compromises during software development. Over time, it leads to higher maintenance costs, less agility, and slower improvements.

Not all legacy systems are necessarily burdened with technical debt, and not all technical debt will lead to a system becoming legacy. But it’s common for the two to go hand in hand.

The Role of Technical Debt

A software product that survives a few years of rapid development inevitably accumulates technical debt. We have a product we’ve been building for five years, and the amount of technical debt items is so immense that we need dedicated developers to address it and keep it at bay.

There are three options that I see when it comes to tech debt:

One is to ignore it and let it pile up. While it may sound crazy to the engineers, ignoring technical debt might be a good choice for the business in many cases. Refactoring the code is not a priority for a startup that might not exist the following year. Addressing technical debt becomes a problem after a while, but only if the company is still around. That’s why working as a software developer on a greenfield startup project differs significantly from other development types. It’s more about hacking together quick and dirty solutions rather than having a long-term vision.

The second choice is to address the technical debt as you go along. We’ve been doing that for a while now, and this approach requires some tough tradeoffs. Most importantly, it increases the complexity and delivery time of new features. For example, we are building a local-first app. Every year, we spend a few months refactoring the data layer that syncs data between the frontend and backend. We’re constantly adding new offline functionality and do it pretty fast. But occasionally, we halt development for a few sprints when we don’t add any new features. We focus on major refactoring during this time. The CTO has to sell this approach to stakeholders, and it doesn’t always work because resolving techincal debt is expensive and doesn’t bring any immediate value to the end users.

The third choice is to ignore growing tech debt and do a complete rewrite. This is what startups often do. Years ago, when our development team focused on startups, we had two types of projects. We either rapidly built MVPs for startups, or we spent months and months rewriting existing MVPs built by someone else into something usable long-term. But MVPs are relatively small — they usually only take a few months to build initially, so they can be analyzed and rewritten with a reasonable effort. It’s pretty different for big projects.

The Challenges of Rewriting Large Systems

Imagine trying to rewrite an entire system that’s been in development for a few years. It would be nearly impossible. In my experience, I’ve never seen a large legacy system successfully rewritten from scratch. If you know of any examples where this was done well, I’d love to hear about them and learn how it was accomplished.

Typically, I’ve seen a “partial rewrite” approach, where a new system is built in parallel, and eventually, everyone grows tired, leaving two systems coexisting. This is a terrible outcome, as engineers must now support both systems, dragging out the painful migration process for a long time, and the development process becomes even slower. Hence, the business feels only negative effects.

Writing code without worrying about technical debt or building systems that will eventually become legacy would be ideal. But that’s not how it works, unfortunately. It’s like our human bodies — we can’t live without care or maintenance. We must make genuine efforts to stay healthy through exercise, checkups, addressing medical issues before they worsen, eating well, etc. Our bodies are complex systems that are constantly evolving and require upkeep.

Software systems, like living organisms, are dynamic — they’re never static or complete. That’s why regular checkups and refactoring are so important. If we allow technical debt to pile up unchecked, systems become rigid and complex over time. Soon, even simple changes become impossible. The system ossifies.

Rather than attempt the impossible task of rewriting everything from scratch, a more pragmatic approach is to refactor legacy code over time. Break up the monolith into smaller, more maintainable components. Pay technical debt incrementally through regular code reviews and rewrites of problematic sections. Build resilience by designing new features to be loosely coupled and embrace modern architectures. With patience and persistence, large systems can be modernized without starting over.

Originally published on