Long-living feature branches — An anti-pattern
A few years back, we were working on a Javascript heavy web application which had built using backbone.js. We found that we were loading all the Javascript at once than applying lazy loading. The loading affected the user experience because of the high page load time and wanted to fix that.
We decide to use Require.js’s AMD [Asynchronous Module Definition] for lazy loading. We had to change our Javascript files. And we also needed to apply specific techniques to the external libraries [like JQuery] to support AMD. We started on that and realized that we are getting into a black hole as we had to change the entire JS code literally. We didn’t react to our learning. And it took around two weeks to support AMD completely.
We saw changes in the application performance. But as a team, we felt terrible because We couldn’t work on anything else during this time because:
- It was taking longer than expected, so the entire team jumped on trying to finish it faster
- During this time, the application was somewhat broken, so deploying something to production was not possible
We thought we were doing continuous delivery, i.e. every commit can go to production, but it was broken this time. We needed to find out how to handle long-running rewriting in a “continuous delivery” manner.
Learning from experience
We came across a similar situation in the recent past. We were building an application which allowed the user to create custom designs, which they can print on their T-Shirts or hoodies. We used RGhost for creating the designs in PDF but later realised that because of certain constraints with RGhost it’s better to use LaTex.
We didn’t want to repeat the earlier mistake. We decided to rewrite in small batches. We wrote an abstraction layer for the PDF Generation somewhat like as follows:
And we change one function by function to the new implementation. We added a feature toggle too, to switch to old implementation in case we find a problem in production.
The approach worked well because we could fine tune the new implementation with the learnings from production. And as it was in small batches and it was okay to complete the port in a few weeks time as there was no dependency on this.
Branch by Abstraction
The above technique is known as Branch by Abstraction, i.e. introduce an abstraction layer on top of the existing implementation.
Add another abstraction layer for the new implementation and redirect to it as it is ready.
You can visualise Branch by Abstraction with the following example given by Paul Hammant. Consider changing the wheels of a car with the conditions:
- Mechanics must be able to work on the upholstery, engine, etc. simultaneously
- The car must be drivable after every change.
It works in large scale too. This case study talks about two significant changes implemented using, Branch by Abstraction, i.e.:
- Moving from iBatis to Hibernate [famous ORMs in the Java world]
- Moving from Velocity and JsTemplate to JRuby on Rails
Every commit should be ready for deployment
Mainline Development and Feature toggles help you to achieve this mantra to a great extent. But the question is how do you handle significant refactorings or rewritings?
Use patterns such as Branch by Abstraction or Expand Contract Pattern to deploy large refactorings or database changes continuously in small batches.
Yes, it requires the discipline to use abstractions appropriately. But why not, if the same help for better maintenance.
Updated version of an earlier post published at www.multunus.com.