A lesson learned as a junior engineer
After about a year being a full time software engineer I was finally feeling confident in my skills. I thought I could handle any task, big or small. I picked up a new feature that was for implementing the use of some new API endpoints and paginating the results on the front end. I was eager to get started until I realized, I wasn’t really sure where to start. So instead of digging straight into some code, I decided to ask for help from one of the senior developers on my team. The task ended up being so large and taking so long, that we were pairing for the next month.
Some quick context before I go any further, I worked on a front end team for a single page react app that uses redux and typescript. We keep track of tasks and subtasks using the Jira ticketing system. Bitbucket is the version control system we used and we would create pull requests for at least 2 members of the team to review. Manual testing of each feature was the last step before merging to master.
After reading and re reading the description on the Jira ticket, the senior engineer and I broke the feature up into 6 separate subtasks. The plan was that each subtask would be merged into a feature branch which, after full completion of the feature, would be merged to master. Each subtask being merged to the feature branch would have its’ own PR and be reviewed by members of the team. Prior to merging the feature branch to master, the entire feature would be manually tested by a member or members of the team.
The feature was one single Jira ticket with about 5 subtasks:
- Create new methods for the API calls
- New actions to call these methods
- Corresponding selectors and reducers
- Modify the connected components
- Modify presentational components
- Load more actions, state, and presentation
Although each subtask was its’ own PR that was reviewed individually, the end result was still one massive feature PR that had to be completely reviewed again, fully tested and merged to master. In the time it took to finish this feature, there were also two other large refactors went into master and the subsequent rebase/merge had many conflicts to sort through. These conflicts took about one full day each and definitely would’ve been worth avoiding if possible. Testing was also a hassle since many pieces of the app were touched and needed regression and unit tests.
Some yellow flags that this feature should be broken up more:
- There’s a pagination task.
- The addition of new subtasks. By the end, there were at least 3 additional subtasks added, there kept being more and more pieces of work being added.
- Other tickets that included refactoring were also on the jira board and in the same timeline as this work.
- The planning process showed the complexity of this task but each chunk of work was broken out into a subtask and not their own ticket.
There are a few things we could’ve done instead. Create a separate ticket for adding pagination, pagination alone could be a single feature and could’ve been broken out into its’ own ticket to be done after implementing the API calls. Instead of creating more and more subtasks, reevaluate the work to be done and create new tickets for slices of work. Since there were other features with large refactors included, the refactoring portion could’ve been separated out into tickets that could be started after this large feature. If during the planning or grooming process, there are many subtasks, it doesn’t hurt to create new tickets for those pieces of work instead of trying to fit everything in.
This single feature resulted in nearly everything that I learned NOT to do. One single massive PR for the team to review, an in depth and very involved test plan, and so many moving parts in what was supposed to be a single task. Through this experience, I did pick up some good habits for planning ahead. Beginning with some kind of planning process is always helpful. This may only be 5 minutes of brainstorming for some tickets. During the planning process the complexity of a feature will present itself. There are three chunks I like to break the planning process into; Identification, Testing (manual testing, regression testing, possible bugs etc.) and Implementation.
What to identify:
If you are new to the code base or the code base you are working in is monolithic, identifying the places in the code that need to be modified is a good place to start. For example, in a redux application, you may want to start with the action creators and reducers and work your way down into feeding the data into components with selectors and then back up with the components dispatching other actions. Identify places where you might need to reach out to others ahead of time. Identify patterns in the code that may be useful for the current task.
What work is ahead? Sometimes tunnel vision in unavoidable but it’s good to keep track of the other features team members are currently working on or what new work is coming. Since we did not merge small chunks into mast periodically, each time there was a subsequent refactor, we got set back because of merge conflicts. By looking at the work ahead and communicating with the team, we would’ve known to either merge small chunks or wait to do the other refactors until after this feature was finished.
Testing:
Be conscious of the testing process for a feature. If testing will be lengthy and difficult for the tester, it may be a sign that the feature is too large. You want to keep a ticket/PR, to a single responsibility that’s easily tested. Ideally, a simple PR an a simple test plan go hand in hand. If the test plan is becoming lengthy seemingly daunting, it might be a signal to break the work up more. Thinking about what it will take for another engineer to test this feature should demonstrate whether or not this is a complex change or a simple one.
How to implement:
Write out some pseudo code. If you’re not sure what direction to go in for how to implement start writing out a couple functions. Think about whether you want to start with the component and work your way up to the actions or with the data and work your way down to the component.
If you’ve reached a point where you want to break a feature into slices that are more manageable, there are many ways to do this.
1. Feature Flag
The first task would be to create the feature flag in the project. Then, you can keep building up the feature you want and keep merging these pieces into master without worry of displaying anything to the user. The last task would be removing the feature flag once everything is ready and tested. Take the vertical slice of work and if it can be broken up into horizontal layers of work, those can either be subtasks or new tickets
2. Feature Branch
Subtasks merged into a feature branch that will be merged to master. Review and test each sub branch before merging into the feature branch and then test all of the changes together once they are all merged. This doesn’t always end as badly as our case.
3. Split task into small slices
Each slice can be their own ticket, tested individually and merged to master without introducing any breaking changes. For me, it’s important to break things up when possible. The need to break up a ticket into chunks isn’t because someone made a mistake in the planning process either. Although, it is best to catch these big tickets while in some kind of grooming meeting so have the benefit of the team to brainstorm the split of work.
Conclusion
Sometimes, large features are unavoidable and by the time it’s identified it can feel like it’s already too late to turn back and separate out the work. It’s important to remember that all engineers have experiences with hard problems that they did not know how to solve right away. Breaking these problems into smaller, more manageable chunks is one of the many different strategies for tackling them.