In my 15 years of experience in the software business, I’ve held various roles throughout my career, starting as a developer and advancing to the position of a staff engineer. My professional journey has taken me through various domains, from real estate to insurance. Now, I’m back in the field, actively writing production code and maintaining systems. I would like to share some practical insights about managing technical debt, addressing legacy systems, and unlocking their potential.
Decoding „legacy“ software
When we talk about “legacy” system software, it’s like having a dual definition: “legacy” refers to a software system built on outdated or ancient technologies. On the flip side, we tend to slap the “legacy” label on code that’s seen better days: it’s a mess with low-quality code, developers cringe at the thought of touching it, it’s fragile, breaking down at the drop of a hat and It’s as reliable as a leaky boat. In other words, when people label a codebase as “legacy,” they often do so because it’s built on outdated technologies and is in a state of disrepair, exhibiting various issues, frequent errors, and a lack of reliability.
Unlocking the potential of legacy systems is a strategic decision that involves tapping into the value embedded within older software while also adapting to contemporary needs. By carefully identifying and extracting critical components or functionalities from the old system, you can integrate them into a modern framework, modernizing your software infrastructure. This approach allows you to leverage the tried-and-true elements of the legacy system, sparing you the cost and risk of a complete overhaul. Simultaneously, maintaining the legacy through continuous improvements ensures its reliability and relevance.
It’s a balance between cherishing the heritage of your software and steering it towards the future, ultimately optimizing your technological investments.
Over my 15 years in business, I’ve had the privilege of collaborating with a wide array of companies. Legacy system software is not confined to the domain of older corporations; it transcends these boundaries. Even within the vibrant startup ecosystem, instances arise where the imperative of restructuring the source code for vital applications becomes readily apparent. Development teams, driven by the urgency of product releases, may initially resort to shortcuts, inadvertently laying the groundwork for a spectrum of challenges.
The unassailable truth is that legacy code is a pervasive and universal feature of the software development landscape, touching startups just as significantly as established enterprises.
So, how can we tackle this challenge?
Future-proofing your software and addressing technical debt
From a business perspective, hiring developers with expertise in obsolete technology can be challenging. Additionally, there are security concerns to consider. For instance, picture a scenario where a framework or library goes out of support, and a critical security flaw emerges. What’s the plan then? Pausing production for two months to rewrite the code isn’t an ideal scenario. Thus, consider your software as a living organism. It requires constant care and maintenance.
In the world of software development, there are times when you need to act fast – save the day with a quick five-minute fix for whatever urgent reason. And when those moments come, do it without hesitation. But here’s the catch: while you’re putting out fires, simultaneously create a ticket for technical debt. This ticket becomes your commitment to providing a much better, more sustainable solution to the problem.
Effective communication is also the key to legacy products and features. Legacy status doesn’t happen overnight; it accumulates over time. If a framework is set to become unsupported in five years, it’s crucial to communicate this well in advance. In other words, it’s essential to keep your customers informed about the status of your product or feature and any impending changes, especially when it involves transitioning to legacy status.
In summary, managing legacy systems and addressing technical debt is a critical aspect of software development. It involves continuous improvement, strategic planning, and effective communication to overcome challenges and keep systems up-to-date.
The ultimate goal is to maintain a fresh and functional codebase, free from the shackles of a bygone era. The key takeaway is this: it’s not about avoiding technical debt at all costs but managing it wisely.
A real-life example of continuous improvement
Continuous improvement is an integral part of the software development process. Think of it as running a restaurant. Imagine being the owner of a restaurant where you have to close once a month for a week to thoroughly clean the kitchen. This periodic maintenance is a necessary side effect of ongoing cooking activities. Similarly, in software development, not consistently taking care of the system can lead to the undesirable “legacy” label.
Here’s an example from a recent project:
- Domain of the project is Global Mobility.
- Our team is responsible for managing a microservice that calculates taxes worldwide.
- This is an annual activity that requires updates to legislation and tax rates.
- Initially, it was a substantial task, demanding the dedication of three developers for an entire tax year.
- The codebase needed some modernization and a little TLC (care, attention, and refurbishment) to enhance its condition.
- After a series of refactorings, it now takes just two developers and half a year to handle the same workload.
- We also introduced a Domain Specific Language (DSL) to cover certain aspects.
- Both business and tax experts can make simple contributions after DSL introduction as the codebase is easy to read.
- It’s a significant win, particularly in the long run.
Within our team, we’ve established an Epic, an umbrella ticket for technical debt, which is treated as a first-class citizen in our workflow. Just as essential as any other task, technical debt is something we acknowledge and actively address.
We also maintain a vigilant eye on error logs. We treat exceptions as first-class citizens in our development process. We understand that transient issues can crop up. In such cases, we introduce resiliency policies. An example is leveraging the Polly library. These policies help us handle temporary errors gracefully, ensuring that our software remains robust even when faced with hiccups.
When we encounter real business exceptions, we take a different route. We handle them meticulously, leaving no room for ambiguity. This includes comprehensive test coverage to verify that the issues are addressed and don’t resurface.
To further enhance the quality of our code, we’ve incorporated continuous code inspection into our workflow. We utilize tools like SonarQube and Kiwan, which offer invaluable benefits:
- Measuring Quality: These tools provide objective measurements of code quality, giving us a clear picture of where improvements are needed.
- Recommendations for Improvement: SonarQube and similar tools offer detailed recommendations on what to fix and how to fix it, guiding us toward more robust code.
- Estimates: They even provide estimates of the effort required to resolve identified issues, helping us prioritize and plan effectively.
- Progress Tracking: Continuous inspection tools allow us to monitor our progress as we go, ensuring that we maintain high code quality throughout our development journey.
Our zero-error policy isn’t about avoiding errors at all costs; it’s about confronting them proactively and methodically, ensuring a resilient and reliable software system. With continuous code inspection tools, we’re equipped to not only handle exceptions but to prevent them in the first place, maintaining the highest standards of code quality.
From onboarding to clean code: paving the way to business success
Proper onboarding is essential, especially when it comes to understanding the internal intricacies of a system. To facilitate this process, we provide step-by-step guides, transitioning from bug fixes to production. We also utilize artificial user stories, simulating internal workflows. We guide individuals through domain complexities. It’s crucial to comprehend that the code itself is an honest source of information, and our developers will gradually uncover its intricacies.
In the world of software development, maintaining the health and quality of your codebase is a paramount consideration.
However, there are situations where customers may not prioritize this, perhaps due to budget constraints. In such cases, it’s crucial to assess the situation and make the decision to either forge ahead with craftsmanship or to step back. Craftsmanship is an investment, and it’s not for those who are struggling to make ends meet.
Clean code is a universal principle that holds, regardless of your specific interpretation. Additionally, the power of a ubiquitous language, a term from Domain-Driven Design (DDD), cannot be underestimated. Aligning the business and implementation layers with a common glossary and shared definitions is fundamental for success.
For us, ensuring clean code is a significant win, but we’ve gone further. We’ve introduced a Domain Specific Language (DSL) to certain parts of our system, making them more accessible and readable, akin to English.
Moreover, we’ve educated the business side on how to effectively use version control systems like Git and how to engage with Pull Requests (PRs). This approach fosters collaboration, as they either publish their changes via PRs or, more commonly, review and approve PRs that are implemented following the practices I’ve described.
In essence, our focus remains on addressing business concerns, ensuring a harmonious blend of pragmatism and craftsmanship in our software development journey.
What to keep in mind
We understand that developers often pursue the latest trends in frameworks and libraries. Here’s the perspective: allocate approximately 20-30% of your time to stay updated with these trends, while dedicating the majority, around 70-80%, to comprehending your application’s domain, the associated business requirements, and fundamental things like architecture, patterns, best practices, etc. Why? Because that’s where you can create substantial value. Flashy frameworks that are in vogue today may rapidly become obsolete.
As a parting thought, embrace the Boy Scout Rule in your coding practices. Just as a scout leaves the campsite better than they found it, strive to leave the codebase in better condition than you encountered it. Make small, incremental improvements over time. This commitment not only fosters a sense of collective responsibility but also ensures the long-term health and sustainability of your codebase.
These recommendations are grounded in common sense and can work wonders, whether you’re starting from scratch or working with an existing codebase. Moreover, it’s essential to recognize that the term “legacy” often carries a negative connotation, but with the right approach, even legacy systems can be successfully revitalized and maintained.