Microservices Design Pattern
Functional decomposition of an application with the help of DDD (Domain Driven Design) is a prerequisite for building a microservices architecture. Only this approach allows you to effectively design for loose coupling and high cohesion. Even if you go with the much simpler service characteristics, you’ll still be able to decompose already existing applications.
However, unlike with applications, which are tied together by the frontend, microservices can interact with each other and span a network of service calls. To keep the variety of interactions comprehensible and maintainable, a first set of patterns have emerged that will help you to model the service interaction.
Common Principles
Every microservice has some common basic principles that need to be taken into account.
To Avoid Trunk Conflict, Each Microservice Is Its Own Build Conduct a separate build for each microservice. One reason for this is that teams can be fully responsible for putting new versions into production. It also enables the team to use the needed downstream services at the correct revision by querying the repository.
The Business Logic Is Stateless Treat the logic in your services as stateless. Needing to replicate state across various services is a strong indicator of a bad design. Services are fully contained and independent and should be able to work without any pre populated state.
The Data Access Layer Is Cached In order to keep service response times to a minimum, you should consider data caching in every service you build. Create a Separate Data Store for Each Microservice.
Aggregator Pattern The most simplistic pattern used with microservices is the aggregator. It is already well known from the Enterprise Integration pattern catalog and has proven to be useful outside microservices architecture. The primary goal of this pattern is to act as a special filter that receives a stream of responses from service calls and identifies or recognizes the responses that are correlated. Once all the responses have been collected, the aggregator correlates them and publishes a single response to the client for further processing. In its most basic form, aggregator is a simple, single-page application (e.g., JavaScript, AngularJS) that invokes multiple services to achieve the functionality required by a certain use case. Assuming all services are exposing a REST interface, the application simply consumes the data and exposes it to the user. If they represent domain services, they should be called by an application service first and brought into a representable state.
The endpoints don’t necessarily have to be REST based. It is totally valid to use different protocols. Because the aggregator is another business service heavily accessing asynchronous domain services, it uses a message-driven approach with the relevant protocols on top.
Proxy Pattern The proxy pattern allows you to provide additional interfaces to services by creating a wrapper service as the proxy. The wrapper service can add additional functionality to the service of interest without changing its code.
The proxy may be a simple pass-through proxy, in which case it just delegates the request to one of the proxied services. It is usually called a smart proxy when additional logic is happening inside the proxy service. The applicable logic varies in complexity and can range from simple logging to adding a transaction. If used as a router, it can also proxy requests to different services by parameter or client request.
Pipeline Pattern In more complex scenarios, a single request triggers a complete series of steps to be executed. In this case, the number of services that have to be called for a single response is larger than one. Using a pipeline of services allows the execution of different operations on the incoming request. A pipeline can be triggered synchronously or asynchronously, although the processing steps are most likely synchronous and rely on each other. But if the services are using synchronous requests, the client will have to wait for the last step in the pipeline to be finished. Chains shouldn’t exceed a certain amount of time if called synchronously.
As a general rule of thumb, according to usability studies, one-tenth of a second is about the limit for having the user feel that the system is reacting instantaneously. One second is about the limit for the user’s flow of thought to stay uninterrupted, even though the user will notice the delay. Normally, no special feedback is necessary during delays of more than 0.1 but less than 1.0 second, but the user does lose the feeling of operating directly on the data. Ten seconds is about the limit for keeping the user’s attention focused on the dialogue.
Shared Resources One of the critical design principles of microservices is autonomy. Especially in migration, it might be hard to correct design mistakes made a couple of years ago. And instead of reaching for the big bang, there might be a more reasonable way to handle those special cases.
Running into a situation where microservices have to share a common data source isn’t ideal. However, it can be worked around with the “shared resources” pattern. The key here is to keep the business domain closely related and not to treat this exception as a rule; it may be considered an antipattern but business needs might require it. With that said, it is certainly an antipattern for greenfield applications.
Asynchronous Messaging Typical RESTful design patterns are common in the microservices world. Most likely, they are implemented in a synchronous and therefore blocking manner. Even if this can be changed and the implementations support asynchronous calls, it might still be considered a second-class citizen in the enterprise systems you are trying to build. Message-oriented middleware (MOM) is a more reasonable solution to integration and messaging problems in this field, especially when it comes to microservices that are exposed by host systems and connected via MOMs(Message Oriented Services). A combination of REST request/response and pub/sub (Publish/Subscribe) messaging may be used to accomplish the business need.
---Shine K Velayudhan (Senior Software Architect)
After thinking about this further, data caching can potentially come back to bite you in the butt when you need a constant stream of realtime data, like in mobility apps. Examples like ridesharing apps, dispatching software, and any navigation software needs the most accurate data right now. I'm not sure if I would implement caching in every case as policy though. In my opinion, a microservice should not share a common external data source as a policy. That should be an indicator that it doesn't make sense to abstract that particular module, and that the two resources that need the shared data should probably be coupled into a single microservice, or you may need to create a single microservice that fetches all external data via a series of endpoints to make the data globally available to other services. Therefore you don't have data that gets out of sync where cache is up-to-date in one service and out-of-date in another. Those are my 2 cents for whatever it's worth, lol. Great article, I've learn much reading this, thank you!
I find this post useful in that it openly acknowledges the need for caching services to overcome the slowdown imposed when moving from a fast in-jvm monolithic approach. That is, not everyone is prepared to admit this need for greater complexity.
Circuit breaker is another one which is a mandate for MicroServices ,rt?
Good one Shine Velayudhan. For sure you will find suitable position. One recommendation, Tweak shared resources and "Create a Separate Data Store for Each Micro-service" part.
Missing Circuit breaker which is very important pattern that gives the health of the service. With out hitting the Service and returning Error