Everyone loves new big features. Stakeholders want them as they make their product more attractive to the users. Which by definition means that the features are designed for users to like them. Developers enjoy coding and big features usually have a lot of coding to do, so a perfect match, right?
Yet with a big feature usually comes big complexity. The changes tend to go over thousands of lines of code, which soon become difficult to keep in your head. A developer need not only to find all existing functionalities that need adjustment, think through all edge cases, limitations and side-effects but also deliver a reviewable result to his fellow colleagues for code review. How can one achieve all of this and not go insane?
I would like to share my approach, which is just one of a multitude out there. Here are the steps I follow.
#1 Understand the feature
Make sure you really understand the feature. Read the requirements very carefully, take your time, maybe even sleep on it and definitely ask all the questions you might have. Usually it doesn’t cost anything just to ask, quite the opposite in fact as making a wrong assumption at this step, might mean that all further work would have to be scrapped.
#2 Know initial conditions
Familiarize yourself with the status quo. Analyze all the relevant existing functionality, code organization and database structure. You need to know initial conditions in order to prepare the roadmap for implementation.
Once you know what you have and what you need to achieve, all that’s left is to get from state A to state B. The question is how.
#3 Design the database
I always start with the database design. Having a thought out data model is usually half the battle won. At this step I would plan the tables, table structure, column data types, relations between tables and consistency constraints. To do that I tend to draw Entity-Relationship Diagrams. I am a person who doodles all the time, while on a call, in a meeting or just if I have a writing utensil in front of me. Thus drawing DB diagrams is a highly productive type of a doodle. I compare the different designs, note their pros and cons, and finally choose the one that fulfills all my requirements.
#4 Plan code structure
Plan the code organisation, the abstractions and interfaces without going into implementation details yet.
#5 Brief the team
I compile my proposed database design and high level code structure into a technical specification — a document that outlines how you are going to address a technical problem by designing a solution for it. Then I would share the spec with my teammates to get their opinion and advice on the approach. Familiarizing them with the project now will make future code review easier and more fruitful.
#6 Split implementation into steps
After having a birds eye view of the final solution, plan how to split the implementation into small increments, so that each one is self consistent, if possible independent of the other changes, and small enough to be reviewable.
Sometimes it is possible to develop a new feature, while the old solution remains in the code for a while. This is the easier case and one can just do as indicated: implement the new feature step by step first and remove the now obsolete feature second, again step by step. During the transition period some data or logic duplication might be necessary, but I find it totally acceptable as a temporary complication.
In other cases the old functionality cannot co-exist with the new one, for example when the features logically contradict each other. Then the transition is more complex as one has to demolish the existing feature and build a new one at the same time. In this case small increments are more challenging, but still possible.
#7 It’s coding time
With the power of
git managing all the increments and their dependencies is almost a piece of cake. I would create a base branch for the feature as a whole and a branch for each increment, followed by a Pull Request per increment for my team to review. The approved Pull Requests would be subsequently merged into the base branch respecting the order of dependencies, getting at the end a feature fully developed and reviewed.
The big feature is now implemented and no developers were harmed in the process.
Divide massive changes into smaller chunks. It might cost some extra time in the planning phase, which will be compensated by faster development due to smaller scoped increments as well as easier and more effective code reviews.
- If you feel like an increment gets too large, take a break and start planning its scope from scratch. I noticed that as I get more tired, the increments become bigger.
gitworks wonders for rewriting history, choosing the commits you need and thus is a great help in managing branches for the incremental changes. Especially worth mentioning are interactive rebase with
git rebase -iand
git rebase --onto, which “transplants” commits from one branch to another (for more details check https://git-scm.com/docs/git-rebase).
- Do not skip writing tests while doing incremental development. Having tests written together with the logical changes helps to catch bugs and makes the history of changes more informative.
- You can use feature flags if you are replacing old functionality which would allow a clean switch over.