Have you ever had to change existing business logic that was hard to understand? Did it break unrelated parts of the system when it was changed? Perhaps it had really poor documentation.
Some of the most difficult software projects I’ve worked on involved working with and maintaining existing code. In one of those systems, much of the business logic was all over the place: in the front end, on server-side code, in database stored procedures, and across multiple web applications that were written in different programming languages! It was near impossible to change one of these “separate” websites without having to change the other, since they all shared the same data schemas.
I was asked to lead the design of a mobile application for an organization. The application that contained the core business logic we needed to expose via an API was written in Visual Basic —an old programming language that even years ago was considered obsolete. Because of this, we couldn't just simply reuse that code in our API, we had to first port the code over to C#. Since the organization had no documentation or co-workers who knew how the business logic was supposed to work, we painstakingly figured out each piece of business logic and ported it in a way that was afterwards easier to understand, less error-prone, more performant, and was compatible with the API.
In this article, you'll learn about why business logic tends to change over time and can lead to unmaintainable code. Finally, you'll gain some techniques to proactively defend against changing business logic or, if you must, improve business logic that's difficult to understand and change.
Let's take a look at some of the most common reasons business logic tends to change so much in modern software systems.
There are times when you release a new feature or product to your existing users and, over time, they begin to use your software in a way you didn't expect. For example, you may have built a "Trello-like" product where users can drag and drop cards onto different boards. What if you found that users kept on using your product as a way to manage their projects? Well, you might start changing your product to meet that need better in hopes of producing more customers and pleasing existing customers (i.e. product-market fit).
New products or features that you create may need to reuse existing code or components. Some questions you need to ask include, "Do I need to make certain parts of my business logic reusable across my codebase?" In most cases, this means taking the existing code for your business logic and refactoring it somehow.
The way your organization prices your product will change over time. What if your product moved to a subscription-based model where your customers had to pay per active user in your system? You might have to change logic around user management UIs, how your product handles disabled users, how the active user count at billing time is calculated, etc.
What if your organization has had a boom of new customers in the last year—and now the overall load of your infrastructure is starting to have issues around performance and availability? In this situation, you may choose to take certain high-traffic business-use cases and extract them as more scalable services/processes to help with scalability concerns.
In growing organizations, you may find your development team being further divided into new teams. If different teams are responsible for different areas of the product then you may need to change where the business logic resides. You may need to extract existing logic to separate out systems and/or code repositories that are owned and maintained by specific teams.
When faced with changing business logic, there are a few reasons those changes are even more difficult to make. The following are some of the most common reasons.
The underlying data schemas and models in your software systems may be shared. This means that they are coupled together. It's hard to change one system without having to change others because they all use the same database.
Have you ever had to change an existing codebase that had no documentation? It’s very difficult. Before you can make your changes, you have to dig into the fine-grained details of the code and then grasp how the wider context works. And, if you have no automated tests to rely on… Well, good luck!
Existing code that you are working with may be poorly written due to a lack of architecture and design, or because your organization is a feature factory. Some developers, when needing to make changes to existing code, will just add one more
if statement. And then one more. Then one more. Next thing you know, you've got a mess of unmaintainable code.
A lack of understanding of a true business problem can lead to writing code that solves it in a naive and poor way. Changing aspects of a solution like this may be more difficult to change because it doesn't match the reality of the business domain.
Software code "rots" over time because of changing environments and changing business logic. The following tips can help you defensively develop your software in a way that supports more easily changing business logic in the future.
Following general coding practices can help to make sure your code is easier to understand. Some of these practices include:
Keeping your methods and classes small
Following the single responsibility principle
Naming your variables well (understanding your business domain helps)
Following the boy scout rule
Considering the use of dependency injection
Having unit and integration tests can partially solve the issue of having poor documentation. Having your code under test means that you can more confidently change existing code without worrying that you broke something.
Having code that's testable, in general, leads to code that's easier to understand and extend too. It also makes you have to stop and think more carefully about what your code is doing and how it reads.
Whenever you need to build a part of your system that's not a core problem your company is solving, it might make sense to use a third-party tool. For example, if your software helps employees to schedule their vacation and time off, then using a third-party service like Cerbos for handling access management means that you wouldn’t have to write as much code. You also wouldn’t have to learn about the fine-grained details of how to "do" access management properly. In other words, you avoid much of the maintenance burden of keeping that code up-to-date.
Often, as software developers, we think that we understand the true problem we've been asked to solve immediately after our first meeting about the business need in question. Usually, we don't truly understand it.
Try to take some time and think of different scenarios that lead to edge cases, ask tons of questions to those who are asking you to solve their problem, try to model the problem using post-it notes, etc. The more ways you can immerse yourself in the problem, the better.
Design patterns are reusable patterns that can help you solve a problem that others have already solved. They also serve as a means to communicate your solutions with other developers in a way that they will quickly understand the general approach to them.
For example, if you solved a particular problem by using the strategy pattern, then you could tell another developer that your solution uses this approach. That other developer should now have a general idea of how the solution’s code is structured.
It's hard for multiple teams to work on the same codebase. You end up with lots of version control conflicts, issues around deploying, and what infrastructure is available to use.
Ideally, individual development or cross-functional teams should be able to work autonomously. This means that the systems they work on, as much as possible, should be decoupled from those of other teams. For example, a team should be able to deploy their systems without affecting or depending on another team.
While this helps teams to accelerate the pace of their development, it can also lead to smaller independent systems that are, in general, easier to maintain and extend than one massive monolithic codebase.
ADRs, for short, help to bring understanding to your software systems for any future developers who will work on them.
Ever work in a system where you thought “why did they do that?” Many times, we lack the context that surrounded the decisions that previous developers made in regard to how code might be structured or written. Keeping track of these major decisions in a way that captures the context and reasons the decisions were made can help future developers to understand the code they are working with much better.
As your software systems grow and adjust to the ever-changing needs of your business, keeping a certain overall level of code quality is essential. If your company wants to keep building new products and features into the future then a constant focus on keeping your business logic maintainable and flexible to change is non-negotiable.
Fine-grained details like the naming of your variables, to higher-level details like how large-scale systems fit with individual development teams all play a part. Having reviewed this, you now have some more techniques and principles to use the next time you are asked to solve another business problem!
James is a Microsoft MVP with a background building web and mobile applications in fintech and insurance industries. He's the author of "Refactoring TypeScript" and creator of open-source tools for .NET called Coravel. He lives in Canada, just ten minutes from the highest tides in the world.
Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team