skip to content

Why does software development slow down?

Published on • 11 min read min read


Starting a new software project is always easy. The blank repo and prior lessons learnt make you think “This time will be different”. You install your standard packages, set up a schema, and start building out the first key feature. By the end of day one you might even have that feature working - nice. You show the team/client/customer, and they’re impressed. “Wow, that’s come together super quick. At this rate we’ll have a whole SaaS in three days!“.

But three days turns into five days turns into two weeks. You might’ve finalised that first feature and gotten the project deployed with some auth, but bugs are already starting to show up, and you just spend three hours trying to get the content of an inner div to scroll without pushing everything off the page. Oh, and don’t forget the list of outstanding emails, messages, and support requests from other projects you’ve been putting off. How did we end up here again?

Having shipped 12 projects to production over the past 18 months, this is something I’ve started feeling recently. All those projects require some level of maintenance or are continuing to be built out with additional features. It’s becoming harder to make large chunks of progress, and this progress usually comes at the cost of ignoring other projects.

Working v.s. production software

There’s a time and place for quickly built software, but rarely is this a good foundation to continue building on. I’ve taken this approach with some of our products to validate an idea and get commitment from an early customer. One to two days max to cobble together some pages, display data, and get the most basic feature doing something. The demo is always done on localhost.

Then the customer says yes and the race is on. However, people struggle to understand why something that took a day to build will now take a week or longer to just get it live. “It’s there on your screen though. Can’t we just send them a login?“. You begin to explain how there isn’t even any authentication yet, followed by a long list of other items that will be needed - half of which they don’t understand. 1 The other one that crops up is the “but it works” and “it’s good enough” mentality. Ummm… yeah, until it doesn’t, which won’t take long in its current state.

I think the issue here is that non-technical people believe developers are just making up a bunch of things to do for the sake of it. Sometimes that’s true, but mostly it comes from a place of fear. Fear from the experience of prior projects. The developer knows the state of the code better than anyone else and knows they’ll be the ones people come to when things break. So they want to do everything possible to avoid that from happening.

Time to do it right, or time to do it twice?

In the end, it comes down to execution vs market risk, and being able to effectively communicate the trade-offs. If there’s more market risk, then you want to err on the side of scrappy until the economics make more sense. There’s a chance it might not work out, so having the best error handling and observability could be wasted time. But the business must understand you’ll need to go back and improve things if it does work out.

If however, you know there’s a market for your product and you’re clear on the requirements, you can afford to spend more time getting it right from the beginning.

My general rule of thumb is to reduce scope not quality. As soon as it’s able to be released for feedback, you should release it. But that feedback from the customer should be constructive and guide your next step, not all about the bugs they’ve encountered or half-working features. That just puts a sour taste in everyone’s mouth.

The myth of finished software

The closest I’ve come to finished software is one of my first projects Prestart. Since releasing the initial version, I can count on one hand how many times I’ve needed to go back and make changes. The reason?

  • It contains just the right amount of features to make it useful and nothing beyond that. You can get an idea by looking at the changelog.
  • There’s minimal reliance on external platforms. Only Postmark and Twillio, and they’ve been the cause of most changes.
  • It started as a side project for fun, so I built it with passion and on my own time. I wasn’t thinking about deadlines or opportunity costs.
  • It appeals to a small niche who want something basic and easy to use. There are dozens of other platforms with 10 times more features out there, so naturally we only attract people who don’t make requests.

The opposite is true for almost all other projects I’ve worked on. Heavy integrations with other platforms, natural scope creep just to maintain feature parity, clients paying for your time, or trying to build a viable business and get a return on investment.

Once you’re in that situation, there’s now a baseline amount of time needed to maintain everything. If that time ends up being two days a week, you’ve essentially cut your capacity to move forward in half. When you do manage to make progress on new items, that only adds to the baseline until you find yourself doing nothing but maintenance.

Part of this maintenance is customer support. People will always find a way to run into edge cases, and we’re naturally bad at estimating how often this will happen. Lindie, another one of my products, is a great example of this. When I initially built it, authorising your Linear account during signup would create a new account in Lindie. However, I didn’t allow a second person authorising from the same Linear workspace to join the existing workspace in Lindie. Instead, I create a duplicate account. “That’s not going to happen often though. Maybe 1-2% of the time” I thought. “Surely they just use the invite user feature”. And while I was correct, I wasn’t expecting 400 accounts to sign up. Before I knew it, I was going through the database manually fixing 8 duplicate accounts.

The other key aspect is the miss-alignment between schedules. 2 Developers naturally want to batch work of similar context together as much as possible as it allows you to build up momentum and get more done for the same amount of time. But as I’ve found out, it makes for a terrible experience when applied to customer support. Instead, two days worth of support ends up costing much more than two days of lost productivity if spread out over the week at random.

What’s the problem anyway?

The hard thing about the situation isn’t necessarily the workload or pressure, but rather the lack of understanding between technical and non-technical people. Customers and stakeholders act surprised when bugs are introduced or projects take longer than expected when this is just the reality of software development. From a developer and team point of view, this can lead to low morale, resentment, and shame. There’s a constant feeling of having done something wrong. Every time.

I understand where the business is coming from though. Time is money. But if you’re margins rely on how quickly something gets done, software ain’t the best business for that! This is the fundamental reason why I’ve been making the change from agency work to products and joint ventures. When you own the asset you’re building your margins are now based on how many businesses or people use your product. If something ends up taking twice as long as expected (say two weeks instead of one) it doesn’t matter as much. Everyone ends up getting the feature still, and the opportunity cost is just when you get started on the next thing. You’re using the nature of software to your advantage, not working against it.

Managing expectations

Trying to avoid the slowdown or pretend like it’s not a thing only adds to the problems mentioned above. You fail to plan for the unplanned work, and you can imagine the downward spiral that occurs.

That’s why I’m relentless in recording everything I do in Linear. Even the smallest of items or bug fixes have their cost. 3 With this, you’re able to look back and get a sense of how much-unplanned work to expect in each cycle. So if you start at 100% of your usual capacity, it’s easier to let the business know not everything will get done. You have past evidence to refer to.

You could also implement more of a shape up method, where you start by asking the business how much time they’re willing to spend on a feature or project, and the team builds to that time constraint. 4

Preparing for it technically

The projects I’m building aren’t crazy big, so I find structuring via feature-based folders naturally works well for the front end, and entity-based for the back end. This is because the user interface is naturally based on features and the database around entities.

Apart from some very common helper functions and the UI library, almost everything goes into these feature folders. A simple eslint rule that restricts imports to the index file acts as a forcing function for loose coupling.

'no-restricted-imports': [
patterns: ['~/features/*/*']

In practice, I’ve found it much easier to build new features from scratch than using some shared abstraction to avoid “repeating myself”. No matter how entangled or complex a feature becomes, it never impacts my ability to build something new. If one feature has something I could use for another, I simply copy the code and adapt it to the new use case. What if the thing I copied ends up changing you might ask? I change it in both locations… a crazy concept. The reality is that two features are more likely to drift apart than converge. 5

I’m also taking the time to refactor where it’s required. That usually means I’m expecting more items to be built out in that area, or there’s a whole bunch of bugs it’s simply easier to start from scratch and build the feature back up again. This could be splitting a large component up while fixing a bug, or dedicating an entire cooldown week to refactoring a big part of the platform. In general, always try to leave the code better than when you found it. The repository should be treated like a garden, not a junkyard.

Final thoughts

While some of these items can be frustrating, it’s not all bad news. They can be good signs as well. People pushing your product to its limit, hammering you with feature requests? Sounds like a product market fit. Do you need to refactor old code to make it easier for new developers to join the team? Sounds like you’re growing. If you’re able to take a pragmatic approach to these “behind the scenes” items and get buy-in from the broader business, then it’s just part of the journey. Don’t forget to enjoy it!


  1. Non-technical people not understanding is okay by the way. It’s not their job to understand.

  2. There’s a great Paul Graham essay on this that’s worth reading.

  3. Even if it’s a one-line change and you immediately know what to do, the context switch, pull request, deployment, and slack messages are worth at least one estimate point.

  4. This works best for homegrown products and medium-sized teams, but you can take a similar approach with client projects. Rather than providing a quote, you ask how much they’re willing to spend on a feature, and you let them know what’s possible with that budget. A good relationship and a high amount of trust are needed for it to work though. Clients always seem to think you’re trying to rip them off…

  5. Obviously if you end up doing the same four or five times, and predict you’ll need to keep doing it, then there’s the change for abstraction. Things like middleware, or an info tooltip. But if you think you could share a part of a component or function between two features, it almost always ends up being more painful to try to account for both use cases than simply copying the code.