Programming advice

I start a new Software Developer job next week, and I came across this blog post by Marcus Buffett (via Theo - t3.gg). His observations hit home for me, and I want to reflect on the advice and how it might apply to my new role.

Marcus starts with:

I finally have the feeling that I’m a decent programmer, so I thought it would be fun to write some advice with the idea of “what would have gotten me to this point faster?” I’m not claiming this is great advice for everyone, just that it would have been good advice for me.

This resonates with me, as I feel like I'm finally at a point where I feel like decent programmer. Let's take the points from the blog post and discuss them.

If you (or your team) are shooting yourselves in the foot constantly, fix the gun

How do we make it harder to make mistakes?

I’m not trying to be like “look at these idiots not fixing an obvious issue with the codebase, when it was obvious to me”, because it would have been obvious to anyone who thought about it for a few minutes. These things stick around a weird amount, because there’s never a natural time to address them. When you’re first getting onboarded, you’re not trying to change anything big. So you may think it’s weird, but you shouldn’t go changing a bunch of things you’re still learning about. Then when you’ve been on the team for a while, it sort of fades into the background.

This is timely advice for me as I join a new team. I want to make sure that I use my time-limited fresh perspective to at least help identify some of these issues, and hopefully help fix them.

Assess the trade-off you’re making between quality and pace, make sure it’s appropriate for your context

There’s always a trade-off between implementation speed and how confident you are about correctness. So you should ask yourself: how okay is it to ship bugs in my current context? If the answer to this doesn’t affect the way you work, you’re being overly inflexible.

Well put. I'm wrapping up my work with a four person start-up, where we released features quickly. While we tested as best we could, we were ok with shipping the occasional bug, as long as we could fix it quickly. We had a very fast shipping cadence, and we were okay with that trade-off.

I expect that in my new job the trade-off will be different. With more resources, more customers, and more at stake, the trade-off will be more towards quality. That said, I still want to be able to ship quickly, so it's important not to veer too far in the other direction. The product is not life-or-death, nor is it underpinning a critical piece of internet infrastructure, so there is a balance to be struck.

Spending time sharpening the axe is almost always worth it

You’re going to be renaming things, going to type definitions, finding references, etc a lot; you should be fast at this. You should know all the major shortcuts in your editor. You should be a confident and fast typist. You should know your OS well. You should be proficient in the shell. You should know how to use the browser dev tools effectively.

This one triggers a smidgen of imposter syndrome in me. I don't know every shortcut in my editor, I'm not the fastest typist. I get a pang of envy when I see an expert vim user making crazy edits in seconds. On the other hand, working as a consultant has taught me that the majority of developers I work with are far from experts in their tools. I often find myself explaining how to do things in their editor, or how to use the shell effectively. The number of times I've used "Show all Symbols (⌘T/Ctrl+T)" in VS Code and the person I'm working with goes "What was that?!" is too high to count.

I think the key here is to be proficient enough in your tools that you can work quickly and efficiently. You don't need to be an expert, but you should know enough to not be slowed down by your tools. I think I qualify here, but I can always improve.

If you can’t easily explain why something is difficult, then it’s incidental complexity, which is probably worth addressing

Love this, particularly the distinction between incidental complexity and essential complexity. A quick explanation: essential complexity is the complexity that is inherent to the problem you're trying to solve, while incidental complexity is the complexity that arises from the way you've chosen to solve it.

It's hard to tell the difference early on, when you're still exploring the problem space. Hindsight helps a lot here, but event then it can be hard to tell. It might not be until several years later that you realise a problem could have been solved in a much simpler way.

My favorite manager in my career had a habit of pressing me when I would claim something was difficult to implement. Often his response was something along the lines of “isn’t this just a matter of sending up X when we Y”, or “isn’t this just like Z that we did a couple months ago”? Very high level objections, is what I’m trying to say, not on the level of the actual functions and classes we were dealing with, which I was trying to explain.

Having others challenge your assumptions is a great way to identify incidental complexity. It's easy to get caught up in the details and lose sight of the bigger picture. Having someone who can ask the right questions and challenge your thinking can help you see things from a different perspective.

A trap I sometimes fall into is thinking part of my solution is an absolute requirement. It's often when a colleague asks my why I'm doing something a certain way that I realise it's not actually necessary, and I can simplify my solution. And to be clear, this is a trade-off. It means we're not handling/supporting a certain edge case, or we're not doing something that might be nice to have. But if it's not essential, then it's worth considering whether it's worth the complexity.

Try to solve bugs one layer deeper

Marcus describes two types of bug fixes:

  1. The first type is where you fix the bug by changing the code that is directly related to the bug. This is often a quick fix, but it doesn't address the underlying issue.
  2. The second type is where you fix the bug by changing the code that is one layer deeper than the bug. This often takes more time, but it addresses the underlying issue and prevents the bug from happening again.

Keep doing the first sort of bug fix, and you end up with a mess. Keep doing the second type of bug fix, and you’ll have a clean system and a deep understanding of the invariants.

I feel this one a lot. It's so easy to just fix the bug in front of you. Often to the appreciation of the product owner and the customer. Sometimes this is the right things to do, but often it's not. Definitely one where experience helps. Either end of the spectrum is problematic: if you only ever do the first type of fix, you'll end up with a messy codebase that is hard to maintain. If you only ever do the second type of fix, you'll end up spending too much time on bugs and not enough time delivering features.

Don’t underestimate the value of digging into history to investigate some bugs

I've been lucky - unlucky? - enough to have been the sole developer on my main current project. I wonder if this is why I don't go back and look at the history as much as I would expect.

I’m not sure what the rule is here for when to do this and when not to. It’s intuition based, a different sort of “huh” to a bug report that triggers this sort of investigation. You’ll develop the intuition over time, but it’s enough to know that sometimes it’s invaluable, if you’re stuck.

Bad code gives you feedback, perfect code doesn’t. Err on the side of writing bad code

If you err on the side of writing code quickly, you’ll occasionally get bitten by a bad bit of tech debt. You’ll learn stuff like “I should add great testing for data processing, because it’s often impossible to correct later”, or “I should really think through table design, because changing things without downtime can be extremely hard”.

This is something I think about a lot. I do tend to start by writing something that mostly works, then make it correct, and then consider factors such as performance. Performance is a tricky one, as it's often not a problem until it is. Sometimes you can't bolt on performance later, but often you can. Hmmmm, I need to think about this more!

Developing an intuition for when to crank up the quality is valuable. For one client, I've had success carving out some areas of the codebase that we know are business critical or will be difficult to change in the future and devoting more time to them. More thought, more time spent in design, more time spent on testing. There's also a higher bar to changes being merged which touch these areas.

Make debugging easier

Bugs should get easier to fix over time, all else being equal.

It's an aspirational goal, and I've not thought about it this way, but I think it's a good goal to have. The "all else being equal" is doing the heavy lifting here. As complexity and complication ramp up, debugging will get harder. But perhaps that's a reason to make reproduction and writing tests easier?

I have been debating the merits of writing writing custom tooling to make debugging easier. Marcus' example:

I have a command to copy all of a user’s data down to local, so I can reproduce issues easily with only a username

is a good one. I have a similar need to copy data for a particular project from the database to my local machine. I've been doing this by hand, but I think it would be worth writing a script to automate this. It would save me time in the long run, and make debugging easier.

When working on a team, you should usually ask the question

There’s a spectrum of “trying to figure out everything for yourself” to “bugging your coworkers with every little question”, and I think most people starting their careers are too far on the former side. There’s always someone around that has been in the codebase longer, or knows technology X way better than you, or knows the product better, or is just a more experienced engineer in general. There’s so many times in the first 6 months of working somewhere, where you could spend over an hour figuring something out, or you could get an answer in a few minutes.

I definitely fell into the trap of trying to figure everything out for myself when I started my career. Colleagues expect competence, not omniscience. Another way of looking at it is that you're doing a diservice to your team/company if you don't ask questions. If you spend an hour trying to figure something out that someone else could have told you in a few minutes, you're wasting your time and your team's time.

Ask the question. The only time this will be annoying to anyone, is if it’s clear you could have found the answer yourself in a few minutes.

This is where it can get tough for me. There's a voice in my head that says "the information is out there already, I must have missed something". It's not a huge leap from there to "given I can't find the answer, I should probably work it out myself". I know this is a trap, and I need to err on the side of asking more questions than I feel comfortable with.

It's funny but I think I ask more questions now than I did when starting out. Definitely working as a consultant has helped with this, as I've had to ask a lot of questions to get up to speed quickly. There's also a bit of a status thing going on here. If I feel like I'm the expert in the room, I feel more comfortable asking 'dumb' questions. I know what I know, and I know that I don't know everything. So if I ask a question, it's not because I'm clueless, it's because I'm trying to get a better understanding of the problem.

Shipping cadence matters a lot. Think hard about what will get you shipping quickly and often

Ideally, your speed on a project only compounds over time, until you’re shipping features faster than you could have imagined. To ship fast you need a lot of things:

  • A system that isn’t prone to bugs
  • Quick turnaround time between teams
  • A willingness to cut out the 10% of a new feature that’s going to take 50% of the engineering time, and the foresight to know what those pieces are
  • Consistent reusable patterns, that you can compose together for new screens/> features/endpoints
  • Quick, easy deploys
  • Process that doesn’t slow you down; flaky tests, slow CI, fussy linters, slow PR > reviews, JIRA as a religion, etc.
  • About a million other things

Shipping slowly should merit a post-mortem as much as breaking production does. Our industry doesn’t run like that, but that doesn’t mean you can’t personally follow the north star of Shipping Fast.

Such a good point about shipping slowly deserving its own post-mortem. It's a good example of a class of problems that can be overlooked: those that aren't acute, don't cause immediate pain, but are still important to address.

Themes

What strikes me reading back over all this advice is the recurring theme of intentionality—making deliberate trade-offs, and asking the right questions.