Category Archives: Software Development

Question on r/ExperiencedDevs: Getting Code Reviewed Faster

I saw this question on the ExperiencedDevs subreddit today: My colleague’s code gets reviewed no questions asked. Getting mine reviewed takes a couple of nudges. How can I improve the situation? The answers on this subreddit are usually helpful, and I saw one that I resonated with me:

Personally quick PRs, sub 15 minutes total review time, I consider to be a mini-break from whatever feature I’m working on over the span of multiple days. If your PRs are 1000s of lines long and require an entirely different headspace it may cause delays that way.

This is what I was getting at in PR Authors Have a lot of Control on PR Idle Time where I told a story about the analysis a colleague at Atlassian did on our PR data. He found that short PR’s had less idle time.

On my team, where short PRs were highly encouraged, most reviews were done in a couple of hours, with 24 hours being the absolute max. Any longer than that would often be resolved by a live 1:1 review because it meant that the code was too complex to be reviewed asynchronously.

The thread on r/ExperiencedDevs has some more advice on solutions that try to resolve the social aspects, which are helpful, but I do think PRs that are easier to review will sit for less time. If your code is inherently complex, there is still a lot you can do to make the review easy (see A Good Pull Request Convinces You That it is Correct).

A Good Pull Request Convinces You That it is Correct

My onboarding peer-mentor at Trello described a good pull request as telling a story. In practice this meant that you would edit and order the commits after you were done so that the reviewer could go commit-by-commit and understand the change you made in steps.

So, for example, let’s say you were working on a feature. In your second commit, you introduce a bug, but then in your fifth commit, you find and fix that bug. Instead of just PR’ing that, you would edit the commits so that the bug was never introduced.

This is analogous to sending a document with superfluous text deleted, not crossed out. If you don’t edit the commits, you will waste the reviewers time because they might see the error in the second commit, make a comment, and then have to go back and amend their comment in the fifth. If you did this a lot, they might not even finish reviews before rejecting them (which is what I suggest you do to PRs with obvious problems).

I like the story frame, but I have started to think of a PR as more of an argument of its own correctness. I am trying to teach the reviewer the details of the problem and convince them through evidence that the new code is correct.

In a PR, I might start by introducing a unit test into the area you intend to change. Then, to make things clearer, I might commit a small refactoring (that isolates the change). It’s now possible to add more tests and possibly a failing one that shows what my intended fix will address. My small code clean-up commits are in service of that argument. Without them, it’s hard to tell if my fix won’t break something else. With them, the fix feels like a natural and inevitable conclusion.

Like a philosophical argument, I will anticipate and address the cases the reviewer might think of. But it’s not enough to handle a case in the code, your whole PR needs to make it clear that you anticipated and addressed it (with tests, comments, screenshots or any other evidence you can think of).

But the most important reviewer to convince is myself, of course, and doing the work to write the argument gives me confidence that my code is correct.

One Time I Recognized a Junior Catalyst

This is a followup to Some Behaviors of a Catalyst where I said that I didn’t think you needed seniority to be one.

More than 10 years ago, I was interviewing college seniors for an entry-level position. Among the group of very talented students there was one obvious front-runner who we offered the position to (and who accepted). But there had been another candidate that impressed us.

They interviewed well and were qualified for our position. But, we preferred candidates with some C/C++, and they had less than our preferred candidate, maybe none, I don’t remember. In any case, that was probably the deciding factor. But in a lot of other ways, they were outstanding (in the literal sense of the word). They stood out.

Here’s what I mean. A bunch of the candidates had worked together on their senior, two-semester, capstone project. We heard about this project from several angles. Over and over, a candidate would point to another person as the reason the project went well. They would, of course, talk about their own contributions, but they would also specifically point out what this other person had done as well. The thing was, the other person was always the same person, the “outstanding” one.

Even with this clue, we didn’t hire them.

About a week after we had made our decision and the offer, my boss called me in. He said a friend of his (another tech business owner) had sent in an unsolicited recommendation for a student he knew. I think he had them as intern or something. In any case, the recommendation was effusive. The student was, of course, that same one who had been recognized by their peers.

At this point we felt as if we’d be idiots not to hire them, but we didn’t have a position. The good thing about junior developers, though, is that they are cheap and you should always have a position if you see a good one. So, we created a position and made an offer. I think this is the only time I recognized a junior catalyst before working with them—the key is that people like working with them and attribute success to them. When that fact comes to light without seeking it, it’s a good sign.

Some Behaviors of a Catalyst

Yesterday I wrote that you should recognize catalysts, but didn’t say how. The problem is that so much of what they do is under the radar, but one tell is that they always seem to be part of successful teams and everyone on the team knows that they were an important member.

Catalysts can be technically great, but perhaps not the most technical person on the team or the best specialist on your corner of the company. In my experience, they are more likely generalists. They likely know the most about the other teams in your organization, which is why they can get things done with them. They often know who to go to, and those people often know them.

It’s not just the other teams, they also know the most about the architecture of other services. They might even have commits in repos they don’t own and in the third-party dependencies they use. When they need to get things done, they don’t limit themselves to just their own codebases.

Their colleagues like them and like to work with them. And the catalyst genuinely liked working with those people too (and they like people in general). They are often your best source of candidates. The best way to hire a catalyst is through a referral from someone on your team that raves about working with them.

Knowing what I know now, if I were to look for them I’d be looking for what Atlassian called “Feature Leads”, which were engineers that led a “feature” project. They often needed to orchestrate the work of several other people and perhaps several other teams. I guess to make sure they were good at it, I’d be looking for people that did it repeatedly at the same place. This work doesn’t rely heavily on their commits, but I’d be interested in people that had commits in disparate parts of the feature, not because of the technical skill it requires to do that, but because it would indicate a holistic view and might indicate a tendency to clear roadblocks, both of which are catalyst behaviors.

As I said in How Senior Software Developers Think, the more senior you are, the more you think of how your work relates to the mission of the company. Catalysts at any level do that. It’s often easier to have success as a catalyst the more senior you are, but I don’t see this as a requirement. Feature leads at Atlassian could be just past the junior level (pre-Staff/Senior level). It was a good indication that they should be promoted, but they didn’t need to have that level to catalyze.

Recognize the Catalysts

I tried to pattern my career after An Unnamed Programmer in Peopleware, whose contributions weren’t always clear, but “had never worked on a project that had been anything other than a huge success.” She was overlooked by her management, so I learned that to do this, you needed to make your contributions clear.

As a manager, I didn’t always know how to hire for this quality, but I knew it when I saw it and made sure they were on the projects that mattered. And I keep track of them, too. I might not know if a new candidate is one, but once you know someone is a catalyst, you can always hire them at your next gig.

I Prompt Cursor Based on My Progress Bar

In Build a Progress Bar for Your Work I explained how I take a task and break it down into subtasks. Each subtask then becomes a commit. A benefit I didn’t mention is that I can look at the commits and see the time it took to do each task, which will be a good way to adjust my estimate going forward. Unlike in Using Zeno’s Paradox For Progress Bars where you have no clue how much time is left, I actually do have some idea.

Having this progress bar of subtasks has also been useful in working with Cursor. I use each of those subtasks to drive my prompt. Here’s an example:

I am working on an app that has a thing like a feed with a concept of posts and reactions, like slack or discord. My stack on the server is MySQL, a TypeScript based ORM, and a TypeScript based GQL wrapper that is served by node and Apollo. My Tasks/Subtask progress bar looks like this

  1. Add Reactions to Backend
    • Create reaction entity and relate to shared objects
    • Create SQL migration
    • Create ORM service function to react to a shared object
    • Test the service function
    • Update shared object ORM getter to have a reaction summary
    • Test the reaction summary
    • Add a field resolver to the shared object resolver to get the reaction summary

Having that, I use each sub-bullet to create my prompt. I give Cursor a lot more information though. As an example, here’s my prompt for the first bullet (I also need to put in the context files)

Add a reaction.entity.ts with a UUID id, a many to one relationship with sharedObject (and do the reverse in sharedObject). Have a reaction type (string), a User, a createdDate (like sharedObject) — do a unique that is like this UNIQUE (shared_object_id, user_id, reaction_type), but in TypeOrm syntax at the top of the class

From that it knows what fields I would likely want to index, and adds that too. It picked the delete rule I usually use (CASCADE), and it updates the User entity and SharedObject entity to have the reverse relations. It made some slight errors, but they are easy to check and fix.

Once I have that, I make a commit and move on to the next one. In that case, I use an external script for migration, so I just do that myself, and commit.

I go on like that, picking the next thing in my list, coding or generating and fixing, and then making a commit. The result is a PR that looks the way I want (not just the whole change in one commit) in the order that makes sense for a reviewer. I do this even though I am the reviewer (as I wrote in Pull Requests for One) because I do actually do a review and I want it to be easy.

This activity (to me) is very much like programming, which is what I was getting at in Can non-programmers use Cursor? A complete non-programmer? Probably not, but I do think someone could do this if they had some programming skill. The main thing I am doing is what I said in Programming is Disambiguating: “Programming is taking a nebulous problem and breaking it down, understanding it, trying to find building blocks, and then building up something that solves the problem.” Those building blocks could be prompts or code, it doesn’t matter, as long as it solves the problem.

Can non-programmers use Cursor?

I wrote Can non-programmers make applications with AI? last month. TL;DR: Yes. But, I hadn’t used Cursor yet. Now, I’m pretty sure that to use Cursor well on a real project, it helps to know some programming. But, if you do, it’s way more useful than it would be to an expert, which is saying something, because I find it very useful.

As an expert, my coding session today was maybe 2x faster for the same code. But, a non-programmer would have taken weeks to do what I did (if they could even do it). I think they have a chance to get close with prompts—I almost did, and they would try harder.

For what I needed to do today, in the first 5 minutes, Cursor did a good first pass. I fixed its syntax errors and the result “worked”. It looked terrible (this was implementing drag drop in a React app)—it took me a couple of hours to get it exactly how I liked it and then polish the code. But, getting me started quickly gave me a ton of momentum, and then I had time to make it exactly how I wanted it.

For a less skilled programmer to do this task, I think the first five minutes goes the same way. I know from experience, that it’s easier for me to just fix little problems, but I think it could be done with prompts. Then, the rounds of successive improvement were helped by autocomplete, but I initiated all of it. I relied on my knowledge of CSS and React to fix issues. I haven’t had good experience with the LLM’s for this—they can’t “see” the problem in the browser yet, and all of my problems were UI nitpicks and complicated Drag/Drop issues (not a static render I could screenshot or easily describe). All of the different modes of Cursor LLM integration have strengths for different uses—but some rely more on your ability than others.

From my use, it feels like knowing some programming is required. But, if it took a less skilled person from 2 weeks to 1-2 days, that’s more like 10x for them. What’s more, I go from 20x faster than them to 4x for this task, and they have more to improve, where my gains are asymptotic.

Three Days of Cursor

I tried out Cursor three days ago. But, before I did, I really did try to give GitHub Edits a chance. I used it for a few things, and it was more trouble than it was worth. I mentioned this to a friend at lunch on Tuesday and he asked why I hadn’t tried Cursor yet. I said I was worried it would interfere with my setup too much, but he confirmed that it reads your VSCode settings and extensions and (for him) just works. I use extensions as Tech Debt Detectors, so they are important to me.

So, the next day, I gave it a try. At 3:15 on Wednesday, I started the download. By 3:20, it was installed and working as I expected. I wrote a prompt to do the thing I needed to do next and its change was perfect. It was 3:27.

My next request did not go as well, but it was a complicated one involving a package I wanted to try. It would not install properly, and neither I nor Cursor could figure it out.

Since then, I’ve been using Cursor a lot. There are three main ways it’s better than GitHub CoPilot.

  1. The Generation from chats is much better than the GH equivalent.
  2. The autocomplete doesn’t require me to place the cursor. It anticipates what I am going to do and offers changes in different parts of the file. Sometimes all at once.
  3. There’s a hotkey for inline generation that works well too. I had been doing this with comments, but this is better because it just lets me type (without interruptions) and it knows I want it to start a generation when I’m done.

For all of these features, I feel very much like I am still programming. I am sequencing the work. It feels like it’s reading my mind since I do know exactly how to do what I am asking, but it’s saving a lot of typing.

This is helped by my app’s code being very regular. There’s only one way to do DB code, one way to wrap it in GQL, one way to call it. My UI is regular. The code has established patterns, and I put example files in the context before I prompt (and mention that I want it done like those files).

The main way it helps me though, is to keep me in flow. I am not constantly juggling files and typing out simple things. I say I want a table with an id, name, and a specific relation, and it knows my id is a UUID, and how I typically name relations. I say I want a cross table and it knows to set up the relations between the tables and the new one (in the way I have done it before). It intuits that I want cascading deletes without me asking. It’s just a lot of little things that save time and let me move on to something else.

How to Increase Story Points Per Sprint

Story points are not a basis for measuring and improving productivity because points are just an abstract measure of time. That’s why I said you should Be Skeptical of Points-based Productivity Claims. Any systematic productivity improvement would decrease the points for a future story, and the velocity would go back to a steady state.

If you had a way to compare stories over time to each other, you might see improvement in an individual story. If you literally are doing the same story over and over—like, for example, a set of recurring tasks—then, you could absolutely see its points decrease over time if you automate it. So, one way to get more done per sprint is to automate repetitive tasks, but once you do that, the points assigned to that task will go down and the velocity will not change. But the thing to look at is the comparison of this story in your sprint with its past incarnations, not velocity.

This is why I Estimate Using Time, not points. They are the same thing, but everyone understands time, and no one understands points. If you reread the first two paragraphs, but substitute “hours” instead of “points”, then they are obviously correct. If you automate a task then the number of hours it takes goes down—no one disagrees with that. If I use CoPilot to get systematic productivity, then my estimate of the number of hours it takes to do something will go down. Points obscure this.

There is a problem with using time (which is also true with points, but it’s just glossed over), which is the difference between work time and calendar time. If you estimate with work time, and report that, you will confuse people, because the only time that matters is calendar time. But, Story Points are often a proxy for work time, so I’ll assume that’s what it is for you too.

Given all of that, it’s easy to see how to increase the number of points done in a sprint. You need to either create time or decrease the difference between work time and calendar time. If you do that, you will find more stories get pulled into a sprint and get done, which will show up in velocity because it probably won’t cause the points for a particular card to go down, because most teams estimate points using work time.

So, how do you create time? Easy. Hire more developers. Despite what you have heard, you can add developers to a team to get more done, as long as they don’t need to communicate with each other.

To reduce the difference between work time and calendar time, you need to concentrate on everything that’s not sprint work. The main culprit is unnecessary meetings, but another is long waits for feedback. There are lots of times developers might need to wait, but waiting for code reviews is probably the biggest one because it happens to everyone all of the time. To work on that, read: If code reviews take too long, do this first.

Add Developers to a Team Without Lowering Productivity

According to Brook’s Law (from Mythical Man Month [affiliate link]), adding software developers to a late project makes it later because the number of communication lines increases rapidly with each new person. A team with N developers has N*(N-1)/2 communication lines, so a team of two has one line between them, but a team of 20 has 190 lines, one for each pair. If we accept this, there are several caveats that mean the law might not apply to you.

It should be clear that this won’t apply to a very small team. If you have two developers, then adding two more will help. The number of communication lines goes from one to six, but the amount of time that can be spent working on the project doubles. Compare this to adding two people to a twenty person team, where you might add more than forty communication lines and only 10% more work time. Also, a team of two have probably not created a very large codebase yet, so ramp-up time is not as bad as what it takes to learn what a team of twenty could create.

Another way the law doesn’t apply is if your developers don’t need to talk to each other. If they can work completely independently, then you are not adding communication lines. That also works if they only need to talk to a small subset.

You can also increase the effectiveness of a given team (before adding anyone) if you can remove lines of communication. This does not mean “better communication” (like documentation), which only reduces the cost of a line, not the number of lines. Reducing the cost of lines is good, but will be overwhelmed by combinatorial explosion of new lines if you don’t work to remove them.

Removing a line of communication only happens if the communication isn’t necessary. The model here is to think of that part of the team as external. If I add AWS to my project, I expect a lot of productivity gains, but I don’t add in communication lines to every developer that built it. A lot of teams move to architectures like micro-services to reduce lines of communication, which is supposedly how AWS came into existence in the first place. That’s one way, but you can get there with clean API’s in libraries too. Whatever you may think of external library dependencies (I think of them as instant tech debt), they can make us more productive (just not for free). If you added developers to make a library for you, you would get the same benefit if they could stay as independent.

Once you have independent sub-teams, they can add developers because they are small. At Trello, when I was on the iOS team, I relied on the Trello backend API, but I didn’t need to communicate much with the developers that worked on it, follow their daily PR flow, or go to their meetings. As a principal engineer, I did read their RFC’s, but most of my team didn’t need to. When the backend team grew, my productivity wasn’t affected. Our team was small, so we doubled in size without causing the backend developers any trouble. Our lines of communication increased, but it was manageable.

Independence wasn’t only at the code level. Teams at Trello didn’t have a shared development process. Many teams did sprints, but they didn’t need to sync with each other. Teams that worked on the same deployable had to communicate more, but it was more hub and spoke rather than every possible pair. One way we did this was to have a role called a “feature lead” who was a focal point of communication. They would have a line to each developer on their project, but could help the rest of the team members stay independent from each other.

So, to add developers with less communication overhead, create small, independent teams. To the extent that there are dependencies between them, try to concentrate that in a few people, rather than spread the effect to everyone.