In the previous chapter, we’ve seen how our binary thinking minds can easily lead us to incorrectly design applications. We’ve learned how to overcome this with properly naming and testing a mutual exclusivity of Causes and Directions. We’ve also learned of new ways to discover those, by clustering Causes and by getting a sense of reach and control to the Sources.
The example we inspected surrounded the false dichotomy of Hardware v.s. Software, and was corrected by naming it as Hardware v.s. Non-Hardware. For us, hardware was just a metaphor of an application’s dependency on something beyond our reach or control. These are not limited to hardware at all. Serverless Functions run on micro-containers, and they too will eventually Change. A mobile application runs on at least two different OSs, iPhone and Android. If we couple our application to one, it would be harder to develop it to the other.
In this chapter, we’re going to use the principle of mutual exclusivity in order to resolve the problems we uncovered in Fragments of Change: Architecture of Products and Organizations. To minimize the need to continuously refactor our applications in order to withstand the unsteadiness of the Change Stream.
Cohesion of Causes
Another false dichotomy we tend to make is according to personnel. Some presume that the opposite of a Software Engineer is a Product Manager, maybe because Technical Tasks are created by Engineers and User Stories are created by PMs. But there is also The Architect, who does more than a little of both. and there is also the Technical Product Manager, whatever that is. Let’s not forget that Engineers can also be PMs and can also come up with product ideas, and vice-versa. And some companies big and small even have a CEO that still sometimes codes.
Just like we did in the previous chapter, let’s name a mutual exclusivity. For me “Technical Causes” v.s. “Non-Technical Causes” makes a clear distinction, but do feel free to find a tuple that better fits your company’s culture. Let’s see how this reflects on our application, starting with Causes and Sources.
As a continuation of exclusivity of a single 3rd party dependency, this time we’ll handle three of those. Stripe, our database and some unknown Notifications vendor. It is easy to agree that each one of them is an independent cut of the Source. Whatever happens within our Notifications vendor, it is mutually exclusive from Stripe. Same goes for whoever in our company Changes our database. He works for us, not for Stripe or any other vendor.
What might come less intuitively, that any Change in any of them is not a Non-Technical Cause. Notice the negation here. If an Engineer at Stripe deprecates an API, to our application’s perspective it would be a Technical Cause.
The three of them together is a closed set of Causes that are definitely mutually exclusive from everything else. They currently are the cluster of Technical Causes. And they are mutually exclusive from each other within that cluster of Causes. And we’ve seen that every mutually exclusive cluster of Causes entails a mutually exclusive Direction of Change.
Each Direction is beneficial on its own: the three of them separately and together are slowing down the evolution towards a Monolith and preventing both future Inefficiencies and future Instabilities by unintended consequences. And they are agnostic to the unsteadiness of the Change Stream.
We also know that at the end of each Direction exists a Module. Changes going through that Direction hits it and it alone. So we know that our application’s design needs to have three Technical Modules mirroring those Directions. And these three would not grow into each other and not into other Non-Technical Modules, same as just one Direction does.
Which brings us to one more beneficial and extremely important outcome of mutual exclusivity. Their mirrored Modules grow independently and it allows us to design each one uniquely. No need to have one design ruling over all of our Modules. If an Observer design pattern would be more beneficial to one Module, our design would not block us from letting it be. Which opens the path for us to prevent the problems that arise with Product Changes.
Another outcome of Directions is that they define relationships for us. In our case, it forms a relationship of dependency/used by. Non-Technical Modules and dependent on Technical ones. Technical Modules are used by Non-Technical ones.
Uncoincidentally, whatever Products are they have this exact kind of relationship with our Technical Modules. I guess that makes them Non-Technical Modules. We should notice not to fall into another false dichotomy, because Non-Technical !== Products. Just like we did with the cluster of Technical Causes, we need to name a mutual exclusivity within the cluster of Non-Technical Causes. That would be Products and Non-Products.
Before we dive in and further divide the cluster of Non-Technical Causes, we must notice a specific trait of mutual exclusivity. No matter how many we would find within it, the Direction from left-to-right and the relationship of dependent/used-by would remain the same. No matter how many new Products we will start working on in the future, that too would not affect the Direction.
Same goes if something Changes within the Technical Cause cluster. When we’d replace one database with another or when we’d replace Stripe with PayPal, or when we’d add any other 3rd party dependency, the Direction of left-to-right and the relationship would remain the same. As they are not expected to be Changed, there would be no Cause to Change the integration with the Non-Technical Modules. We’ve slowed the evolution process towards a refactor. And that is an eventually beneficial outcome.
We need to recall that mutually exclusive Directions are also a clear and independent cut out of the Change Stream, a tunnel within in. We have seen more than once how misalignments with the Change Stream causes Inefficiencies. Product-related Changes that cut through layers happen because of a mismatch to these two main Directions of Change.
[Note: while researching for a future chapter, I’ve also ran into Event Storming, a collaborative thought process ending with the same outcomes as the principles presented here and do not contradict them.]
Layers are not only misaligned with these Directions. It is a Product Change coming from right-to-left, and an Engineer trying to make it one big Change due to it. This results in Instabilities and unintentional consequences. Instead, he should have made at least two smaller Changes, one in each Direction. A group of smaller Changes from right-to-left and another group of smaller Changes from left-to-right.
Our Directions have the potential to slow down the evolution process towards a Bundle. It will be fulfilled when each Change would be committed and deployed independently to avoid the Inefficiencies caused by bundling the two together. That is multiple Sub-Tasks in a User Story (Deliverable).
It is beneficial to break a big Change into smaller ones, and it’s harder to do with a design not intended for it. It is also a part of the practice of Software Engineering. And yet, some people don’t do that and I can see several reasons for it.
First, because it is a know-how. It takes practice and experience to break down large problems into smaller independent ones. I was taught how to do so when I studied Industrial Engineering in university. I have no idea where they teach that outside of it and I’ve never been taught that in any other work place so far.
A second reason would be an Engineer saying that the first left-to-right Change is not done until the right-to-left one is done too, thus they can not be really separated. There is some truth to that, because without a consumer/client that uses the code, the code is not fully guaranteed to work. To ensure it would fully work, it might require a lot of preparation which might be non-beneficial. On the contrary, it would be eventually non-beneficial to code it tightly coupled to one specific client. It would make it harder for future reuse.
I would like to offer some techniques that helped me overcome both of the above:
- A “generic/general client” and one already exists and it is called Unit/Integration Tests. Which we should do anyway because tests prevent Instabilities.
- Work with two branches, the second coming out of the first. Code the first Change in branch A and continuously rebase branch B to it. That way, we’d maintain two Directions and two deployments.
- Coming up with more ways to do so, share and ask our colleagues.
Lastly and honestly, people avoid doing so because it takes time and effort. Not because they are lazy, but because they are binary thinkers. As planning ahead and deploying slowly can sometimes be non-beneficial, our binary thinking mind came up with the solution of just never designing and planning ahead. This is exactly what leads us to do big Changes instead of smaller ones.
A binary thinking mind would also read this chapter and think that we should never make big Changes. So no, we should make small Changes when it’s eventually beneficial and make big ones when it is eventually beneficial. Whatever avoids or removes inefficiencies or does not speed up the evolution processes.
In this chapter, we made a way to develop Products (and Non-Products) independently from Technical Modules while making sure they would not limit Products. In the next chapter, we’re going to make sure that Changes to Products would not have them interfering with one another with another way to reach mutual exclusivity.