Simple problem, simple design
How does the level of complexity of a problem dictate whether to apply SOLID or not? Or any software design principles for that matter? This might look like an easy question at first glance. Especially because of the idea that code should be easy to change, always. So it’s simple, right? However, applying software design techniques when not needed can do more harm than just not applying them. So when is it needed? When should you apply software design and when not?
Problems of software design
Software design has two problems in my opinion, and these problems revolve around complexity. The first problem is that software design is actually kind of hard to explain. You can’t really seem to demonstrate its undeniable benefits with just some simple examples. Pragmatic students, unfamiliar with software design, will keep on asking why you would need whatever magic you just pulled. In order to make software design really shine, you need some complexity.
The second problem of software design is that it adds complexity to a software solution, especially if there is none to begin with. If you have a simple problem, adding abstractions, indirections and design patterns just doesn’t seem necessary. It is overkill. It leans very closely to YAGNI (You Ain’t Gonna Need It). And while thinking about how you can anticipate as many different changes as possible, you aren’t doing anything else. But then why are so many developers triggered to do it anyway?
Let’s start with why we have software design (read more about the importance of softare design here). Software design is the art of enabling change in a codebase. We do that by carefully looking at the problem we need to solve and by using software design principles, patterns and best practices. By enabling change in a codebase, we attempt to contain the inevitable costs of maintaining software. Even if a problem is simple, we can imagine different ways of how it might change. And code should always be easy to change. Therefore, we are tempted to apply design principles and add complexity. You know… in case we do need it. In case change is going to strike down and mess up our code.
Because software design tends to shine with complexity and also because it can add unnecessary complexity, I say that the level of software design you apply, needs to be justified by a somewhat matching level of complexity. In other words, if the complexity of what you are doing is not that high, you probably won’t need too many design patterns (if any at all), you probably won’t need most of the design principles and guidelines. You would want to try and keep things as simple as the problem allows it to be.
Use fundamental concepts for simple problems
Now don’t get me wrong. I am a software design enthusiast. And I do not mean you should not apply software design at all. There is a big difference there. I am saying that what you apply should be justified by a level of complexity. And I think this requires a lot of skill and insight to get it right. I believe that this defines true mastery in software design, or at least a big part of it.
Maybe we need to consider a couple of things in order to avoid misunderstandings. And let’s take SOLID as an example. To me, SOLID are a set of guidelines/best practices which, as a developer, you should definitely know and understand. However, even though I am a big fan of SOLID, I find these principles/guidelines not simple. I can imagine that some people find them easy. Those people probably have tons of experience, and great insight and understanding. But personally, I find most of them quite vague actually. I would say that the SOLID principles (at least most of them) are higher level guidelines. They are higher level in that sense that you need whenever the complexity of the problem you want to solve with code is increasing.
The SOLID principles/guidelines are built upon more fundamental design concepts, like connascence, cohesion, coupling, abstraction and very important: change.I would say in general, for low complexity projects you need to understand the following:
- High cohesion: group code units that accomplish a goal or a use case. This also means that when you create models/entities, you group together everything the model needs to be able to work on its own. No less, no more either.
- Low coupling: You want to isolate those cohesive code units, don’t mingle them with code units that serve a different use case or a different purpose. You want your code to scream what it does or represents (oh and for the record, it shouldn’t scream ‘SPAGHETTI’). In other words, you want a clear structure. Also make sure that your code units only have one purpose.
- Some understanding of change and complexity to be able to evolve the codebase while keeping maintenance costs as low as possible. I think you need a little understanding of how change and complexity will impact your code and the design decisions you are going to make.
I believe that if you understand these concepts and how they affect the ability of your code to change (and in my opinion, they are more easy to understand), you know enough to make educated design decisions for simple problems. You can write code that is relatively easy to change. Most of it comes down to isolating change.
Now another thing we should clear out is complexity. I have been talking about complexity, but what exactly do I mean? With the complexity of a project or a problem, I am mainly aiming at 2 aspects: The complexity of the domain (so the problem itself actually) and the complexity of stakeholders.
The complexity of the domain is about how difficult it is to understand and to model the problem. Is the problem something very familiar (like for example an eCommerce platform) or is the problem very specific to a niche industry? How many different parts are there to take into account? Sometimes some domain concepts look alike but aren’t quite the same. How are you going to model that? These are all factors that will impact how easy it is to understand the problem and to model a solution for it. And the more complex this domain is, the more effort you need to put in designing it well.
Now, the complexity of stakeholders. I think that we can agree that most change (not all, but most change) is coming from stakeholders: users, customers, people who have an interest in the success of the application or solution . They will provide us with most of the reasons for a module to change. So the complexity of stakeholders is a mixture of how many stakeholders and actors there are right now but also how diverse their needs are
So how does this work? How come having more stakeholders and actors is increasing the complexity of a problem? Well, the more stakeholders and actors there are for this problem you want to solve, in other words, the more customers, users, etc, the more diversity you will have in that group. This translates into more variations of WHAT these people want and need. Maybe their businesses are similar, but still different in some ways. So there will also be more variations in HOW certain requirements need to be achieved. More actors and stakeholders also means more needs and requirements. And all of this together, the diversity of your stakeholders, actually means more potential reasons for change. So you need to take into account this kind of complexity. The more stakeholders you have for your problem, the better you need to design your system in order to be able to keep up with these changes.
How does the level of complexity dictate whether to apply SOLID or not? I would answer that with: If the domain is simple and the stakeholder group is simple (meaning not many different people with different needs), you don’t need to go full blown DDD and SOLID. To put it concretely: in my opinion, you don’t need aggregate roots nor dependency inverting interfaces in a TODO list app or feature which is going to be used by 20 people. I would consider that to be ‘overengineering’ and YAGNI.
Again, that doesn’t mean that whatever you do should be implemented without thought or design. I am just saying that if there are less stakeholders and they don’t have that much diversity and the problem is quite straightforward, there are going to be less reasons for change. So in my opinion, you can afford your classes to be a bit more coarse grained, even though, theoretically, there might be some potential reasons for change there. In my opinion, you can afford going with ‘just high cohesion and low coupling’ and still keep your code easy to change and maintain.
Nobody has a crystal ball which will reveal the future if you rub it hard enough. So it feels a bit contradictory to state that on one hand, software design should keep code easy to change, but on the other hand, we shouldn’t go all the way with the principles and patterns and such. So the big question still remains: when should you then apply more software design?
The answer is pretty simple: when it is required. Actually applying this piece of advice is not so easy though. And again, this is not a fundamental law. There is no means to look into the future. So this is a bit like a weather forecast: you have to look at the indications and then you try to use those to make the best judgement call you can. But in the end, you make judgement calls based on assumptions.
I would say that the most important indication in the field of software development, is the origin of the change. So when change is knocking at your door, you should have the reflex to figure out where it is coming from. Is it an existing stakeholder/ actor, or does it come from a new group of stakeholders and actors? If it comes from existing stakeholders who are unanimously behind this change, you can assume that the diversity of your stakeholders hasn’t changed much. This is of course an extreme example. In my experience, only a part of the existing stakeholders comes with a change, which actually makes the entire group more diverse. But anyway, for the sake of simplicity, let’s say they unanimously agree with the change. You can then assume that the diversity hasn’t changed that much, it is just your stakeholders themselves who have evolved. So if you have highly cohesive code, making that change should be pretty easy.
But if the change comes from new stakeholders, or only a part of the existing stakeholders, the diversity of the stakeholder and actor group has changed. This means that their requirements are different and there is a big chance that these will continue to evolve on their own in the future. And so now it is time to make that possible with some better and maybe more high level software design techniques and principles.
This entire concept of complexity might be a bit abstract, especially the way I presented it. So maybe an example could give a better understanding of how complexity evolves and how it can dictate the level of software design. The following example is based on reality, but is in itself pure fiction. Its very purpose is to be an aid in understanding a concept, it is not a description of real life events or something like that. It is tailored to be an example for explaining complexity in software which is loosely based on real life.
Imagine you are starting a small business where you are building the next generation PoS system for restaurants in Europe, more specifically, in Belgium (I am from Belgium by the way, that makes it easier). You know a lot of restaurant owners and you know they are kind of struggling with the paper orders, with tax registration and so on. So you decided to help them out with a next generation PoS system. The restaurant owners you know all have dine-in restaurants. In this type of restaurant, people come in, pick a table or get one assigned and then wait until a waiter comes to get their order. The typical restaurant. You know about 20 restaurant owners, they all become paying customers. They also know a few others who might be interested and so you end up with 80 something customers of your PoS solution for restaurants. That is a good start.
A restaurant is a pretty familiar domain. You don’t have to be a restaurant owner to have a good idea of how things are going. So even though there might be some specific details, the domain is pretty straightforward and not that complex. You have orders, these orders contain a table number, and payments usually happen at the end of the dine-in experience. For the sake of simplicity, let’s only focus on the order process.
So what is the complexity of this order process? I would say it is pretty low. The domain is quite familiar and straightforward. I mean, you don’t have to be a restaurant owner to know how the ordering process in a dine-in restaurant is going. Your stakeholders and actors are all restaurant owners and waiters in dine-in restaurants. These restaurants are all Belgian restaurants. There are only 80 restaurants so far. So the stakeholder/actor group is not that big and not very diverse. If the code that is responsible for dealing with orders is highly cohesive, well isolated from other, unrelated code and well structured, you have done a good enough job.
Your customers are actually really happy and so they praise your PoS system to other restaurant owners. Your customer base is skyrocketing! All of sudden, a group of new customers is somewhat annoyed with the fact that there is no customer name and no delivery address on the order. They are very happy with your PoS system, but just annoyed by this little thing.
Hmmm, now this is very interesting. The change looks easy enough. You could just add the new required fields to your order entity and get it over with. Fine. But this could very well be a key moment where your code is in need of some more software design. Let’s find out why.
Where is this seemingly simple change coming from? It turns out that some new customers of your PoS solution are not only doing dine-in, they are also doing takeaway and delivery. What does this mean for the complexity of the PoS problem and solution? Well, you have a bigger group of stakeholders and actors. That is one aspect already. And the group is more diverse. Because now you also have takeaway and delivery, next to dine-in. This has an impact on the domain. Dine-in is quite familiar, so a takeaway and delivery. But your solution must now cover all 3 of these types. And even though the basic concept of ordering is the same, each type of restaurant has its own specific context, its own nuances. For example: if customers of the restaurant come to pick up the food they have ordered, you probably need a name on the order. If the food is being delivered, you need an address in order to be able to deliver the food.
But that is not all. The ordering processes are also different. When the context is dine-in, payment happens at the end of the experience. The experience of dine-in might include many different courses: hors d’oeuvres, main dish and dessert. But when the context is delivery, most of the time payments happen up front and there is only one round of ordering. There is also no waiter involved to take the order. Usually takeaway and delivery orders come in by phone or through the internet. That might mean a different entry point in the application for those orders. So you can expect that your PoS customers, the different restaurant owners, sooner or later will need these slight variations in the PoS that reflect these nuances in the ordering process. I hope it is clear that the complexity has increased. It didn’t look like it when you read the proposed changes. So just adding the required fields (customer name and delivery address info) to the existing order entity or order model isn’t going to cut it. You will need some more and better software design techniques to keep your code easy to maintain and easy to evolve.
Now if you think that this is as complex as it gets, think again. What if you start conquering the market in other countries? Sure, going to a restaurant is a universal thing. Everybody knows what it means. But maybe in France the order total is calculated differently, because some order items have different tax rates. Or maybe fast food restaurants in Germany work slightly differently? Can you see how the complexity grows? And thus also how the need grows for more and better software design?
I think that there is a correlation between complexity of a problem and the level of software design needed to keep the codebase easy to maintain and easy to change. I would even go as far to say that software design should be justified by a level of complexity. If the problem is really simple but you go full blown SOLID and design patterns, you will make the code unnecessary complex. This can make the code base hard to change and to maintain. On the other hand, if the problem has high complexity, but you took shortcuts (in other words: you didn’t take the effort to apply proper software design), this will also make the code hard to maintain and hard to change.
If you want to get started with high cohesion and low coupling, but don’t really know where to start, I can help you with that. You can register for my free course on the fundamentals of software design.
If you want to go a step further and want to learn how to tackle complexity and write maintainable code, I can also help with that. You can take my course on decomposing problems into code