Tight coupling in code is something that builds up with time because we treat different parts as one big whole. We should avoid doing that.
It’s been a rough couple of months. A lot of work had to be done and in the end I just needed a break. Reading relaxes me so I picked up The Three-Body Problem by Liu Cixin.
I did not know anything about the book or the problem before I started reading it and I can say that I was positively surprised.
It is a science fiction novel, the first book in the Remembrance of Earth’s Past trilogy. The book takes a three-body problem, one of the most difficult problems in classical mechanics, and builds a story around it.
So let me, without spoiling the original story for you, do the same thing in my own way.
To be able to explain the three-body problem and how it could relate to software development, let me start by explaining the one-body problem. It is better known as the central-force problem. Central-force problem tries to determine the motion of a particle when there is a central unmovable source of a force acting on it.
To put it bluntly, it could be used to well enough describe the movement of light planet orbiting a heavy star, where the star can be treated as stationary. This movement can be represented with trigonometric functions.
To complicate things, now let’s imagine we have two massive objects affecting each other’s movements. This is known as the two-body problem. It could be used to describe movements of Earth and Moon orbiting around each other. Or even better, Pluto and Charon, just like it is represented in the animation below.
For many forces, including gravitational ones, the general version of the two-body problem can be reduced to a pair of one-body problems, allowing it to be solved completely. Again, we have a formula.
But if we add another massive object, and make the whole thing the three-body problem, things become unpredictable and chaotic. The three-body problem, for most initial conditions, does not have a general closed-form solution like one-body problem or two-body problem.
How does the three-body problem relate to software development? Well, it doesn’t. But if we look at both processes we could find similarities in behavior. Tight coupled features affect one another, and very loose coupled features can peacefully coexist within the same system without forcing change on one another. Let’s compare software development to the n-body problem.
In the beginning everything is easy and understandable. We have one feature, one central thing, and everything revolves around it. There are a small number of functionalities which do not collide with each other.
For example, we are building an inventory app. All we need is the possibility to insert new items, decrease or increase quantity, and have insight into the state of stocks. So we implement that.
With time we eventually want to add new things. It would be really nice to have a webshop to be able to sell our products online. So we start adding things in our inventory.
First, a web page. We fetch the state of inventory with available quantities. Now the web page needs to describe the state of available inventory. But this doesn’t mean that those items are no longer in inventory, only that they are in different state. So we need to introduce states in inventory. We need to have quantity for “on stock” state, and for “ready for shipping”.
But now fetching of available quantities for the web shop needs to be changed to reflect this change. It doesn’t matter how many items are really in the warehouse if we can’t sell them. We only want to display “on stock” quantity on our web shop. We need to change the web shop again.
The gravity of one part of the system pulls another part of that system into changing. There is a “power struggle” between features until a stable relationship is created between those two. Once we finally polish functionalities everything falls back into predictable motion. Everything works just as we planned and we are happy. We can still relatively easily predict that this will happen, and how change in one part reflects on the other.
But things can be better. We could provide our customers with a delivery service. So we go through the existing system and make changes on every step of the way. Delivery service changes inventory, inventory changes web page, web page changes delivery service, inventory changes delivery service…
Delivery service has helped our business grow. One warehouse is no longer enough to meet our needs. We want to expand our business to more locations and enable the system to support this way of working. But how will this affect the existing system? Inventory needs to be changed to enable multiple locations. Since the web page is decreasing the quantity of items in inventory, it needs to be changed to support multiple locations. But how to do this? This will force changes on inventory and delivery service… Chaos.
So how can we avoid this problem? How can we avoid that one feature that influences the other?
Solar system is full of objects that are big enough to affect movement of other objects. Yet, if we try to predict Earth’s orbit around the Sun, we can safely ignore all of them and focus only on Sun and Earth. This will give us a good initial approximation of a true motion. Same goes for Jupiter, or any other planet.
If we decouple features from one another we can treat them like planets in the Solar system. Gravity of one planet should not affect the orbit of another planet. It actually does, but we can ignore it in most cases. Same thing will happen with features. They will still influence one another, but those changes will be less intense and in some cases even non-existing.
Imagine if we are trying to calculate dates for Easter for the next 10 years. Easter is Christian holiday celebrated on the first Sunday after the first full moon after the spring equinox. Do we really care about Jupiter’s 79 moons when we are trying to calculate these dates? Do we want to care about them? Certainly not. And we do not have to.
We break our solution into small pieces and let them circle around “the Sun”. “The Sun” in our case can be a message broker, service bus, or even well established contracts (interfaces). We decide how decoupled our Solar system needs to be. It doesn’t matter if those small pieces that orbit around the Sun are modules, domains or micro-services, the important thing is that they are independent as much as possible. That makes them easier to comprehend. This way of calculation of Easter dates could be done without even knowing that Jupiter has 79 moons.
Earth’s Moon affects its orbit greatly, that is the fact. If we look at the relations of the Sun and the Earth we do not talk about Earth and Moon’s orbit around the Sun. We talk only about Earth’s orbit. No matter how complex a feature is (Jupiter with 79 moons) in the whole (Solar) system we should treat it like one whole.
This way we do not have (approximately) 1 200 000 objects in the Solar system. Nor 700 planets, minor planets or satellites. We have 8 planets. Because, generally speaking, when we talk about the Solar system we only care about these 8 planets. Now we can determine their movements much easier. Even though the result is not perfect it is good enough for us to be able to work with it without problems.
When we think of an inventory, what do we think about? Probably about some big warehouse and a lot of items stored in it. What is the job of an inventory? To store things and keep it safe until someone comes to take it away. Our software solution should then only cover those functionalities.
Web shop should only be about displaying items and creating purchases. But purchases on web shop need to change the state of inventory. So, how to do this? Now, this could be done in many different ways, but bare with me.
Purchase is complex enough functionality. It gets orders, checks inventory to see if items are available, does the billing part and creates shipping. This might seem like just another feature, but due to its size it can easily be separated into an individual part.
We create strict contract for the inventory where we can get a list of all items, all active items, check if items are available, and decrease their quantity. Web shop only needs to know about active items. If we decide at some point to support soft delete, or multiple states like we did in the example above, the web shop does not need to know about those changes. The only thing that will be changed is the data it receives, and that is going to happen without the web shop knowing about it.
For the purchase feature we need to do the same thing. Contract needs to have an action to make a purchase. Web shop calls that action and its job is done, purchase feature takes over. It checks if an item is available and, if everything is OK, it completes the purchase and decreases quantity in the inventory.
If we look at our system we have a clear separation of features that can stand on its own (some more than the other). We started with an inventory, so it definitely can be a system of its own. Then we added a web shop and purchase feature that could be implemented as independent.
We have a delivery service, but so far the whole process did not need to know of its existence. So we should treat it as a system of its own too. We should not forcefully push it into existing feature just to make it fit.
And there we go. Now we do not have a system that has a list of features with complex dependencies. We have a set of subsystems, each with specific complexity, that together make one comprehensible and maintainable solution.
Building, maintaining and extending software is hard. It might seem easy in the beginning. “Just add this feature”. But the more things we add, the more complicated equation becomes. If we try to push in too many things eventually we will find ourselves with a problem that can’t be solved. And there is a thin line between those two.
We could, on the other hand, try to simplify it and break it into many small pieces. There is a larger number of people who can work with those smaller pieces. Solving one body problem is done by teenagers in school. On the other hand, the three-body problem seems to be an unsolvable problem. Yet, the difference between those two looks so small at first glance.