Part 2 – Microservices: It’s not (only) the size that matters, it’s (also) how you use them
Part 3 – Microservices: It’s not (only) the size that matters, it’s (also) how you use them
Part 4 – Microservices: It’s not (only) the size that matters, it’s (also) how you use them
Part 5 – Microservices: It’s not (only) the size that matters, it’s (also) how you use them
Part 6 – Service vs Components vs Microservices
We have finally come to the exciting development I hinted at in SOA – Hierarchy or organic growth?
In the blog post SOA : synchronous communication, data ownership and coupling, we examined the 4 Tenets of Service Orientation and specifically focused on service boundaries and autonomy problems due ( synchronous) 2 way communication between Services. With this knowledge in hand, we are well prepared for the topic of this blog post.
The subject, as the title indicates, is micro services, which in many ways is a response to monolithic architectures. As with SOA, Micro Services also lack a clear definition . The only thing people seem to agree on is that Micro services are small and they are individually deployable . Rule of thumb says that Micro services weigh in around 10-100 lines of code (for languages with minimal ceremony and excl. Frameworks and libraries – although the last point is disputed among purists). Number of lines of code is in my opinion a horrible measuring stick for determining whether a (micro) service has the correct size or for that matter if it is a good service.
Good guidelines for designing micro services in terms of scope (size) and integration form (how to use them) seems to be lacking. Without these guidelines it becomes difficult to separate the wheat from the chaff and one could easily be tempted to claim that the layered SOA (anti) pattern (see diagram below) also meets the micro service size rule of thumb (and then we know that some people will be tempted to cross off micro services on their list and say that they have them too, without looking closely at what micro services is all about and therefore never will come near designing proper micro services).
So are the services within a classic layered SOA real micro services?
Entity / Data Services are thin services that have roughly the same purpose as a Repository / Data Access Object in classic layered OO code. An Entity service is a thin shell on top of a (typically) relational database. Depending on the programming language and framework, a Repository exposed as a (REST / Web) Services could be implemented using 10 to approx. 300 lines of code.
Micro service size rule compliance – CHECK
Task services are thin services coordinating / orchestrating calls to multiple Entity services. Depending on the framework / library and extent of data conversion a Task service can be implemented using 10 to 1000 lines of code .
Micro service size rule compliance – CHECK to just about check
Process Services are thin to semi thin Services that coordinate calls between multiple Task/Entity services. They typically require a bit more work as they often need to handle data conversion, compensations in case of update failure and support long running transactions. Depending on the framework and library (e.g . BPEL and an ESB), a process service can be implemented using 100 to several thousand lines of code.
Micro service size rule compliance – approximately CHECK
As we discussed in SOA: synchronous communication, data ownership and coupling (synchronous) 2 way communication, which is used in layered SOA, is deeply problematic in terms of service responsibilities delineation and service autonomy. The need for compensation with process/task services alone imposes a lot of complexity. Another problem is contractual and temporal stability: If just a single service is down, nothing or very little works. Latency (the time from a service is called until the answer is received) is typically high since we’re communicating with many services over a network protocol.
Based solely on the rule that Micro Services is categorized by containing few lines of code, we could boldly claim that Entity / Task / Process Services also are micro services. This clearly shows that the use of lines of code to determine whether a service is a micro service or for that matter a good service is lousy!
If we decompose our services even further and make them really small (micro) services and then let them communicate 2 ways our latency will suffer tremendously . If the focal point of micro services is exclusively on size and not use usage patterns it is not hard to imagine a star chart of service calls: Our application calls a service, which in turn calls a lot of little (reusable) services, which again (potentially) calls another service that calls other services, etc. Circular calls is a real problem with such a usage pattern.
In our attempt to decompose our services we have made them very small (e.g. responsible for handling a few data attributes). This easily creates the challenge of the individual services to need to talk to each other to accomplish their own task. It is as if they’re jealous of each other’s data and functionality.
One of the goals of service orientation was to ensure reuse. How do we ensure the highest possible reuse? Make all service so small that they can be reused in many different contexts as possible.
The logic is fine, the problem is just that every time we reuse, we also increase our coupling. One of the other goals of service orientation was to ensure loose coupling such that we can easily change our services to keep up with business and technical demands.
In SOA: synchronous communication, data ownership and coupling we discussed how (synchronous) 2 way communication leads to some pretty hard forms of coupling that are not desirable:
- Communication-related coupling (data and logic are not always in the same service)
- Layered coupling (business-related-security, persistence are not the same service)
- Temporal coupling (our service can not operate if it is unable to communicate with the services it depends upon)
Coupling has a tendency of creating cascading side effects: When a service changes its contract it becomes something ALL dependent services must deal with. When a service is unavailable, all services that depend upon the service are also unavailable. When a service fails during a data update, all other services involved in the same coordinated process / update also have to deal with the failed update (process coupling):
In the example above a client, which could be another service, is performing a call against a service. Since the communication is 2 way the client sends a Request message to the Service. The service receives the Request and performs some kind of processing, for example updating a database. After completing the processing, the service sends a Response message back to the client to indicate the result of the processing. The communication occurs over a network, e.g. an HTTP call or message on a Queue. The network is slow and less reliable than the in-memory calls we are used to having with our monolithic applications.
If the client runs into a timeout or another network IO error it is typically for two reasons:
- Either the Request message did not reach the service which in turn did not update the database
- Or the Request arrived at the service that updated the database; but Response message never made it back to the client.
Lack of Reliable Messaging means the client does not know if the service has performed its job or not. This leaves the client with a problem: What should it do?
- Should it try to ask service if the job was performed and then retry if it wasn’t performed?
- Should it blindly retry the call?
- Should it try to compensate?
- Or should it give up?
Last reaction tends to be the predominant solution.
If the client tries the call again, then the service operation must be implemented so that it can handle more than one call / request message for the same job and still only perform the job (e.g. database update) once. This is known as being idempotent.
If the call to the service was part of a series of update calls to multiple services we have a larger consistency problem becausee we do not have, or should not use distributed transactions to handle the coordination of the updates. As we saw in SOA: synchronous communication, data ownership and coupling compensation logic is not necessarily trivial or simple to implement when using 2 way communication:
Imagine an extreme micro-service architecture where each service is responsible for only one attribute (e.g. first name, last name, street name, street number, zip code, city, etc.). With such a design latency time will be a huge problem, stability terrible and our coordination and compensation problem very big. There must be missing something to guide us to a better service design!
In the next blog post we will look at how to integrate services in a distributed context and see how this affects our service granularity and the way we integrate our services. Until then, I look forward to your comments, questions and ideas 🙂