Open for extension, closed for modification as an architectural pattern
The Open Closed Principle is part of the SOLID principles. It isn’t new or anything exciting, but I wanted to discuss this today in the context of using that not as a code artifact but as part of your overall architecture.
The Open Closed Principle states that the code should be opened for extension, but closed for modification. That is a fancy way to say that you should spend most of your time writing new code, not modifying old code. Old code is something that is known to be working, it is stable (hopefully), but messing around with old code can break that. Adding new code, on the other hand, carry far less risk. You may break the new things, but the old stuff will continue to work.
There is also another aspect to this, to successfully add new code to a project, you should have a structure that support that. In other words, you typically have very small core of functionality and then the entire system is built on top of this.
Probably the best example of systems that follow the Open Closed Principle is the vast majority of PHP applications.
Hold up,I can hear you say. Did you just called out PHP as an architectural best practice? Indeed I did, and more than that, the more basic the PHP application in question, the closer it is to the ideal of Open Closed Principle.
Consider how you’ll typically add a feature to a PHP application. You’ll create a new script file and write the functionality there. You might need to add links to that (or you already have this happen automatically), but that is about it. You aren’t modifying existing code, you are adding new one. The rest of the system just know how to respond to that and handle that appropriately.
Your shared component might be the site’s menu, a site map and the like. Adding a new functionality may occasionally involve adding a link to a new page, but for the most parts, all of those operations are safe, they are isolated and independent from one another.
In C#, on the other hand, you can do the same by adding a new class to a project. It isn’t at the same level of not even touching anything else, since it all compiles to a single binary, but the situation is roughly the same.
That is the Open Closed Principle when it applies to the code inside your application. What happens when you try to apply the same principle to your overall architecture?
I think that Terraform is a great example of doing just that. They have a plugin system that they built, which spawns a new process (so completely independent) and then connect to it via gRPC. Adding a new plugin to Terraform doesn’t involve modifying any code (you do have to update some configuration, but even that can be automated away). You can write everything using separate systems, runtime and versions quite easily.
If we push the idea a bit further, we’ll discover that Open Closed Principle at the architecture level is the Service Oriented Architecture. Note that I explicitly don’t count Microservices in this role, because they are usually intermixed (yes, I know they aren’t supposed to, I’m talking about what is).
In those situations, adding a new feature to the system would involve adding a new service. For example, in a banking system, if you want to add a new feature to classify fraudulent transactions, how would you do it?
One way is to go to the transaction processing code and write something like:
That, of course, would mean that you are going to have to modify existing code, that is not a good idea. Welcome to six months of meeting about when you can deploy your changes to the code.
On the other hand, applying the Open Closed Principle to the architecture, we won’t ever touch the actual system that process transactions. Instead, we’ll use a side channel. Transactions will be written to a queue and we’ll be able to add listeners to the queue. In such a way, we’ll have the ability to add additional processing seamlessly. Another fraud system will just have to listen to the stream of messages and react accordingly.
Note that there is a big difference here, however, unlike with modifying the code directly, we can no longer just throw an exception to stop the process. By the time that we process the message, the transaction has already been applied. That requires that we’ll build the system in such a way that there are ways to stop transactions after the fact (maybe by actually submitting them to the central bank after a certain amount of time, or releasing them to the system only after all the configured endpoints authorized it).
At the architecture level, we are intentionally building something that is initially more complex, because we have to take into account asynchronous operations and work that happens out of band, including work that we couldn’t expect. In the context of a bank, that means that we need to provide the mechanisms for future code to intervene. For example, we may not know what we’ll want the additional code to do, but we’ll have a way to do things like pause a transaction for manual review, add additional fees, raise alerts, etc. Those are the capabilities of the system, and the additional behavior would be policy around building that.
There are other things that make this very attractive, you don’t have to run everything at the same time, you can independently upgrade different pieces and you have clear lines of demarcation between the different pieces of your system.
This is the topic that is not often talked about. Or maybe, that is not phrased that way. Being open for extensions. Of course, depending on underlying mechanisms there's a lot to discuss in regards what/how things should be published/streamed, but the underlying premise of extension is not what I see that often.
I don't think that I'm following you here.
If we're only thinking about making web applications then the PHP example is great - i really liked the simplicity of PHP and its old, posix-style API. But i dont think this analogy applies to building business software in general. Every 'business system' project i have been in has gradually switched from new feature development into neverending changes and modifications of existing functions. And no matter how much thinking you put ahead into planning for future changes and extensibility, the customer will always come up with something that you didn't expect and that goes against your beautiful design. And you have to handle it even if you think it's stupid and ugly. I'm not suggesting here that any architecture planning is worthless and you should start from spaghetti to avoid frustrations in future, quite the opposite: you have to actively modify your design to keep it extensible, consistent and future proof and to stay this way you need to keep making deep changes.
That is something that is generally a good thing. At some point the system grows up and ends up at a certain stable state. At this point, you rarely do major changes but just small ongoing maintenance. But every now and then, you add complete additional behavior, and then the long term cost of the system comes into play.
Another way to look at this, down the line, the idea is that we can limit the blast radius of changes in the system.
A good example where the principle applies: assume you build a software product that you want to offer to companies on the market. So your goal is to have a product meeting the needs of multiple customers, not a custom-made application for one customer only. This is quite hard when you're starting the business and just have one or two big customers that pay most of the bills but also have their special needs for higly custom functions applicable only in their companies. You dont want to end up with a separate version of your program for every customer, but also you dont want the customers to pick another vendor who will do everything they want. Then a possible way to handle this is to make the system extremely customizable (at many levels, this includes database structure, integrations, business logic, user interface) and handle customer-specific functions as extensions / overrides to the base system. And when you get a request for something that's totally outside of your extension framework then modify the framework first, then add the extension.
Just one note on this approach. Making things customizable has a cost, sometimes it is a necessary one, but sometimes it is a high cost for no reason. Allowing the addition of separate code for each customer in the system architecture directly is generally simpler than trying to create a customizable system.
I have been saying and doing this for years. Why change existing code when you have the opportunity to write new code? Developers really enjoy writing new code, but more often than not they tend to modify existing code instead. Using things like the strangler pattern, or just implementing code that depends on existing abstractions are often possible and probably faster. I would agree with Scooletz, this isn't talked about nearly enough. Good article!