Having been a developer for 10 years, Colin jumped over that fence developers love to chuck work over and became an SDET with interests in automation, the visualisation of test report data and value-driven development practices. Outside of work, Colin loves augmented reality, Pokemon Go (for which he helps run the Leeds community), death metal and board games.
The term ‘legacy code’ is used to define any code for which the original context of its writing is no longer available. There are methods to combat this contextless code, namely to re-add the context via documentation and unit tests (themselves a form of documentation).
When taking on an existing project, it's not just working with legacy code that can impact a team's ability to deliver. Other artefacts used in software development that lack context can cause blockages and shape the techniques available to the team to deliver value.
A year ago I joined a project in my first leadership role where the codebase and working practices were already established. As the team and I got adjusted to these new ways of working, it became clearer that there were some improvements that would make life far easier for the team.
Part 1: Legacy User Stories
During initial backlog review we had taken the Stories as being ‘Sprint-ready’ as it seemed to be a continuation of the previous development effort. Unfortunately, as we worked through them, we started to realise a few things:
The Stories were too small. You couldn’t release a working functional change in one Story.
The Stories were highly dependent on each other. Because they were too small, final functionality was defined by several interdependent Stories.
Acceptance criteria weren’t effectively defined. While the Stories themselves contained a lot of acceptance criteria much of it was made up of regression scenarios that were repeated across several Stories.
There wasn't a consistent narrative across Stories. Descriptions of what the final functionality needed to do were inconsistent and sometimes contradictory.
Not being able to implement a functional change in one Story has an impact on the team, as this often leads to a build-up of Stories waiting for test. This puts pressure on the testing team, and any re-work that comes out of the testing process involves context switching for the developers.
The effects of the build-up of Stories in test also means that the code changes sat waiting for test are at risk of becoming stale, as the main development branch could have moved on considerably from when the initial change was branched off.
Part 2: Releasing code in a multi-team project
Feature development for the codebase is split across cells that are merging into the same repositories and branches, with any team having to cut a release from develop at any time.
This way of working means that in order to build a feature increment over multiple Stories we need to be able to merge into the main development branch with a mechanism to disable the increment's functionality until it's ready to be made available to the users.
A common pattern for achieving this is to use 'feature toggles', essentially a conditional that controls the availability and execution of the developed functionality.
Faced with the issue of how to deliver small Stories in a widely shared codebase, without rewriting all the Stories, we had a whiteboard session and decided to implement feature toggles. In addition, we would abstract out as much shared functionality as possible into external modules, so we could deploy versions with complete functionality when we needed to.
Best is the enemy of good
When we actually sat down to implement the feature toggle and external module approach, we started to realise something we hadn’t factored into our whiteboard exercise – the codebase was in need of a little TLC before we could realise the approach we had hoped to have taken.
In order to implement feature toggles for one piece of behaviour, we’d be adding about six times as many conditional statements as we had expected, increasing the complexity of the code immensely.
Additionally, the external module approach wouldn’t work. Previous design decisions had included committing the codebase’s external dependencies to version control, adding a layer of version management to what should be a simple process.
We had to come up with a means of unblocking the development team, while also ensuring that incomplete functionality wasn’t accidentally released when another team wanted to release something they’d completed work on.
I’d initially rejected the idea of a long-lived feature branch as a solution to our problem. It would have been a hassle to handle the rebasing against the main development branch whenever another team merged their changes into it.
However, faced with the constraints of the codebase, I had to side with the approach. It’s actually had a very positive impact on the team’s ability to get these smaller Stories delivered into a branch we can integration test in, subsequently merging changes into the main branch, ready for UAT.
This is very important because the inherited codebase lacked an effective level of unit and integration tests, meaning there was a heavy reliance on manual testing at the UAT stage to catch any regressions.
As the use of an ‘Epic branch’ has sped up development immensely, we actually don’t face that many merge conflicts. The Epic branch only lasts a few days or so at most.
The increase in velocity also means that the continual improvements we make during feature development are added to the codebase more often. This in turn moves us towards our goal of using feature toggles.
Part 3: Adding context back to the User Stories
It wasn’t until we started to Sprint that we realised the issues with the User Stories being too small, and at that point we were locked into delivering them, which proved too difficult.
Initially this was stressful - I was the person responsible for ensuring the cell could deliver. I started to realise that the bottleneck caused by the Stories was actually a good way to visualise the issue, and worked with the client to improve the User Story writing process.
We collaborated on how to solve the issue of Stories being too small. Instead of aiming to make them small enough to be ‘one pointers’ we could size the Stories around the smallest releasable functional change to implement the Epic.
This new approach to Story sizing has meant that, while it looks like we’re taking less points into a Sprint, we’re actually delivering more functionality. It’s no longer stuck on a bunch of independent branches and we can merge directly into the main development branch.
We’ve also now got to a point where our backlog has a bunch of releasable independent stories. These can be re-prioritised, which has allowed both the Delivery Manager and the Product Owner to start planning Sprints to meet key points in the programme’s roadmap.
This project has been a great experience, giving me many opportunities to grow and has taught me, and the team, some important things:
While we often want to follow best practices, many times there are constraints that mean we have to compromise.
Using less-than-best practices as a temporary fix is fine. You can work to remove the constraints and move towards best practice.
It's not just legacy code that can cause problems when picking up an existing codebase.