Simple prioritization framework for software developers

Prioritization is an essential skill for software developers, yet many find it challenging to decide what task to focus on next. In this post, I would like to share a simple prioritization method I have successfully used for years. It is particularly effective for simple tasks but can also be helpful in more complex scenarios.

Here is how it works

To determine what to work on, I look at my or my team’s tasks and ask: Can we ship without this? This question can be answered in one of the three ways:

  • No
  • Maybe (or It depends)
  • Yes

First, I look closely at tasks in the No bucket. I want to ensure they all are absolutely required. Sometimes, I find in this bucket tasks that are considered a must by the task owner but barely meet the Maybe bar from the product perspective. For example, it is hard to offer an online store without an inventory, but search functionality might not be needed for a niche store with only a handful of items.

Then, I look at Maybe tasks. Their initial priority is lower than the ones in the first bucket, but they usually require investigation to understand the trade-offs better. Once researched, they often can be immediately moved to one of the remaining buckets. If not, they should go to the Yes bucket until they become a necessity. For the online store example, a review system may be optional for stores selling specialized merchandise.

Tasks in the Yes bucket are typically nice-to-haves. From my experience, they usually increase code complexity significantly but add only minimal value. I prefer to skip these tasks and only reconsider them based on the feedback. An example would be building support for photos or videos for online store reviews.

The word ship should not be taken literally. Sometimes, it is about shipping a product or a feature, but it could also mean completing a sprint or finishing writing a design doc.

This framework works exceptionally well for work that needs to be finished on a tight timeline or for proof of concepts. In both cases, you need to prioritize ruthlessly to avoid spending time on activities that do not contribute directly to the goal. But it is also helpful for non-urgent work as it makes it easy to identify areas that should get attention first quickly.

Storytime

One example where I successfully used this framework was a project we worked on last year. We needed to build a streaming service that had to aggregate data before processing. We found that the streaming infra we used offered aggregation, but it was a new feature that had not been widely adopted. Moreover, the aggregation seemed very basic, and our estimations indicated it may not be able to handle our traffic volume.

After digging more, we learned that the built-in aggregation could be customized. A custom implementation seemed like a great idea because it would allow us to create a highly optimized aggregation logic tailored to our scenario.

This is when I asked: Can we ship without this?

At that point, we did not even have a working service. We only had a bunch of hypotheses we could not verify, and building the custom solution would take a few weeks.

From this perspective, a custom, performant implementation was not a must-have. Ensuring that the feature we were trying to use met our functional requirements was more important than its scalability. The built-in aggregation was perfect for this and did not require additional work.

We got the service working the same day. When we started testing it, we found that the dumb aggregation satisfied all our functional requirements. Surprisingly, it could also easily handle our scale, thanks to the built-in caching. One question saved us weeks of work, helped avoid introducing unneeded complexity, and instantly allowed us to verify our hypotheses.

Accelerate your software engineer career by learning to love ‘boring’ technologies

Software developers are constantly bombarded with new, shiny stuff. Every day, there is a new gadget, JavaScript framework, or tool promising to solve all our problems. We want to use them all and cringe when we think about the boring technologies we use in our day-to-day jobs.

But we should love these boring technologies.

They get the job done. They pay the bills. They are there for us when we need them the most.

Most boring technologies have been around for years or even decades. They are versatile and battle-tested. Are they perfect? Absolutely not! They all have quirks and problems, but there are good reasons why they survived most of the contenders trying to replace them.

I don’t suggest that you avoid new technologies altogether. On the contrary, I encourage everyone to explore what’s new out there. You just need to know when to do this and understand the risks.

I recommend using mature technologies for risky, critical, or time-sensitive projects. When there is little margin for error, you want to use a reliable technology you understand and can work efficiently with. New technologies rarely meet these criteria.

Smaller or non-critical projects are perfect for trying something new and learning along the way, as long as you understand the consequences if things don’t work out. The most common issues with new, often unproven, technologies include:

  • trade-offs – you are trading a set of reasonably well-understood problems for a set of unknown problems
  • bugs or unsupported scenarios whose fixing is outside of your control
  • issues with no acceptable workarounds may block you or even force you to pivot to a different technology
  • poor documentation and support; limited online resources
  • the technology may unexpectedly lose support forcing you to either sunset your product or rewrite it completely

Occasionally, you will get lucky, and the new technology you bet on will become a ‘boring’ technology. I experienced this at my first job, where we decided to experiment with the .NET Framework. At that time, it was still in the Beta stage. Today, millions of developers around the world use the .NET Framework daily. I ended up working with it for more than 15 years. I even contributed to it after I joined Microsoft, where I worked on one of the .NET Framework teams.

I had less luck with my Swift SignalR client. For this project I needed WebSockets, but at that time, there was no support for a WebSocket API in the Apple ecosystem. I decided to use the SocketRocket library from Facebook to fill this gap. When my attempts to get help with a few issues failed, I realized that the SocketRocket library was no longer maintained. This lack of support forced me to look for an alternative. I soon found SwiftWebSocket, which I liked because it was small (just one file), popular, and still supported by the author. Moving to SwiftWebSocket required some effort but was successful. Fast forward a few years, and the library stopped compiling after I updated my XCode. I fixed the issues and sent a pull request to make my fixes available to everyone, but my PR didn’t receive attention. I also noticed that more users complained about the same issues I hit but were not getting any response. This unresponsiveness was a sign that the support for this library ended also (the author later archived the project). As I didn’t want to go through yet another rewrite, I forked the code, fixed compilation issues, and included this version in my project. Eventually, Apple added native support for WebSockets to the Foundation framework. Even though it was a lot of work, I was happy to migrate to this implementation because I was confident it would be the last time!

Accelerate your software engineer career by managing scope creep in your pull requests

After submitting code for review, you will often receive requests to include fixes or improvements not directly related to the primary goal of your change. You can recognize them easily as they usually sound like: “Because you are touching this code, could you…?” and typically fall into one of the following categories:

  • Random bug fixes
  • Code refactoring
  • Fixing typos in code you didn’t touch
  • Design changes
  • Increasing test coverage in unrelated tests

While most of these requests are made in good faith and aim to improve the codebase, I strongly recommend against incorporating them into your current change for a couple of reasons:

You will trade an important change for a lower-priority one

Your main change was the reason why you opened the pull request. By agreeing to additional, unrelated changes, you sacrifice the more important change for minor improvements. How? The additional changes you agreed to will need at least one more round of code reviews, which can trigger further feedback and more iterations that will delay merging the main change.

I learned this lesson the hard way when I nearly missed an important deadline after agreeing to fix a ‘simple bug’ that was unrelated to the feature I was working on. Fixing the bug turned out much harder than initially perceived and caused unexpected test issues that took a lot of time to sort out. Just when I thought I was finished, I received more requests and suggestions. By the time I merged my pull request, I realized how close I had come to not delivering what I promised on time, only because I agreed to focus on an edge case that ultimately had minimal impact on our product.

You will dilute the purpose of the pull request

Ideally, each pull request should serve a single purpose. If it’s about refactoring, only include refactoring. If it’s fixing a bug, just fix the bug. This approach makes everyone’s job easier. It makes pull requests easier and quicker to review. Commits are less confusing when looked at months later and if you need to revert them, no other functionality is lost. All these benefits diminish if your pull request becomes a mixed bag of random code changes.

How to deal with incidental requests?

You have a few ways to proceed with suggestions you agree with. Usually, the best approach is to propose addressing them in separate pull requests. If your plate is full, log tasks or add them to the project’s backlog to tackle them later. Occasionally, a request may suggest a significant redesign that may fundamentally alter your work. If the idea resonates with you and won’t jeopardize your timelines, you might want to consider implementing it first and redo your changes on top of it.

How not to ruin your code with comments

The road to the programmer’s hell is paved with code comments

Many developers (and their managers) think that comments improve their code. I disagree. I believe that the vast majority of comments are just clutter that makes code harder to read and often leads to confusion and, in some cases, bugs.

A heavily commented code base can significantly slow down the development. Reading code requires developers to constantly filter out comments, which is mentally taxing. Over time, developers learn to ignore them to focus better on code. As a result, comments are not only not read, but also forgotten when related code changes.

Moreover, because all comments look similar, it is hard to distinguish between important and not-so-important comments.

Unhelpful comments

In my experience, unhelpful comments fall into a few categories.

Pointless comments

Pointless comments are comments that tell exactly what the code does. Here is an example:

// check if x is null
if (x == null)

These comments add no value but have a great potential to turn into plainly wrong comments. There is no harm in simply removing them.

Plainly wrong comments

My all-time favorite in this category is:

// always return true
return false;

The most common reason for these comments is to miss updating the comment when changing the code. But even if these comments were correct when written, most were not useful. Similarly to “pointless comments”, these comments should just go.

TODO/FIXME comments

When I see a // TODO: or a // FIXME: comment, I cannot help but check when they were added. Usually, I find it was years before.

Assuming these comments are still relevant, they only point to problems that were low priority from the very beginning. This might sound extreme, but these comments were written with the intention of addressing the problem “later”. Serious problems get fixed right away.

Let’s be honest – these low-priority issues are never going to get fixed. Over the years the product had many successful releases and the code might already be considered legacy, so there is no incentive to fix these problems.

Instead of using comments in the code to track issues, it is better to use an issue tracking system. If an issue is deprioritized, it can be closed as “Won’t Fix” without leaving clutter in the code.

Comments linking to tasks/tickets

Linking to tasks or tickets from code is similar to the TODO/FIXME comments but there is a twist. Many tasks will be closed or even auto-closed due to inactivity, but no one will bother to remove the corresponding comments from the code. It could get even more problematic if the company changes its issue tracking system. When this happens, these comments become completely irrelevant as the tasks referred to are hard or impossible to find.

Referencing tasks in the code is not needed – using an issue tracker is enough. Linking the task in the commit message when fixing an issue is not a bad idea though.

Commented out code

Checking in commented code doesn’t help anyone. It probably already doesn’t run as expected, lacks test coverage, and soon won’t compile. It makes reading the code annoying and can cause confusion when looked at if syntax coloring is not available.

Confusing or misleading comments

Sometimes comments make understanding the code harder. This might happen if the comment contradicts the code, because of a typo (my favorite: missing ‘not’), or due to poor wording. If the comment is important, it should be corrected. If not, it’s better to leave it out.

Garbage comments

Including a Batman ASCII Art Logo in the code for a chat app might be amusing but is completely unnecessary. And no, I didn’t make it up.

Another example comes from the “Code Complete” book. It shows an assembly source file with only one comment:

MOV AX, 723h     ; R.I.P. L. V.B.

The comment baffled the team, as the engineer who wrote it had left. When they eventually had a chance to ask him, he explained that the comment was a tribute to Ludwig van Beethoven who died in 1827 which is 723h in hexadecimal notation.

Useful comments

While comments can often be replaced with more readable and better-structured code, there are situations when it is not possible. Sometimes, comments are the only way to explain why seemingly unnecessary code exists or was written unconventionally. Here is one example from a project I worked on a while ago:

https://github.com/SignalR/SignalR-Client-Cpp/blob/4dd22d3dbd6c020cca6c8c6a1c944872c298c8ad/src/signalrclient/connection.cpp#L15-L17

// Do NOT remove this destructor. Letting the compiler generate and inline the default dtor may lead to
// undefined behavior since we are using an incomplete type. More details here:  <http://herbsutter.com/gotw/_100/>
connection::~connection() = default;

This comment aims at preventing the removal of what might appear to be redundant code, which could lead to hard-to-understand and debug crashes.

Even the most readable code is often insufficient when working on multi-threaded applications. As the environment can change unpredictably in unintuitive ways, comments are the best way to explain assumptions under which the code was written.

Finally, if you are working on frameworks, libraries, or APIs meant to be used outside of your team you may want to put comments on public types or methods. They are often used to generate documentation and can be shown to users in the IDE.

How to write good comments

Here are a few tips when it comes to writing good code comments.

  • Comment sparingly. Make your code speak for itself – use meaningful variable names and, break down large functions into smaller ones with descriptive names. Reserve comments for clarifying non-obvious logic or hard-to-guess assumptions.
  • Focus on “why”, not on “what”. Readers can see what the code does but often struggle to understand why. Good comments explain the assumptions, intentions, and nuances behind the code.
  • Be concise and relevant. Avoid including unnecessary details that could lead to confusion.
  • Ensure comments are clearly written and free from typos or mistakes that make them hard to understand.
  • Use abbreviations cautiously. Expand them on the first use. An abbreviation that is obvious now may become incomprehensible after a few years even for you.

7 Tips To Accelerate Your Code Reviews

One complaint I often hear from software developers is about how long it takes to merge Pull Requests (PR). When I dig deeper, I often find it is all about code reviews. Indeed, code reviews can be a bottleneck, as they require someone to shift the focus from their work to review the PR. The key to faster code reviews is making reviewing PRs as easy as possible. There are several ways to achieve this.

Send clean PRs

You shouldn’t expect to quickly merge a PR with red signals. If your code doesn’t compile, tests fail, or if there are linter warnings you might only hope that someone will notice and ask for a fix. Often this won’t happen. Instead, people will quietly ignore your PR thinking it was sent by mistake.

I wrote a post dedicated to this topic recently

Write a good commit message

The struggle to find someone to review a PR is often a common reason why code reviews take so long. Good commit messages can help with that. A good commit message helps potential reviewers quickly understand if they are the right person to review the change and gives them an idea of how much time and effort the review will need.

Consider these two PR titles as an example: “Fixing a bug” versus “Fixing memory leak caused by retain cycle in X”. Which one do you think will catch more attention? The second title is more likely to attract reviewers because of its specificity. It tells them what the problem is and where in just a few words.

If you would like to learn more, I wrote a detailed post on this very subject.

Ensure good test coverage

“Can you add tests?” might be the first, and the only comment you get on your PR if you haven’t provided sufficient test coverage. It’s easy to understand why people hesitate to review PRs without good test coverage. Adding tests often uncovers issues or gaps in the code. Fixing them might lead to significant changes, that will trigger a complete re-review. Good test coverage not only saves time for the reviewer, who won’t need to go through your changes twice but also for you, as it reduces the number of necessary revisions.

Keep your PRs small

Conducting a thorough review of a huge PR is time-consuming and requires significant effort. Not many team members can commit to such a task. However, most large PRs can be broken down into smaller, logically complete PRs. By doing this, you can distribute the effort to review your changes among more team members, shortening the time needed to merge them.

For a more in-depth discussion on this topic, check out my post.

Address comments quickly

Respond to comments on your PR promptly, to keep the reviewer engaged while the details are still fresh in their mind. Delaying your response can lead to deprioritizing your PR as the reviewer will have to spend time and effort to recall necessary context. Addressing comments doesn’t always mean you need to send a new revision. It is often about providing clarifications or confirming assumptions. However, if the reviewer identifies an issue that requires a fix, you should promptly update your PR.

Ask for reviews

If you did everything possible to make your PR easy to review but it is still not getting traction, you may need to ask for a review directly. If anyone on the team can review your change, consider asking in your team’s chat room. If you would like a specific person to review your PR, contact them individually. However, be cautious not to overdo this. On most teams reviewing code is an expectation, and pinging people for reviews immediately after publishing a PR can be very distracting. In our team, we have a 24-hour rule for non-urgent PRs: we only ask for a review if a PR hasn’t been picked up within a day.

Review your team members’ PRs

If you don’t review your team members’ PRs, don’t expect them to prioritize yours. PRs that are constantly at the end of the queue will inevitably take longer to be reviewed, approved, and merged. That’s why it is important to dedicate a few minutes each day to code reviews. My rule is to review at least as many PRs as I publish. It works wonders – most of the time I can merge my PRs on the same day I submit them. Moreover, if my PRs aren’t getting enough attention, I don’t feel uncomfortable asking to review them.
Here is a post about other benefits of reviewing code.

And when you get your PR reviewed, remember to promptly merge it

What are your tips to speed up code reviews? I’d love to hear them!