Complexity stands in the way of application agility. At its core, a complex system creates a fear of change. Once that fear takes hold, organizations and engineering teams mitigate it by adding layers of processes to ensure quality. This results in a development culture where code is only ever added, not changed or removed, to avoid the risk of unintended bugs. Over time, this leads to increasing complexity in both processes and systems architecture.
WHAT CREATES APPLICATION AGILITY?
To achieve true application agility, we need a combination of automated tests, automated builds, clear APIs and contracts, robust DevOps pipelines, and responsible use of technical debt. Automated tests and builds are fundamental; they provide the safety net that allows developers to make changes with confidence. Clear APIs and contracts minimize dependencies between systems, enabling teams to work independently and efficiently. Robust DevOps pipelines ensure that the entire development process is streamlined and reliable. Lastly, technical debt should be managed responsibly, with a clear plan to pay it down over time.
Despite knowing what is needed for application agility, many organizations struggle to establish and maintain it. The main culprit? Legacy applications. These systems, core to driving business value, are where the majority of IT budgets are focused. They also hold the potential for the biggest performance gains and the largest cost savings. However, the tools and techniques that keep software soft and agile work best on new applications, where the code is still simple and easy to change. In contrast, legacy systems have often been complicated by suboptimal practices and organizational pressures over time.
RESTORING AGILITY TO LEGACY APPLICATIONS
To restore health to legacy systems, we need a structured approach. First, we must understand our product structure. Software exists to bring value to the customer by supporting a product. Therefore, it needs to be easiest to change within the product’s boundaries. This requires reworking the system architecture to align with the product architecture. The goal is to increase independence and minimize dependencies so that a product enhancement can be built, tested, and deployed with minimal coordination with other products. Where systems must interact, clear APIs, contract tests, mocks, and fakes allow teams to govern interactions without the need for manual coordination or testing.
The next step is to determine the level of refactoring required. This depends on the complexity of the current system and the degree of misalignment between the system’s agility and the product’s demands. Core functions that change infrequently may only need to be stabilized and encapsulated so that other functions can easily integrate and leverage the core. More strategic functions might need to be extracted and componentized so changes can be more easily and reliably made. Where rapid innovation is required, the investment may be warranted to rewrite or rebuild the code to create maximum agility. Refactoring legacy code is often a balance between immediate business needs and long-term technical improvements.
Prioritizing the backlog of remediation work based on business value is crucial. Not all code needs immediate attention and what does require attention can’t all be done at once. By focusing on areas that offer the greatest return on investment, we can make meaningful progress without overwhelming the engineering teams.
Finally, uplifting software engineering practices is essential to maintain and enhance simplicity. This involves adopting modern development practices such as test-driven development, continuous integration, and continuous deployment. By embedding these practices into the development process, we can reverse the cycle of increasing complexity and make the software easier to maintain over time.
PRACTICAL EXAMPLE: E-COMMERCE TRANSFORMATION
Consider the case of a large e-commerce client. Initially, this client struggled with transitioning from dealership sales to e-commerce due to the complexities of their legacy systems. They quickly ramped up their e-commerce capability but found that adding more teams and vendors slowed them down, rather than speeding them up. Quality suffered, and lead times increased.
To address this, we focused on reducing dependencies and simplifying their system architecture. Here’s how we did it:
- Creating Clear APIs: We established well-defined APIs between different components of the system. This allowed teams to work independently without constantly needing to coordinate with others. For instance, the order processing system was decoupled from the inventory management system through a clear API, enabling each team to develop and deploy changes without waiting for the other.
- Automating Tests and Builds: We implemented automated testing and continuous integration pipelines. Automated tests provided immediate feedback on the impact of changes, reducing the risk of introducing bugs. Continuous integration ensured that all changes were integrated and tested regularly, catching issues early.
- Encapsulating Dependencies: We encapsulated dependencies by creating clear boundaries around different parts of the system. For example, we encapsulated the customer management system from the payment processing system. This meant that changes in customer management would not directly affect payment processing, and vice versa. Each system could evolve independently as long as they adhered to the agreed-upon API contracts.
- Reducing External Dependencies: We minimized dependencies on external systems by introducing mocks and fakes for external services during development and testing. This allowed teams to simulate interactions with external systems without relying on their availability or stability. For example, we used mocks for the shipping service API, enabling the order processing team to continue their work even if the shipping service was unavailable.
- Managing Technical Debt: We prioritized and addressed technical debt systematically. Critical areas that impacted performance and maintainability were refactored first. By focusing on high-impact areas, we improved the overall system health without overwhelming the development teams with too many changes at once.
As a result of these changes, we saw a significant improvement in software quality and delivery speed. Automated tests led to a 70% decrease in defects, and the reduction in dependencies resulted in a ninefold increase in cycle time efficiency. The teams could now work more independently and efficiently, leading to faster deployment of new features and enhancements.
CONCLUSION
In conclusion, restoring application agility to legacy systems is both challenging and essential. By understanding our product structure, reworking system architecture, determining necessary refactoring, prioritizing remediation based on business value, and uplifting engineering practices, we can reverse the cycle of increasing complexity. This approach not only enhances the quality and reliability of our software but also supports long-term agility and growth. Successful application modernization is about making informed decisions that connect technical changes to business value, ultimately making our software easier to maintain and adapt over time.