Getting to Know the Feature
I start with reading the whole story and looking through the designs, to load all the context into my head. Then I go through the story again, but this time listing everything that needs to be done.
And I do mean everything, not only the implementation pieces.
Need to add a copy to the translation tool — list it. Ned to talk with other teams about some integration details — list it.
Entries like “load data” are good for now. I worry about all the details later (fetching data from the API, caching locally, handling errors, etc.).
In a perfect world, the user stories that we pick up to work on should be small enough to result only in a few points on the list. Unfortunately, this is not always the case in the real world.
Making Decisions (But Only Those You Have To)
Then, with all the high-level requirements listed, I decide on some general details of the solution.
Here I choose which navigation components I want to use (in the current Android project, we’re using Activity or Fragment, depending on the case) or which module the code should go into (Gradle modules in our case).
I make only the big decisions up front and delay the rest as long as possible. This way, I can make better decisions considering all the additional details I find on the way.
If I’m not sure which approach to choose, I ask other team members for their opinion — it's better to have the discussion now than during the code review, when the coding is done.
Another option is to quickly code a rough implementation and have the discussion around a “work in progress” pull request — having a specific example makes the discussion easier.
Doing One Thing at a Time
With all the necessary decisions made, I'm creating a feature branch for the whole story. I name it “OA-5” (we're using JIRA issue keys as branch names).
Next, I pick ONE item (i.e., load data) from the list and create a sub-branch to host the implementation, e.g., “OA-5_load_data."
How do I decide which item to implement? In general, I focus on getting the working MVP first and then extend it with the more complicated flows. The faster I can test something end to end, the better.
The second criterion I use is to start with the architectural layer that dictates the rules:
- I would start from a layer that shapes other interfaces (domain layer in our case), or
- if I need to integrate with an unknown API that is already implemented, I would start there (note: writing exploratory tests is a great way to get to know an API).
Other than that, I pick whatever I feel like implementing at the moment.
Breaking the Subtask into Smaller Pieces
On another list (let's call it a sublist), I note smaller steps that need to be done to complete the “load data” item. The sublist doesn't have to be complete for now.
When new things pop up, I jot them down to deal with them later. This way, I can always focus on working on one specific sublist item.
I try to keep those items small enough so that they are easy to implement. For example, "load data in viewmodel" covers only getting entries from the interactor, for now.
No need to worry about error handling, showing progress, or the way that data is displayed to the user. All of that will be covered in separate steps.
Using TDD helps me effectively work with such small items because I can test them in isolation without launching the whole app.
Keeping the Code Simple
When writing code, I try to keep it as simple as possible (following KISS and YAGNI), focusing on the current requirement and extending it as needed.
This approach helps me avoid over-engineering.
When a refactor is needed to accommodate additional requirements, it’s safe and pleasant thanks to the high unit test coverage.
Every time I have confirmed that something works, I make a commit. When I complete an item, I move on to another one on the sublist.
Making the Code Review Easy
I follow this pattern until I'm out of items on the sublist.
Then I prepare a pull request from the sub-branch to the feature branch: “OA-5_load_data” ->“OA-5.” This way, reviewers can get through all the code in smaller pieces that are easier to digest.
Before actually creating the pull request, I read through all the changes. Such a pre-review step makes the code review process easier. Most typos and styling issues are caught here and other developers don't have to point them out.
After merging all the pieces, I create another pull request — this time to the development branch: “OA-5” -> “develop.” In this one, reviewers can only skim through the code looking mostly for leftover developer tweaks (unnecessary logs, navigation shortcuts, etc.), since they've seen all of the code before.
Once this pull request is merged, the feature is ready for the QA.
Adjusting the Process to Your Case
This is the full version of the process that helps me implement complex features in a finite time. Breaking big features down into small and well-defined tasks helps fight decision paralysis.
Luckily, some stories are broken down on the refinement meetings and the full version of the process is not necessary. In such cases, I end up using parts of it, for instance, I skip creating sublists or WIP pull requests.
As you might have noticed, the process builds upon the TDD described by Kent Beck in “Test-driven Development: By Example.” This book, which I highly recommend, definitely changed the way I work, even though years passed before I started actually writing tests.
Related articles
Supporting companies in becoming category leaders. We deliver full-cycle solutions for businesses of all sizes.