Photo by Ivan Bandura

Maximizing flexibility and minimizing rework in software development

Flexible software Nov 30, 2021

I recently wrote about why future-proofing as we generally think of it isn’t possible, and why the next best thing we can work towards is flexible software. I followed up by writing about how companies can successfully build a culture that aligns everybody to the software planning and development process, which improves how forward-looking your software might be.

Now in this third post, I want to talk about real-world, practical strategies that can help minimize how much time and effort is spent reworking software. Since we can’t actually “future-proof” anything we can’t completely eliminate rework, but we can do a few things to minimize it.

It’s important to understand that many of these principles require some extra up-front work. Just as you wouldn’t consider building a house without taking time to plan it first, you shouldn’t approach your software development without taking time for similar efforts.

I’ll use a hypothetical “Uber for pet sitters” to illustrate these principles. Let’s call this hypothetical company and software UberSitter. Unimaginative, yes, but unfortunately, the branding agency was unavailable to help us achieve a better name for our service.

Build modular software

Here, modular means that the software is constructed as different components that can communicate with each other, without being inherently tied together.

Horizontal modularity (layers)

Software is often built using “layers” as a type of modularity. Database, data access, business logic, API, user interface – these are examples of typical layers in a large software system. This type of modularity can be useful because each layer generally only needs to know about the layer below it.

The power of this approach comes when a change needs to be made. The effects of any change can be reasoned about one layer at a time, through as many layers as required, rather than needing to consider the entire impact throughout the entire application all at once.

Let’s suppose that for some unfortunate reason UberSitter needs to swap out one type of database for another. In this hypothetical scenario, if we built the application with a well-designed, layered approach, then only the data access layer needs to be told how to use the new database. All the layers on top of it can hum along blissfully unaware that the bottom foundational component was swapped out. Swapping out a database is never easy, but it is at least possible because of our modular architecture.

If we decide we need a per-sitting mobile app in addition to our web application, the new mobile app can talk to the API layer and doesn’t need to be aware of any of the details of all the layers beneath it.

Vertical modularity (services)

If layers are how software can be split up horizontally, “services” are a way of splitting the software vertically. With the UberSitter system, the billing and payments process would likely be handled by one service, for example. Then, a separate service might be continually working to match the perfect pet sitter with each pet sitting request that comes in. A third system might handle the sign-up of new pet owners, and a fourth could handle conversations between sitters and owners once a match is made.

The important principle with services is keeping individual components as independent from each other as possible. If you decide to branch out from pet-sitting for cats and dogs and add support for reptiles in UberSitter, your billing system might need to know a few details about how much to charge for the new type of pet. But the billing service doesn’t need to know all the rules about how to match sitting requests with potential sitters – that’s the job of the request matching service.

Well-designed and modular software can have features added to individual modules, and the impact on other systems is relatively minor compared to applications that aren’t modular. This way new features can be added more quickly, and with less risk.

Automate your testing

Testing is an area that requires some amount of effort upfront, but when done properly pays significant dividends over time.

There are a lot of different types of testing that a good software development team will employ. I won’t go into detail about all of them here, but anybody involved with the software developed at a company should be aware of what sorts of tests are available.

There are a lot of differing opinions about testing, usually revolving around how much is enough vs. how much is too much. The discussion can be very situational, and the right answer depends on the circumstances of the team, the company, and the software being built. The software controlling a critical component in an airplane is naturally going to have a very different approach to testing compared to the Slack bot someone threw together to suggest where the team should go for lunch every day.

My opinion on the sweet spot for software testing:

  • The “further up the stack” a test operates, the more value it usually brings. If you have a test that fully scripts every action a pet owner can take in the UberSitter web application, most bugs that those types of users would run into can be found on an automated basis.
  • Anytime there is a bug in production, add a test to make sure that bug never crops up again. If a bug keeps coming up during development and testing, add it too just to make sure!
  • Test must be maintained just like the rest of your application code. The more tests you have, the more maintenence you will have to deal with. Because of this, its certainly possible to have too much of a good thing.
  • Some amount of performance testing is helpful, especially when it focuses on areas where a performance degradation will be most felt. If a new deployment suddenly causes pages to take 6 times longer to load, this will usually be quickly felt immediately and can be quickly corrected by the developers. Having 20 deploys in a row which each add a small increase to page load might not be noticed, but the end result would be the same. Basic performance testing can let you know when your application is slowing down your users.
  • Acceptance testing (for bug resolution and feature implementation) should be performed by whoever reported the bug or whoever requested the feature. This is talked about in one of the many timeless posts by Joel Spolsky.

When tests are automated, features can be produced more quickly without having to invest a ton of time into trying to find every possible bug that could be introduced. Since it isn’t possible to anticipate every bug that could come up and manually test for them, automated testing can catch bugs that you never would have thought to explicitly look for.

Stay in the mainstream

It can be fun to try a trendy new framework, library, or language. Letting engineers explore and try new concepts can have a lot of positive returns, but as soon as frameworks, libraries, or languages used in production fall out of favor it can cause you a lot of headaches.

If you find yourself using a framework or specialty software that won’t be updated anymore, you are left with the choice of maintaining the framework or database yourself or replacing it. Both are painful, time-consuming, and expensive choices.

Another benefit of staying with mainstream technologies is that it is much easier to hire team members who are familiar with those technologies. You won’t be stuck waiting for the rare (and expensive) developer who actually knows how to work with your choices.

There are definitely situations where a specialized, non-mainstream technology is the right choice, but those scenarios are infrequent and should be arrived at very carefully.

Mitigate dependencies

You can’t completely avoid having your software be dependent on various components, service providers, and platforms – but you can help mitigate the risk associated with using those dependencies.

One of the components in our hypothetical UberSitter is mapping, since we pay sitters for travel to the pets they are sitting for. If we build our software in a way that a particular mapping provider is assumed to be the only way to get that data, we could find ourselves paying a lot more when price increases happen. Or, perhaps the provider we choose stops providing the service. Both of these scenarios happen all the time and spending some time upfront mitigating that dependency can greatly increase your flexibility.

A very common dependency for modern software is cloud providers. Wherever you run your software, you are invested in a relationship that you hope is mutually beneficial – but you may find yourself wanting or needing to make a change in the future. Azure, AWS, GCP, all of them will do whatever they can to keep you locked into their service.

The more you can do from the beginning to provide yourself choices in where you can deploy your software, the more flexibility you will have. Luckily, there are more options than ever before for mitigating this particular dependency.

Cloud and mapping providers are just two examples out of thousands. Always be aware of your dependencies, and make sure to treat them appropriately.

Buy, don’t build

Considering whether to build or buy software can be an interesting exercise. Generally, for the greatest flexibility, you should lean heavily towards the “buy” side of the equation barring strong reasons to build the software yourself.

Purchased software usually has the following advantages:

  • It is almost certainly cheaper to buy software than to build it after you account for all the costs involved.
  • Purchased software is likely more flexible, having been built for a variety of paying customers across various use cases and industries.
  • It is probably less buggy than something you would build internally since it has been getting battle-tested, real-world usage.
  • The software is usually ready to go right now – no waiting for development to complete.

There are exceptions to every rule, but the less you have to build yourself, the more you can invest in building what only you can build.

A few more principles

Many of the commonly-accepted best practices in software development will help you be flexible:

  • Avoid “premature optimization”. The more code you have surrounding a particular process or feature, the more locked in you will find yourself. The least-constraining code is the code that you never write. There are lots of reasons to not spend weeks writing hundreds of lines of code optimizing the last 1% of a process that is used by 5% of your users – but it makes your code less flexible on top of everything else.
  • Along those same lines, don’t put features or approaches in “just in case” they might be needed. Architect the software so that unplanned future features can be implemented as easily as possible, but don’t actually start building to those “what-ifs”.
  • Put logging and monitoring in place. Just as testing can automate some of the steps in building software, good monitoring and logging can automate a lot of your day-to-day operations and production troubleshooting. This gives you the flexibility to worry about other things, and confidence to build new things without over-worrying about the unanticipated impact on your production systems.
  • Organized, documented, source-controlled, peer-reviewed code can be expected to run better, have fewer bugs, and be easier to build on when you need to come back to it in the future. Naturally, all of these increase flexibility by improving the ability to easily add to the codebase.

It takes real effort to make sure your software is flexible. That up-front cost will earn you real dividends though, for as long as your software is being developed and used. The cost of software development is significant, and everything you can do to reduce that will dramatically impact your bottom line.

Software development also takes time. The more time you take to add functionality, the more of an advantage you offer to your competitors.

In the absence of being able to gaze into a crystal ball and see the future, aligning your development approach and strategy to favor flexibility is one of the most important steps you can take.