2. How We Execute - Ways of Building
Image by Kyle Hale
As covered in Ways of Working, this blog series is an attempt at describing my approach to the topic of Execution. If you got here after reading the first part, thank you for following along. If you haven’t, I would recommend checking it out if you are interested in reading about ways of working. In this blog post, I will touch on some themes around ways of building. So let’s get right into it.
Ways of Building
Bringing others along
As you grow as an engineer you will find yourself building deep expertise in some product or feature. The more expertise you build, the more you want to influence design choices in the area. As you help make decisions, you end up owning the feature and are held responsible for critical deliverables around it. As these deliverables accumulate over time they end up taking up a lot of bandwidth making you the single point of failure. In a nutshell what’s described here is a perfect recipe for high stress, one that is simply not sustainable in the long run.
Times like these are worth pausing to consider the short and long-term implications. Setting aside some time to bring someone else along on these projects considerably eases the burden. Transferring knowledge with a teammate will also distribute the responsibility within the team. Though it’s easy to say these things, we often fail to recognize these tradeoffs in the moment. For example, when assigned a bug in a familiar project, you end up thinking that it is less of a hassle if you just fixed the issue instead of helping someone else to do it.
When done right, sharing ownership over features frees up a lot of time and opens up possibilities around optimizing for the health of you and your team. You could 2x the output of deliverables by parallelizing streams of work or use the time to bring a third engineer along or pursue some more impactful projects.
Finally, a note on handing over responsibility. It should come as no surprise to find out that every individual on your team is different. So it’s unfair to expect them to work and think like you. People have varied styles and approaches to getting stuff done. We make a big deal about how uniquely different someone is during the interview process to immediately forget about it when working together. Work on understanding the strengths of individuals and how best to leverage them to get things done.
Focusing on the outcome rather than the approach allows a measure of freedom in determining how the team goes about solving a problem. This does not mean that mentorship goes out of the window. You still have a responsibility to coach team members as they work on interesting challenges. Be available to provide relevant context, ask the difficult questions and communicate the desired end goal.
Optimizing too early
By nature, engineers like to build perfect systems that try to pass the test of time and survive any change in requirements. In reality, this is rarely ever true as we spend the majority of our time working on code that is imperfect and always needs some level of refactoring to account for new use cases. As time goes by some of this code becomes what we call “legacy”, as the engineers who once wrote it no longer works at your company.
It is very difficult to build systems that last. When considering a system that accounts for multiple use cases we should always focus on one use case at a time - preferably go with the one with the highest priority. There is a subtlety here that needs to be elaborated on: do not ignore the other use cases, account for them but not actively build for them. Completion of the first use case may well present a better approach to accommodate the second. Since you haven’t built for the second, there is some flexibility in terms of adjusting the approach. As new use cases are added you will uncover the nuances of the approach including the edge cases, dependencies, roadblocks, and so on.
Situations where early optimization leads to a difficult or unmanageable codebase is fairly common. A few examples include:
- Overly generic abstractions that slow down individual use cases - say you’ve been tasked with building a robust networking library that supports Wifi, Bluetooth, and so on. You spend a considerable amount of time accounting for the nuances of each like pairing mechanisms, latency, package structure, and so on. Now when Wifi is implemented, the implementation does not slot in seamlessly prompting a significant tweak to the approach. Next when Bluetooth is added, the nuances of the pairing process breaks the design causing regressions to the Wifi use case and maybe even slowing it down.
- Products shelved early with an over-engineered technical stack - say you spend months on end building an overly extensible component framework with a ton of functionality. The product that is built only ends up using only one function before being shut down due to low usage.
- High cost flows within a feature that cater to a very small percentage of the total audience - say a user during beta testing runs into a particular network error condition. You end up using up a significant amount of bandwidth figuring out the problem and building a user flow to realize that the error is only encountered by 1% of the total user base.
- Libraries that are only used once and abandoned without great adoption - this one speaks for itself. You integrate a shiny new library to a feature only to realize that the team is not completely on board or the cost to migrate the remaining features is too significant.
Optimization is not a bad thing but we need to use our discretion to choose situations that warrant it. When you do optimize, make sure there is sufficient justification to reinforce the reasoning.
Getting comfortable with failure is often an undervalued trait for an engineer. It is never the expectation for an engineer to be perfect and never make mistakes. Really good engineers are ones who have made many mistakes in the past. Success is measured by how you handle, react to, and learn from these failures.
Fostering an environment where it is okay to fail creates a safe space for everyone to talk about it and ask meaningful questions to learn from, a personal favorite being:
What would you have done differently if you were to start again?
Using retrospectives is an effective way for us to take note of what went wrong and create a plan for the future. The outcome of a retrospective is to have a set of actions to either prevent unfavorable outcomes or reduce the impact in the future. Retrospectives also help establish checks and balances to trap problems earlier without having them spiral out of control.
Of course, we should not set out with the intention to fail, but instead to be comfortable when failures do arise. We need only to use our experience as engineers to foresee situations where we might fail and avoid them in the future. Seeing failures early on becomes easier with more experience, in time you will be able to recognize these patterns early and overcome them skillfully. The more we normalize this feeling of failure, the greater our willingness will be to pursue riskier streams of high-impact work.
With that, I would like to close out this blog post that talks about my biggest themes around building for Execution. Some of these themes may seem fairly obvious but we often fail to recognize them in the moment. So if they serve as good reminders and you would like to read more I would suggest checking out the third part that talks about ways of planning.
How We Execute - Ways of Planning
Feel free to get in touch with me via LinkedIn
If you find this kind of work interesting, come join us!