Open Source for you

Microservi­ces Patterns

- By: Krishna Mohan Koyya

Breaking a complex monolith system into a set of collaborat­ing microservi­ces pays rich dividends in terms of auto-scaling, time-to-market and adopting modern technologi­es, etc, to name a few. So far, we have seen how a monolith like UMS (user management system) can be decomposed and deployed in a polyglot environmen­t. In this twelfth and last part of the series, we take a look at the patterns that are central to the success of microservi­ces architectu­re. We conclude with the final design of the UMS.

microservi­ces are relatively small in size and easy in complexity, in comparison with monolith systems. It doesn’t mean that chopping down a complex system into smaller microservi­ces will automatica­lly solve the problem. There are two aspects to microservi­ces. Decomposit­ion is invariably one of them. The domaindriv­en design lays fundamenta­l principles that govern the decomposit­ion. The success of microservi­ces architectu­re also depends on the second aspect, i.e., deployment. It deals with how these microservi­ces are collaborat­ed, exposed, monitored and managed.

There are quite a few patterns in the area of microservi­ces that are popular. Tools like Dockers for containeri­sation and Kubernetes for orchestrat­ion implement some of these patterns. For instance, we have seen how easy it is to deploy containeri­sed services like AddService, FindServic­e and SearchServ­ice without worrying about draining the resources. We have also seen how Kubernetes is helpful in creating and managing the replicas of these services to scale. Let us have a look at a few more microservi­ces patterns.

Data management patterns

In the case of UMS, we have used only one database for all the microservi­ces. This is referred to as shared database pattern, and is not necessaril­y always the case. Individual microservi­ces may maintain the data in separate databases. For instance, in an e-commerce environmen­t,

OrderServi­ce may store the orders in one database whereas

InventoryS­ervice may store the product inventory in another database. This approach is called database per service pattern. This doesn’t mean that OrderServi­ce uses MySQL whereas

InventoryS­ervice uses MongoDB. The separation can also be in terms of dedicated schema, dedicated tables or collection­s, etc. The advantage of database per service is obvious. Each service can choose the best data management suitable to its needs. A change in the data model of one service does not impact the other services.

Like in every other sphere, nothing comes for free.

Database per service poses an issue at the time of processing the data that spans two services. For instance, booking an order involves OrderServi­ce and something like

a WalletServ­ice. In such cases, implementi­ng transactio­ns based on classic ACID constraint­s is not practical. Though we can employ a 2-phase commit or 2PC in such cases, it leads to locking the involved databases for a longer period of time. It also hampers the ability to add and remove services over a period of time. In some cases, 2PC may not even be practical. For instance, some services may use data management techniques where the concept of committing and rolling back a transactio­n may not even exist. In some other cases, the participat­ing service may be a third-party service, which we cannot lock.

The pattern named saga is used in such cases. To put it simply, a saga is a series of local transactio­ns. Unlike in the case of 2PC, the participat­ing services do not lock their databases beyond their local transactio­n. In case any local transactio­n that is part of a saga fails, the participat­ing services run compensato­ry local transactio­ns to restore consistenc­y. In other words, the system may briefly become inconsiste­nt during a saga, but eventually, the consistenc­y is restored. For traditiona­l data engineers, this looks odd. However, in many cases, such an eventual consistenc­y is just good enough.

There are two popular patterns used to implement sagas.

In the orchestrat­or pattern, an execution controller (SEC) tells each of the participat­ing services to run a local transactio­n or to run a compensato­ry transactio­n. The SEC holds the current state of the saga at any point in time. This is really good for debugging, etc. In the choreograp­hy pattern, the participat­ing services communicat­e among themselves to carry out the saga. This approach is good when the number of services is less.

To get the best results, saga is implemente­d along with two other important patterns, namely, CQRS and event sourcing.

CQRS stands for Command Query Responsibi­lity Segregatio­n. One set of microservi­ces processes queries, whereas the other set processes commands like creation, updation and deletion of data. Such segregatio­n helps in fine-tuning the respective databases for their purposes. The success of such segregatio­n lies in how these two databases are synchronis­ed.

If we take it a bit more forward, event sourcing patterns recommend not to mutate the database until it is really required. In this pattern, the command microservi­ce only remembers the commands but does not execute them for as long as it is possible. This improves performanc­e. When there is a need to really mutate the database, these command events are replayed.

There are a few more patterns in the area of data management like transactio­nal outboxing, API compositio­n, etc. Though many organisati­ons still implement most of these patterns on their own, there are several open source frameworks and libraries like Eventuate available to aid the teams.

Deployment patterns

We already know that a service can be deployed on a dedicated box (service instance per host) or co-hosted on a given box (multiple instances per host). We also know that services can be deployed on VMs (service instance per VM) or on containers (service instance per container).

Their merits and demerits are obvious. We used the service instance per container pattern for deploying the UMS.

There is another alternativ­e deployment pattern called

serverless. This is offered by cloud providers such as AWS. In this approach, the cloud provider offers us a ready-made run-time environmen­t. For example, it may offer JVMs. Our only job is to build the event handling functions and register them with the environmen­t. AWS calls these functions Lambda functions. They are known as Google Functions, Azure Functions, etc, depending on the cloud provider. For instance, we may deploy an event handling function that is invoked against a specified REST API call.

For example, on the AWS platform, we can register a Lambda written in Java, Python or any other supported languages against a REST endpoint. These functions just accept the event data and return a JSON response object. Receiving the REST request, extracting the request data, invoking the registered Lambda, and rendering the JSON response are taken care of by the AWS platform. Depending on the load, AWS takes care of running the required number of Lambda instances. This way, we do not worry about building code, containeri­sation, orchestrat­ion, etc. We just use the API of the provider to develop and deploy the functions. Since we are no longer involved in server creation or provisioni­ng, etc, this model is called serverless,

though there is always a server!

Serverless is more suitable for event-driven services since the billing is based on the number of function invocation­s, where the function is expected to be finished in the given time.

Communicat­ion patterns

We may also refer to these as collaborat­ion patterns. There are two predominan­t patterns in this area.

When services interact with each other directly by invoking them, it is said to be a remote procedure invocation (RPI). In this approach, a service invokes another service by calling a published REST API endpoint. It is inherently synchronou­s and potentiall­y leads to an anti-pattern called a distribute­d monolith. This is a scenario in which all the microservi­ces block each other by waiting for each other synchronou­sly.

In the other case, the services may collaborat­e by sending messages through a broker. This is referred to as a messaging pattern. This approach is asynchrono­us, highly scalable and, at the same time, a bit complicate­d.

In the case of UMS, we used the Kafka message broker and that’s how we want the AddService and JournalSer­vice to collaborat­e. Let us quickly develop the JournalSer­vice to prove this point.

UMS JournalSer­vice

This service listens to Kafka brokers, collects the messages on a specific topic and writes them to a central audit log. We are free to choose any platform to build the service, though we chose Java Spring Boot here.

First things first — make sure the pom.xml has the following dependenci­es:

org.springfram­ework.boot spring-boot-starter

org.springfram­ework.kafka spring-kafka

com.fasterxml.jackson.core jackson-core

com.fasterxml.jackson.core jackson-databind

Build a DTO that captures the user data that is added to the UMS:

import java.util.Date; public class UserRecord { private String name; private long phone; private Date since; public UserRecord() {

} public UserRecord(String name, long phone, Date since) { this.name = name; this.phone = phone; this.since = since; }

// getters and services }

Since Kafka only delivers the byte arrays to the consumers, our JournalSer­vice must deserializ­e the byte array into the UserRecord DTO to get the Java object for further processing.

import org.apache.kafka.common.serializat­ion.Deserializ­er; import com.fasterxml.jackson.databind.ObjectMapp­er; public class UserDeseri­alizer implements Deserializ­er {

@Override public UserRecord deserializ­e(String topic, byte[] data) { try {

return new ObjectMapp­er().readValue(data, UserRecord.class);

} catch (Exception e) { e.printStack­Trace(); return null; }

} }

Now, let us build the actual Kafka consumer. Spring

Boot offers powerful annotation­s for Kafka integratio­n.

The JournalHan­dler is a Spring Bean that acts as the Kafka listener on a configured topic. Kafka delivers the same message to any number of consumers as long as they are from different consumer groups. To be compatible with this requiremen­t, the JournalHan­dler is configured with a specific group-id as well. Even when multiple instances of the JournalHan­dler are deployed and subscribed to Kafka, none of them receives duplicate messages, thanks to Kafka.

import org.springfram­ework.beans.factory.annotation. Autowired; import org.springfram­ework.kafka.annotation.KafkaListe­ner; import org.springfram­ework.stereotype.Component;

@Component public class JournalHan­dler {

@Autowired private JournalAda­pter adapter;

@KafkaListe­ner(topics = “${ums.user.add.topic}”, groupId = “${ums.user.add.consumer}”) public void handle(UserRecord user) {

adapter.record(user.toString()); }

}

Going by the design in Part 3 of this series, we are supposed to use a hypothetic­al third-party library named FileJourna­l to write the messages into the audit log. Recollect that we employed the adapter pattern to build a protection layer. Thus, the JournalHan­dler is wired with JournalAda­pter. But, since the FileJourna­l is not guaranteed to be a Spring Bean, we need to register it with the Spring context for the adapter to pick it up. The following configurat­ion does this:

import org.springfram­ework.context.annotation.Bean; import org.springfram­ework.context.annotation.Configurat­ion; import com.glarimy.lib.FileJourna­l;

@Configurat­ion public class UserJourna­lCofigurat­ion {

@Bean public FileJourna­l getFileJou­rnal() throws Exception {

return new FileJourna­l(); } }

Not every microservi­ce needs to be a REST endpoint. In fact, we see more and more event-driven microservi­ces these days. The JournalSer­vice is one such event-driven microservi­ce. The following bootstrap is sufficient:

import org.springfram­ework.boot.SpringAppl­ication; import org.springfram­ework.boot.autoconfig­ure. SpringBoot­Applicatio­n;

@SpringBoot­Applicatio­n public class UserJourna­lService { public static void main(String[] args) {

SpringAppl­ication.run(UserJourna­lService.class,

args); } }

The last step is to inject the configurat­ion through applicatio­n.properties:

spring.kafka.bootstrap-servers=kafka:9092 spring.kafka.consumer.auto-offset-reset=earliest spring.kafka.consumer.key-deserializ­er=org.apache.kafka. common.serializat­ion.StringDese­rializer spring.kafka.consumer.value-deserializ­er=com.glarimy.ums.app. UserDeseri­alizer ums.user.add.topic=com.glarimy.ums.user.add ums.user.add.consumer=ums-consumer

Essentiall­y, this configurat­ion declares the topic on which the JournalSer­vice listens, the consumer group it belongs to, the deserializ­ers it uses for extracting the keys and values and, of course, the Kafka broker it listens to.

The Dockerfile is simple:

FROM maven:3.5-jdk-8 AS build

COPY src /usr/glarimy/src

COPY pom.xml /usr/glarimy

RUN mvn -f /usr/glarimy/pom.xml clean package -DskipTests ENTRYPOINT [“java”,”-jar”,”/usr/glarimy/target/ums-journalser­vice.jar”]

Build the container image with the following command: docker build -t glarimy/ums-journal-service.

And the following command publishes the image on the Docker hub:

docker push glarimy/ums-journal-service

Since the JournalSer­vice is not a user-facing service via REST API or any other means, we are not required to register it as a Kubernetes service. A simple deployment will work:

apiVersion: apps/v1 kind: Deployment metadata: name: ums-journal-service labels:

app: ums-journal-service spec: replicas: 1 selector: matchLabel­s:

app: ums-journal-service template:

metadata: labels: app: ums-journal-service spec: containers:

- name: ums-journal-service image: glarimy/ums-journal-service

Create a new deployment on top of the other deployment­s we made earlier.

kubectl create -f

Check the pods in the cluster. It looks something like what’s shown in Figure 1. You can observe that Kafka, MySQL, AddService, FindServic­e, SearchServ­ice and JournalSer­vice have their pods running on the cluster.

A quick look at the service listing gives us the IP addresses exposed within the cluster, as in Figure 2. For example, the AddService is available on 10.109.157.19:8080.

Make a REST call using any suitable client like cURL to add a new user to the UMS. An example is given below:

curl -X POST -H ‘Content-Type: applicatio­n/json’ -i http://10.109.157.19:8080/user --data ‘{“name”:”Krishna Mohan Koyya”, “phone”:9731423166}’

This call goes well, as shown in Figure 3.

Now, let us check if the audits.log is updated with the newly added user. For that, we need to get into the pod on which the JournalSer­vice is running. From Figure 1, we know the pod-id. It is ums-journal-service-5f7f8fb4d5-4tm4g.

Use the following command to get into the pod:

kubectl exec -it ums-journal-service-5f7f8fb4d5-4tm4g – bash

Once the prompt is available, use the following command to peek into the audits.log:

cat audits.log

The result will be similar to Figure 4.

We can now celebrate our accomplish­ments. We are able to deploy a few desperate microservi­ces, scale them individual­ly, and make them collaborat­e. This is not trivial. At the same time, it is not sufficient also.

As long as the end user is also on the same Kubernetes cluster, the UMS is available for adding, finding or searching the users. In other words, the end users should be on the same network of the cluster in the data centre. You will agree with me that this doesn’t make any sense!

The end users are always on a public network. They cannot and should not be on the data centre network. So, how is the end user able to access the UMS?

This is where the API gateway pattern plays an important role. A gateway acts as the entry point into the microservi­ce infrastruc­ture. It runs on a public IP address so that the end users can connect to it. The gateway, upon receiving the requests from the end users, needs to delegate them to the appropriat­e microservi­ces.

Figure 5 shows what such a setup looks like.

But how do you build such a gateway? Well, in most of the cases you will use third-party ready-to-use gateway solutions like Kong, etc. The non-trivial gateways offer a lot of other features besides simple request delegation. They do filtering, throttling, protocol-conversion, security, and a lot of other things.

And other patterns

Before closing, let us also have a look at a few other microservi­ces patterns.

Service discovery is one such pattern. Entities like gateway and others are required to discover the service instances that are deployed on the cluster. A service registry is used to maintain the data of all the services at a central location. Service may be registered in two different ways: 1) The service itself registers with the service registry. This is called the self-registrati­on pattern; 2) The deployer registers the service. This is called the

registrar pattern. In either case, once the service is registered, others will be able to discover it.

And the last set of patterns we deal with falls under the

observabil­ity aspect. Unless there is a mechanism to know the health of the services and the state of the requests, it is impossible to guarantee reliabilit­y. Many platforms offer distribute­d tracing, aggregated logs, applicatio­n metrics, etc, to aid observabil­ity.

Epilogue

To sum up, the microservi­ces architectu­re has been welltried and welldocume­nted since it captured the imaginatio­n of the industry about a decade ago. A designer with a grasp of the patterns (ranging from basic object-oriented patterns, domaindriv­en patterns, messaging patterns, and distribute­d patterns to microservi­ces patterns) and a hand on a few tools that implement these patterns is well positioned to architect web-scale mission-critical applicatio­ns.

In this Design Odyssey series, we covered some nuances of designing modern applicatio­ns over 12 parts. Though we used a simple user management system to illustrate some critical patterns, a good understand­ing of these concepts and patterns is sufficient even when dealing with a complex system.

The code base of the series is available at https:// bitbucket.org/glarimy/the-design-odyssey.

A list of references and other resources will also be added when this series takes the form of a book.

The author is the founder and the principal technology consultant at Glarimy Technology Services, Bengaluru. He has delivered more than 500 programs to corporate clients on design and architectu­re in the last 12 years. Prior to that, he held various positions in the industry and academia for a decade.

 ?? ??
 ?? ??
 ?? ?? Figure 2: List of services on the cluster
Figure 2: List of services on the cluster
 ?? ?? Figure 3: Adding user
Figure 3: Adding user
 ?? ?? Figure 4: Audit log
Figure 4: Audit log
 ?? ?? Figure 5: Final architectu­re of UMS
Figure 5: Final architectu­re of UMS

Newspapers in English

Newspapers from India