Done is better than perfect

I'm undertaking some mentoring at work with the main goal of becoming a better developer, whatever that means. I've been striving to ship more work without sacrificing on quality, and in trying to do that for the past 6 months I learned a couple of lessons I wanted to write down for future reference.
Disclaimer: the lessons I share in this post ought to be valid for similar circumstances to mine, where focus is mainly adding new functionality to an increasingly large codebase. This context matters, because there is no universally perfect workflow.
Start shipping more work somehow
It's easy to dismiss this as obvious goal that everyone should have, but actually embracing this and trying it out was a big challenge. I used to call myself a perfectionist but I've grown to become suspicious of that word. Mainly because my experience being a perfectionist envolves 2 main characteristics that I now feel are generally unproductive: criticizing one's work and striving for the perfect solution. Let's go through each of these individually:
Approaching your work from a critical standpoint:
This can be useful, especially if you're writing code that is mission-critical. It helps you find issues in your implementation and think of corner cases. The reason it hinders productivity is because often times this exercise if futile. The time spent looking for corner cases and catching them is much better spent taking on a next unit of work. Furthermore, thinking about corner cases when developing new flows is also a bad use of precious time. It's not uncommon for features to get cancelled or significantly changed, and every time that happens the amount of time wasted will be proportional to the time spent looking for corner cases. Thus, focusing on the happy code path will lead to less waste and being more efficient.
For me this was a new concept and taking it on was a pain-point. Corner cases just seem to pop in my head for some reason and I get worked over not addressing them. It made me feel my work was incomplete and bad. So I reached out to my mentor, who suggested I document these corner cases instead, and this was how I was able to follow through. Every time I think of potential failure scenarios, I write them down as code comments. Here's an example:
# fails spectacularly with negative numbers
def fibbonacci(0), do: 0
def fibbonacci(1), do: 1
def fibbonacci(n), do: fibbonacci(n-1) + fibbonacci(n-2)
This keeps the knowledge in the code while minimizing the amount of time actively looking for – and addressing – corner cases.
Striving for the perfect solution
Looking for the perfect solution usually means exploring several ways of implementing a certain part of the code and determining which one is the optimal for a given use case. You don't have to write 3 implementations and choose the best, but you'd consider each of them briefly and try to way in the pros and cons of each before ultimately typing out the winning solution. The main purpose this serves is (in my opinion) performance. This is easy to debunk: an overwhelmingly large percentage of code is "fast enough" such that it does not require any special attention. Sure, it would be good to keep in mind not to write algorithms that have exponential time complexity, but chances are that the code you write every day does isn't too slow.
Yet sometimes you might ask yourself: "Is this code performant?". The answer is surprisingly complex. Firstly because it's hard to even define what is performant code without some sort of baseline reading – which you probably won't have if you're writing a new feature – but also because benchmarking code is a hard thing to do right. At best you spend a lot of time taking readings and end up choosing a slightly faster piece of code, and at worst you write a benchmark that leads you to wrong conclusions and that is an even bigger waste.
My recommendation here is to write code first and measure execution times through existing mechanisms like tracing, metrics and telemetry. These tools help identify the bits of code that are taking too long and help make it obvious where it's most worth optimizing.
Push code more often
I've been trying out a new workflow that involves pushing out code very often. This isn't easy because some times it might not even look like it will make sense to push code, but hopefully by the end of this post you'll see why this can be good.
Getting into the mindset of pushing out code regularly makes several things complicated. I found it even changes the way you develop, the order by which you make your changes and the thought that leads up to that is also interesting.
The idea is that knowing that you need to push often will make you slightly more disciplined in how you tackle things. Let's say I'm taking on a task that takes 3 days to complete. Here are 2 different ways to work on it:
- Work on it for 3 days and push once at the end
- Push code at the end of the day for 3 days, open a draft pull request on day one
The first approach was what I was used to, so I took whichever order felt natural to me to complete the task. However for the second approach I found that to build a decent draft pull request I needed to consider the order by which I did things. This sounds like it might be a slight change, but it ended up changing everything.
Writing a draft pull request on day one means that this first push will contain the main idea of what you're working on. To do this, we need to distill the task down to the essential parts and stub out much of the implementation. It looks weird and feels even weirder. But the end result is that you end up with something that anyone can comment on very early, and you can then submit it for review to check that you're headed in the right direction or that you understood the assignment correctly. For more beginner developers or people that are new to a programming language, this is a great opportunity to open up the stage for a senior developer to chime in with opinions on the implementation.
If you really value growth and learning like I do you are pretty much sold on the benefits at this point. Despite the big shift in thinking and the long period where I felt a bit uncomfortable, I am happy that I gave it a try because I feel it's making me a better developer.
Fake it 'till you make it
I had developed some bad habits from my previous experience so trying this out was hard. I still find it a bit foreign to work this way, to be honest.
But I keep working this way because it became obvious that I ship more work. This is hard to quantify, but I feel I push out more lines of code and I also handle more units of work in the same time frame. It's becoming more and more natural to me everyday and I'm beginning to think this way. Eventually this will become my usual workflow, and there are benefits I'd like to mention.
Although this might sound silly, I get a hit of dopamine every time I finish a piece of work. Being more efficient, going through more units of work than usual has been great for me.
Secondly, I am officially a mid-level developer and so I'm still gaining experience. I suspect that as I progress in my career, there will be patterns and once you implement a type of functionality once, you will be able to do it faster the next time. By becoming quicker to complete units of work, I'm gaining this experience quicker than what I normally would have.
Being consistent
Seldom in life are there silver bullets for problems and goals. Often times what's worth pursuing requires conscious effort and dedication, and this is no different. I've been trying to adapt to this workflow for 6 months and it's starting to become natural to me. This means that there were 5 months where this was very challenging, almost painful. I kept going because I had positive reinforcement and great support and understanding from my team and management. Having done the work, I feel an enormous difference in the way I approach each piece of work, and I have confirmed I am indeed shipping more work.
Now that we have a strategy for shipping more work, lets talk about quality.
Maintaining a standard of quality
Exploring this workflow gave me a headful of concerns, mainly around the quality of my work. Code quality is hard to define, but one metric I use as a rough gauge is the number of review comments I get in my pull requests. It's far from perfect, but it gives me a number to work with.
I'm interested in increasing quality over time, or at least not decreasing it. Here are some of the questions that are relevant for me when thinking about quality:
- How do I at least maintain my usual level of quality?
- What should I do if I start seeing a decrease in quality?
- Are my team mates reacting well to this change in workflow?
- Am I somehow impacting my team's experience by using this approach?
Houston, we may have a problem
As I practiced this new workflow I began to see that I definitely was seeing more review comments. I work in a small team too, so that means that often time I'd get multiple comments from the same person, which is typically an indicator that I went the wrong way or that the implementation was lacking.
My team lead was very much involved in this review process, so I asked him directly if he felt my code was in worse shape. He replied that he didn't feel that way – just that more code was shipped.
That seems to be what happened. The number of review comments I had per 100 lines of code was roughly the same as before, but to me it felt like I just had more comments for the same amount of code. We humans have a tendency to focus on negative trends... :)
Early feedback is great
In the middle of all these experiments, I am standing by to have my second kid any minute now, and to be cautious I push code every single day. When I start working on a branch and I push at the end of the day, I make sure to have a good outline of how the code is going to end up looking, but sometimes tests will be broken, it might not even compile! But once I get code to look like an early draft I open a draft pull request and request review from my team.
While it may involve more back and forth between people, I found this to be a great strategy. Multiple times in these early reviews someone suggested an easier way of solving things that might have saved me a couple of hours, and those time savings compound over time. By asking for feedback early I either get pushback or affirmation for my implementations which helped guide me towards a solution.
I argue that this has benefits for the reviewer as well: it's way easier to review a sketch of an implementation than the actual full implementation in detail. It's a distilled version of what the code is eventually supposed to do. Doing an early review means you end up getting familiarized with the work itself instead of looking to see if there are enough tests or if the code should be refactored.
Once I get a draft review, I address all the comments and make it look good for a second, definitive review. I ask the same reviewer to do the definitive review since they will be familiar with the main idea already, and this second stage ends up being more focused on idioms and details. If any comments come up at this stage they are small and quick to address.
TL;DR
There is no perfect workflow for everyone, but for mid-level developers whose work mainly consists of adding features, I found the strategy of pushing code more often and doing a 2-stage review process helps improve efficiency by minimizing time spent on premature optimizations and implementing bad solutions. Following this workflow increased my work output and I didn't exactly know why so I wrote this up trying to make sense of it.