Some shared thoughts on dealing with technical debt in Agile development teams
Technical debt is unavoidable in software development. Due to the complex, adaptable nature of software development, we work with a lot of unknowns. This means that even with infinite planning, we will always be making some assumption of how our software will be used in the future. These assumptions, when wrong, become Technical Debt.
Technical Debt also consists of external changes affecting our ecosystems. Changes to technologies, updating integrations and _best_ practices mean we will always have a backlog of technical changes that we will need to address.
Agile and Technical Debt
Agile development aims to limit the risk of these assumptions. This is done by making small, incremental changes to solve some customer pain point. These small changes, with lightweight and continuous (instead of up front) planning, means that we are creating Technical Debt with every iteration. Some practitioners will refer to the known Technical Debt as Good — this is when we make certain tradeoffs. Martin Fowler explains this concept a lot better than I ever could in this post. The Technical Debt we want to avoid is the type that slows down our ability to respond to change.
The misconception some people may have of Agile techniques or frameworks is that all Technical Debt is bad and thus, a second-class citizen — banished to the bottom of the backlog over new and exciting features. This misconception is pretty dangerous, but in my experience, extremely common.
“Take a well understood reference feature and pass it through your product development system. Without managing complexity / refactoring / automation, it will take longer to complete this feature each year. Even if your team remains the exact same.” — John Cutler
The above quote sums up the effect of ignoring Technical Debt in your development workflow for extended periods of time. Responding to change is one of the core Agile Values. Can you see how a build up of Technical Debt hinders your ability to respond to change effectively after some time?
It’s no wonder then that mentioning the term refactoring sometimes results in groans from PO’s and exasperated sighs from developers. Non-technical POs struggle to understand why developers can’t do it right the first time. Developers tend to either slip in refactor tasks to get around this, or they too avoid Technical Debt entirely.
So how do we address Technical Debt while also managing to fulfil our customer’s desires by focussing on delivering value?
Focus on Technical Excellence
I feel the answer here has been in the origins of the Agile Manifesto the whole time. The original authors were technical stalwarts who made sure to include principles that highlighted the importance of technical excellence and good, pragmatic programming practices.
Continuous attention to technical excellence
and good design enhances agility.
Frameworks like Scrum are purposefully light on technical practices to allow the team’s to find their own. XP however, addresses these in a lot more detail — see Refactor Mercilessly. Like Agile planning and delivery, we need to focus on addressing Technical Debt continuously and in small, iterative increments.
Keep it small and iterative
All developers are familiar with that feeling of dread when you receive a task that needs to make changes to that Class. You know the one — no unit tests or code comments, methods that are a few hundreds lines long and a variable naming structure written in Klingon. We’ve all come across, and contributed to these cases at some time in our careers.
Many of us are tempted to want to block off some sprint time (or multiple sprints) to address these. This isn’t always necessary. By keeping changes small and limiting risk, we can implement the simplest technical solution to the problem one step at a time.
A practical example of the above horror code is to start with some comments to make the method flow easier to understand. Then start with some minimal risk changes such as renaming variables and/or extracting some functional private methods. This should make it easier to limit risk even further by adding unit tests for basic functionality and known scenarios. Doing this iteratively with small changes means that we will know immediately if we’ve changed the core behaviour, or introduced a new bug. This quick feedback means a lot less risk than blocking feature development to slowly refactor the code in isolation.
Dealing with Technical Debt
We’ve tried including continuous refactoring and technical improvements in our Sprints in two ways.
First, we started by making our larger refactoring tasks visible by adding them to the backlog. Our Development Team worked closely with our Product Owner to help share the benefit of these tasks. Instead of logging every single bit of debt here, we focussed only on the most beneficial to the codebase which would allow us to better respond to change. These tasks were chosen such that every subsequent improvement could be done iteratively in future sprints.
Second, we started planning “house keeping” time in our 2 week iterations. During Sprint Planning, our development team makes sure to cater some time for cleaning up some code, adding unit tests and/or making existing code easier to refactor later on. This is not done with formal tasks but is managed entirely by the development team themselves. Cleaning up code, documentation and adding missing unit tests are done continuously.
These changes mean that this team can constantly improve their velocity. Not by working later, not by gaming estimates, but by focussing on technical excellence that enhances their overall agility.