A couple years ago, while working on some extremely brutal refactors spanning days if not weeks, I came up with a system for tracking todos across a codebase that I’ve stuck with since. It has worked well for me across all sorts of projects.
The basic ideas of the system are:
For many things, inline todos are far better than tasks in an external application
Doing a “todo” search and seeing 100 seemingly disconnected notes is completely unproductive. Therefore, todos should be grouped
Todos added as part of a task — like a large refactor — must be finished along with the task. Therefore, “temporary todos” should be separated from long-term tasks
Categorized todos
Let’s start with point 2) since it’s simpler. What I do is just write todo@tests
or todo@v4
or todo@routing
to group todos by category. Then I can easily fuzzy search either of those terms and I find all of the todos related to that category even if they’re scattered across the codebase.
Just like you’d do:
// todo rename this class to WellNamedClass
class PoorlyNamedClass { ... }
You do:
// todo@v4 rename this class to WellNamedClass
class PoorlyNamedClass { ... }
Searching todo@v4
gives you the full list of things to change in version 4.
Priority todos
Often while working on a complex feature you may end up with a “stack” of tasks:
To achieve Task A we need to change this function to return errors differently (= Task B)
To do Task B, we need to change a different function (= Task C) and then not forget to check all the uses of this function so errors are still handled completely (= Task D)
Hmm looking at those uses quickly I see we’ll need to change error handling overall in these routes (= Task D)
Working on some tasks you basically end up going “deeper” and get a longer and longer list of things to do. To do the first thing, you actually need to do the second thing first, but that needs the third thing, but also quickly looking at some other stuff we should not forget to do this other thing as well.
It’s very difficult to keep track of this and during complex refactors this stuff is often critical.
The way I keep track of this is priority todos. Basically, I write todo1
, todo2
and so on.
Specifically:
todo1
means resolve as part of this whole feature, but it’s not related to what I’m thinking about right nowtodo2
means resolve after todo1todo0
is what you’re doing right nowas you get deeper into a task (you’re thinking about the two todo0 notes you wrote, but realize you will not move forward until this other thing is done first) you use a todo00
then a
todo000
and so on
Think of this as “lower number means higher priority” — todo1 has a priority of 1, todo0 has priority 0, todo00 has priority -1, and so on.
It may seem counterintuitive at first but using todo0 for anything to be done as part of the immediate sub-task works better than if you used higher numbers for higher priority.
The reason is that searching todo0 also yields all todo00000s. You will be actively searching your codebase for todo0s while working on complex tasks, and only when you see there are no todo0s, you know the most “immediate” stuff is done and you can move on to the “positive numbers” which are basically related, “not to be forgotten about” tasks, rather than “deeper parts of the current task” — which is what todo0s are.
Everyone I’ve taught this system has stuck with it. It works exceptionally well in projects where you collaborate with others. One thing I like to do in some projects (but not others — it depends on whether you’re collaborating with others or not, whether it’s an app or a library and so on) is add a CI action that fails if there are any priority todos:
jobs:
validate:
name: Validate code
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Check for priority todos
run: '! grep -r "todo[0-9]" --exclude-dir=workflows .'
if: always()
The regex will capture any priority — numeric — todos. The numbers you end up using most are really just:
todo0 — what I’m doing right now
todo00 — what I’ve just learned I need to do BEFORE todo0 tasks
todo000 — some even higher priority thing that need to be resolved first
todo1 — just don’t forget to do this after I’m done with all todo0s aka the current task
todo2 — if there’s a lot of todo1s that they form a single “group” or a task of some sort, todo2s are only resolved after all todo1s are
You can easily see if a PR is incomplete if it has any priority todos. On the other hand, if it has just “todo@longTermTask do something” you know you can ignore that.
You don’t get this distinction with regular “TODO: Do something”. When should that something be done? As part of this PR? Immediately after this PR? At some point in the future?
The combination of priority todos and category todos is everything you need to (in some cases fully) organize your project’s tasks, in a way that’s colocated with the relevant code.
And paired with version control, you get colocation not just in terms of code lines, but time. Just git blame the line the todo is on and you can see which commit added it. You get actual development context for the todo, not just whatever info you put into your issue tracker where it’s your responsibility to add all the necessary context and link any related things.
CLI tool
Since I use this system a lot, I made a simple CLI tool that serves as a companion to a dumb fuzzy search in an editor. At the same time, I want to emphasize that searchability is a major part of this system — it should not need additional tools, only an editor.
You can find the tool in this repo: archtechx/todo-system. To install it, you need to have the Rust toolchain installed. Then you can just run:
cargo install --git https://github.com/archtechx/todo-system.git
The tool will be added to your $PATH
as todos
. I personally also like to make an alias:
alias t="todos | less"
So I just type t
in any project and I see all todos displayed in the system pager. The reason I like the pipe to less
is that it makes sure you see the top of the output first without scrolling. Close it by just pressing Q or navigate using J/K.
The CLI tool shows priority todos first (sorted by priority — e.g. todo00 at the top) includes any extra comments you add to your todos and locations rendered in a way that lets editors like VSCode cmd+click to get to the file.
Then categorized todos are rendered — grouped together.
Finally, it shows uncategorized todos.
The tool also reads TODO.md (my personal convention) which should be structured as just bullet points within markdown sections (headings), or a TODO/TODOs section from README.md.
In essence the tool is just “grep todo” on steroids, it checks everything in the current working directory (except gitignored paths), searches for todos, and then presents them in a nicely formatted way.