Balanced reusable components
_We all know these engineering teams' discussions about whether or not to make extra effort to do the "right" thing. The harder it is to define what is "right", the more empty buzz-words are thrown into the air.
A great way to build sustainable systems is by implementing modular and reusable components. We strive to create building blocks that not only solve immediate challenges but also stand the test of time. However, achieving a balance between flexibility and specificity in reusability is an art.
The problem
From my experience, the hardest decisions engineering teams make when designing software components are related to this topic. The first part of getting better at making these decisions is by understanding them.
In my other blog post on over-reusability I enumerate serval factors to consider when developing for reusability. Some factors are "dos and don'ts" -ish, like "Avoid reusing legacy code", and some are more dependent on the context of the problem, like "Uncertainty & Flexibility". Here I tackle the context-dependant factors. Let's start with some definitions...
Convention — Widely agreed standards. Where "widely" depends on your problem's world.
Conventionable — The ability to define something with other conventions in the problem's world.
For example, in the context of simplifying the work of building microservices coordination — "MySQL" is not conventional enough.
Common use-case — An actual use-case that exists in all present (and known future) users of the component you design.
Maximum conventionable common use-cases — All the use-cases that you can define with the problem's world's conventions, and that exist in all the present (and known future) component users.
The balance
Your solution should support the maximum conventionable common use cases while keeping in mind that more conventionable use cases may appear in the future.
Too much philosophy? Let's take it to the real world!
You try to simplify the work of building a microservices coordination framework.
You know that your solution would serve some teams in your company, and you know that those teams use AWS SQS for messaging.
Supporting SQS is a common use case, but it is not a convention in the world of your problem. So you should not assume that only SQS is used.
What can you do? You can make an interface that follows the conventions, and an SQS implementation of it. It should not block you from also using Kafka in the future.
You also know that all of the teams use only asynchronous messaging, however, synchronous messaging (e.g. HTTP) is also well-known. You should not support it right now, but design your component so it will be able to support it in the future. Or, make a clear and documented assumption that your solution works only for asynchronous messaging and synchronous messaging can be implemented by the user.
Conclusion
One nice observation I noticed while thinking of these examples is that SOLID helps make loose coupling between the use cases and the conventions. Same for DDD.
As I said at the beginning of this post, the balance of building reusable components is an art. However, from the above examples, you can see how defining the problem and its world gives you better tools for making design decisions.