Photo by Milad Fakurian on Unsplash
In software development, the pressure to deliver features - and deliver them quickly - can be relentless. It is easy for a software developer to get caught up in all of the pressure, and opt to cut corners in order to be able to deliver by a deadline that wasn’t decided by them. Early on in one’s career it can even become a point of pride to be the person consistently able to meet tighter and tighter deadlines.
The problem with setting this kind of precedent is that the deadlines do inevitably become tighter - not necessarily because timelines have genuinely shortened - although that can happen too. More often its the accumulation of all of those shortcuts that, over time, make software increasingly difficult to change. This is the decent into legacy, the big ball of mud, that we as developers are often all too familiar with.
It’s done but…
I think one mistake that many developers, myself included, often make is that we are too eager to say when something is “done”. Again, this calls back to that idea of pride in being able to complete a piece of work quickly. If something is functionally done 1, but doesn’t have tests - it’s not done. If something is *functionally *done, it’s got tests against it, but you’d really like to take a little bit more time to clean the code up - it’s not done.
Leadership types, particularly those without a technical background hear “it’s done but…”, and then a whole bunch of dev jargon that gets tuned out. We can’t blame them, it’s not really their job to know why we need to do this stuff, but it is our job to get a sense for when we can push back against a deadline and when we can’t.
Which brings me to another point: rather than providing a lowball estimate for upcoming work, and then trying to negotiate time for clean up, we should be padding our estimates to account for the fact that we will be writing tests and cleaning up and so on. Our definition of done includes all these things that exist beyond just the functional change: testing, security, error handling, observability etc… and so naturally our estimate for the work will reflect that.
This idea goes along with knowing when we can push back and when we can’t; there will be times when we can’t and we have to take shortcuts, but by pushing back as often as we can we give ourselves leeway to occasionally take those shortcuts when we need to. Even in these cases where we definitely can’t push back and are forced to take shortcuts, we should be pushing very hard for time to circle back and stabilise the work when the deadline is passed.
If you’re reading all of this and still think “yeah cool, wouldn’t work at my company”, you have two options:
- Easy - Find a new job somewhere that already gets software engineering
- Very hard - Dig deep into the problem, to see if there is any broader organisational change you can affect
It’s easy to just say “oh my manager just doesn’t get it” - in my experience it tends to run a lot deeper than that: excessive handovers, conflicting incentives and priorities, risk aversion, hero culture - all these things and more contribute to an organisation with dyfunctional practices around shipping software.
Ultimately though, we are the engineers, we know what we need to do to build things right, and if we don’t do those things we have to take at least some responsibility for the consequences. If a bridge collapses because the engineers took shortcuts, yeah they can argue that they were “forced” to take shortcuts, and maybe in some way they were. At the end of the day though, it’s not management signing off on the work, just like it’s not management that will have to live with years of bad software design decisions.
Fast brain, slow brain
Okay so we’ve estimated high or pushed back on our deadline, giving us time to do the work the way we want to… what now?
One thing I like to think about; and this might sound a little obvious; is just to sloooow doooooown. I don’t know about you all, but when I’m working my brain operates at one of two speeds - blazing fast like the hare, or slow and methodical like the tortoise - without much in between. These two speeds both have their uses:
Be like the tortoise - slow and methodical
Fast brain
Pros: Can pump lots of code out very quickly, particularly the more boilerplate-y kind of stuff. Gets features to a functionally done point really fast. Has the appearance of being incredibly productive.
Cons: Prone to silly mistakes. Tends to miss a lot of the bigger picture. Code tends to be messy and poorly structured, not cohesive, tightly coupled, poorly tested or not tested at all.
Slow brain
Pros: Far less prone to silly mistakes. Sees the bigger picture, helping to write code that is well-structured, cohesive, appropriately coupled, and well tested.
Cons: The necessary depth of thought means work tends to happen at a significantly reduced pace. Appears to be getting less done to those without sufficient context on the work. Hanging out here too long can mean missed deadlines.
Going slow to go fast
The idea of going slow to go fast is not a new one. Martin Fowler has written extensively around the idea that taking the time to ensure sound software design early, while costing a little bit of productivity in the early days of a project, *very *quickly pays off compared to a no, or low-design approach.
Credit: Martin Fowler on martinfowler.com
So what I’m really advocating for here is a healthy mix of both speeds. What often feels appropriate to me looks a bit like this:
Slow ⇒ fast ⇒ slow
To elaborate —
Slow ⇒ A piece of work lands on my lap. I don’t immediately start writing code. I make sure I understand the work, including the why 2. I start by looking at the code where I think I’m going to need to make changes, and putting together a picture in my head of how I’m going to build it. I don’t rush myself through this phase, as I want to be sure to start the work on a stable foundation.
Fast ⇒ I get down a rough draft of the changes - this may be in the form of pseudocode or real code, whatever feels appropriate. This rough draft may technically get me to functionally done; but it is not, I repeat, NOT done. Don’t even use the word “done” at all to describe your work at this point - it’s not worth the risk of being told to ship it.
I like the idea of TDD - test-driven development - but I am not particularly well practiced at it. I’ll usually at least try to test-drive work, but if I find I’m getting bogged down, or I have something truly inspired pop into my head that I just absolutely need to smash out quickly, I’ll put the tests to the side temporarily, knowing I’ll come back to them later.
Slow ⇒ Now that we have our rough draft that is definitely NOT done, we stop for a short time. I encourage taking at least a short break from the computer screen - even better if you can sleep on it and come back in the morning. Thinking about the work while taking a break from the screen is okay, but not required. All that matters is that you take a break; play with your kid, go for a walk, go touch some grass - whatever gets you away from the screen.
What this break does is create a through-line - we separate ourselves from what we’ve written, and can come back to it with a much more objective, critical eye.
Be your own worst critic
We now have the space and the mindset to really critique our work, and here is where the real engineering gets done. It can be thought of as a sort of hardening, although I think the idea of a “hardening sprint” is a bit of an anti-pattern - we should be hardening as we go.
Have we written tests? Do they adequately cover the scenarios we know we need to cover? Are there any scenarios we might have missed? Are we testing any implementation details by accident?
Do I even like the way I’ve implemented the change? Can I make the code clearer? More cohesive? More appropriately coupled?
Is the code sufficiently performant for our needs? Is it secure? If it breaks in production, how will I know? Does it handle errors gracefully or just blow up in my face? Is the code appropriately documented?
All of these things and more are what we can and should be thinking about in the second slow brain phase of work. I hesitate to say it’s the most important phase - all three phases contribute to us being able to deliver quality software - but it is a very important phase, and one that I think often gets skipped. When the second slow brain phase is skipped, this is when we end up with half-baked solutions that might solve the problem now, but set us on a trajectory towards *big ball of mud *territory.
Pragmatic application of the pattern
Sometimes we just really need to pump out a change. I get it, I’ve been in that situation before - and I will undoubtedly find myself in that situation again. One thing that I am not an advocate for is the dogmatic application of patterns in software development and delivery - and that’s all this is, a pattern. Use it when it makes sense to use it, and don’t use it when it doesn’t. I will say that avoiding the hardening work in the second slow brain phase for too long is generally a bad idea for the longevity of a project - broken window theory and all that.
Footnotes
1 I’m using the term *functionally *done to mean that the change captures the functional requirements - the requested new behavior - but very little, or nothing more.
2 Understanding the why of a piece of work is crucially important, especially if one wants to elevate themselves to senior developer and beyond. Being able to have meaningful discussions with product folk for example, about the work we do is a key skill. It’s something I’m still developing myself, but I may write something about it in the future.