disclaimer: it’s because you put business logic in service classes
Recently I came across a post talking about the author’s experience with microservices. The experience was quite negative, but the reason was so typical and fundamental that I decided to write a follow-up post. But before talking about microservices themselves, let’s start with lower-level issue.
How procedural mindset looks like
What differs procedural mindset from an OOP one is when using the former you treat your program as a sequence of steps performing some action upon data. Hence, data and behavior are intrinsically disconnected.
Consider the following example. Your system receives a
register card transaction request. First, you should validate it, then process some business rules (in case you differentiate between validation and business rules), then send an http-request to some external payment service, then parse its response, and then, based on its result, carry out some business-logic — for example, send an email to a client and remember a card. So here is a mental image of flow that you conjure up using a procedural approach:
What are the typical classes to implement this scenario? Well, it’s simple:
How the overall procedural-style system often ends up to look like
The next thing that happens is that you have to implement some business-logic for another bank (considering the first bank is called Bank1, the second one is called Bank2). So if you’re not being lazy, you fix your classes so that each one handles only one bank, and you extract a common parent class. So your class set becomes the following:
Bank1PaymentRequestValidationService (which now extends
Bank1PaymentRequestSender (which now extends
Bank1PaymentResponseParser (which now extends
CommonBankPaymentResponseParser), and the same goes for Bank2. What happens to
EmailSendingService? Do you need a separate class for each bank? Well, maybe yes, maybe no. Probably no: there are just two banks, and the logic for both of them is almost identical. So right now it’s very likely that your code structure looks like the following:
It seems to be a good idea to extract all those
Common-classes into a single directory called
base, or, if you’ve gone that far,
shared. So with the latter name it’s become absolutely legitimate to extract each bank related code in its own microservice, with attached
Couple of dozens banks later, you find yourself in exactly the same tricky situation that Segment developers were in: your shared library has significantly grown in size and complexity, containing a lot of client-specific details, and you’re afraid of modifying it since there are far too many clients depending on it.
So, in short, that’s how distributed monolith is created.
What’s wrong with distributed monolith
For me there are three main downsides of this approach. The first one is coupling to specific technologies, so that you can’t solve a problem at hand with a technology that suits the best. The second one is necessity of simultaneous deployments of all services in case the shared library was modified, and hence the higher probability of the overall system outage. The third one is the one I’ve already covered earlier: client-dependent code inevitably creeps in the shared library. So the whole concept of shared library is inherently brittle.
It’s not the only reason of why microservices fail, there are actually much more of them, but it’s the one that stands out quite prominently.
Why service classes lead to poor design
Since it’s the service class concept that often leads to a distributed monolith, you must be wondering why is that so. From the first sight, they are great. They are reusable and sometimes composable. The only problem with them is that they don’t belong to OOP area. Why? Well, the core principle of OOP is that behavior and data belong together. And this principle is violated in service classes most of the time, if not always.
OK, so what? After all, we are not OOP zealots, blindly following its principles. How comes that service classes result in poor design?
That’s a bit philosophical question. Software engineering nowadays is more of a social and behavioral science than, well, an engineering one. There are no set in stone principles that will surely lead you to great results. There are techniques that can facilitate reaching good results, and there are approaches that can make it more difficult. Service classes are just like that. You can keep your code clean, but it’s just more difficult to achieve with service classes.
Giving it a thought for quite a while, I came up with a primary subjective reason of why it is so. Service classes have no feel of identity, no feel of belonging to a specific context, such as, say, a user story. So it’s psychologically easier to add another client-dependent if-clause. Thus most of them end up the same: deceptively reusable service class gets bloated with client-dependent logic. Creating a shared library and a bunch of microservices just makes the situation even worse.
Shared libraries that contain client-specific code are by definition poor abstractions. Abstraction should be, uhm, abstract. If you often have to modify code that you pose as being abstract, then you created wrong abstractions. And the reason (which feels more like a subjective perception) of coming up with poor abstractions based on service classes is that it’s hard to come up with good abstractions which are just actions. Personally, my mind tends to first identify a noun, and second — a verb, that is, its behavior.
How to avoid failed microservices
First, concentrate on user stories forming cohesive business-capability. Then, within each one, focus on objects that possess some identity and behavior. That’s how you’d get the higher-level view of your business-processes expressed with DDD aggregates (which are often sagas). Put the rest of the logic in value-objects. Thus you can end up with almost no service classes.
My mental image of a good service is something like that: