Best Practices for Microservices

Hasitha Subhashana
Geek Culture
Published in
10 min readJun 9, 2021
Source:https://ebookslibrarydownloads.blogspot.com/

If you are new to microservices I suggest you go through my previous article to get to know some must-know concepts about microservices.

So, if you are good with microservices be prepared to learn about common mistakes and best practices for microservices.

  1. Design.
  2. Hardcoded values.
  3. Logging.
  4. Versioning.
  5. Authorization and authentication mechanism.
  6. Dependency.
  7. Make executable contracts.
  8. Fault tolerance.
  9. Documentation.

Design

Source: https://martinfowler.com/

🌟 Design should be domain-driven design (DDD).

🌟 Think of an e-commerce application with built-in microservice architecture. This e-commerce application has several services like customer service, shipping service, order handling service.

🌟 The customer could appear in both customer service and shipping service but two different aspects. In customer service, there is customer registration, profile management (a customer creation part).

🌟 But in the shipping module customer could be represented in a different aspect. But the main scope of the shipping service should be delivering products to customers.

🌟 So basically, when you design a system or think of migrating to microservices, make sure you have a fresh domain driven design.

🌟 Let me explain another mistake some people do when they want to be on microservices so badly just because it's trending and popular.

🌟 Assume an e-commerce application has the below services,

  • customer service.
  • shipping service.
  • order handling service.
  • Seller Service.
  • Inventory service.

🌟 Some people put these services together and create only one or two services and say “now we are on microservices”. But this is a wrong practice.

🌟 Even though you can rewrite the application, that could depend on the company budget. Now you may not need to write everything from scratch but you need to rethink from scratch.

🌟 If your service has something that is not relevant to your service, that something should be taken out. In such cases, we don’t need to redesign and rewrite everything but we can isolate services and make our service independent as possible

Hardcoded values

🌟 Think about your application’s configuration parameters.

🌟 So, remember in the previous article we talked about sharing monolith applications complexity (or metadata) among services when we use microservice architecture (which was a disadvantage of microservice architecture).

🌟 Assume the customer has added all the items to the cart and has completed to payment as well. Now, all there’s to be done is delivery. So, our customer service is calling shipping service.

🌟 Customer service should have the address (hostname/ URL/ IP address) of the shipping service to call that service. In such cases what most developers do is hard code the address of another service within the other service (hardcode address of shipping service in customer service).

🌟 When it's hardcoded customer service will know where to find the shipping service. But the problem arises if the network team decided to change the hostname, network address or something like that. Now that address has been changed customer service will no longer be able to communicate with the shipping service. So now you have to change the address and send another deployment.

🌟 This is not right and it's not the best practice.

🌟 The ideal solution to this type of scenario is to use a network discovery mechanism. (Ex: You can use a Service registry or a proxy). So, you need to use some kind of a network discovery tool to discover the other service and make sure not to hardcode foreign services addresses.

Logging

🌟 Having too many logs and having no logs at all, can make a developer’s life hard.

🌟 To understand this better let’s think of an e-commerce web application (not microservices). Assume there is a service to validate the buyer. Assume this service calls several other methods internally.

🌟 When you are trying to validate the customer, your request goes to the service layer.

Source: https://garywoodfine.com/

Scenario 1

When you try to validate the customer, the request goes to the service layer and some exception happens. So, it's logged in the service layer.

Scenario 2

When you try to validate the customer, the request goes to the service layer and then to the repository layer and you call hibernate. So, there you get an exception and you log it in the repository layer. Now your repository was called from the service layer. Now service layer sees this exception and it's logged in-service layer as well. So the same error was logged twice.

🌟 This is bad because now when you try to debug or access the logs you can see the same error is logged twice.

🌟 So, the best practice would be to fail fast log later. So, if something fails come back immediately, but don’t log. (Logging is the responsibility of the last layer)

🌟 So, to explain this simply, assume your application has 3 layers A, B and C.

🌟 Layer A calls, layer B and B call layer C. In layer 3 some exception occurs. But don’t log there (in Layer C). Don’t log in to layer B. But log in to the last layer. Which is layer A.

🌟 This is directly not applicable for microservices. Because in the perspective of microservices you might not maintain such things. But key point is that, don’t log too much or don’t log everywhere. Log where ever you initiated the process. Make sure to log to stack trace (stack backtrace or stack traceback) and also make sure no information is lost during the process, so you can understand the error well. You must know clearly where the error really occurred.

🌟 So, in this clarification, I didn’t involve anything about microservices. But when we involve microservices there’s one more thing we should discuss.

🌟 Assume service A received a request. Now that request goes through several layers of service A and calls service B. Then the request goes through several layers of service B and calls service C.

🌟 Now, what happens if an error occurs and you need to debug this. Well, that could be a nightmare because you don’t know where that request goes, what it does. All you can see is it failed. But you don’t know where it failed.

🌟 In such cases one of the best practices, you can try is to have a unique id. Let me explain to you how it works

🌟 When the request reaches the initial service, you can generate a unique id. Sometimes your web server can generate this unique id before the request reaches the service layers. This unique id is called correlation id.

🌟 Let’s take our e-commerce application. Assume that a customer requests (from shipping service) drone delivery for a package.

🌟 So, before this request reaches the service layer (microservice layer) you can use the webserver (using a script, a proxy) or service layer to generate an id (ex: 00001) at the very initial point.

🌟 Now when the request reaches service A, 1st layer you can do an INFO level logging with a message (ex: service initiated). When we do this in each and every log entry you can track your service calls at any given time.

🌟 Great..! now with this approach if an exception occurred in the shipping service you can go to logs and see where the request initiated, what are the services request went through, where the exception occurred.

🌟 But if you had separate log files it would be hard. Because you have to go through multiple log files. It would be much easy if you use a framework for logging.

Ex: Splunk, Logstash

Versioning

Source: https://apifriends.com/

🌟 How do you think you should version your service? When you are doing the next deployment of your service, what should be the version number. Well, the best approach for versioning would be to use Semantic Versioning.

🌟 With semantic versioning (also known as SemVer) you can version your services in a meaningful way.

🌟 According to semantic versioning, a version number has 03 parts as MAJOR, MINOR and PATCH.

Semantic Versioning
  1. MAJOR version change: Incremented when you make incompatible API changes. Consumers that use the service will be affected.
  2. MINOR version change: when you add functionality in a backwards-compatible manner.
  3. PATCH version change: when you make backwards-compatible bug fixes.

🌟 With this kind of versioning, anyone can know the compatibility of the updated service version and the existing version of the service, just by looking at the version number.

🌟 Now that you are familiar with good versioning practice let's learn about force upgrading an elastic mechanism.

Force upgrading

🌟 We have a service A. and this service A calls service B. Now you need to update service B. During this update, you do a MAJOR change. Since this is a MAJOR change now the service A will not be compatible with the new version. So, you can increase the MAJOR version number and deploy this update as a separate service.

🌟 Now there are 2 services. Service B 1.0.0 and service B 2.0.0. But the traffic still goes to 1.0.0.

🌟 So, consumers should be made aware of this version change. Then you can provide a timeframe for the consumers to update their version to version 2.0.0. when all the consumers are migrated to version 2.0.0 you can shut down and decommission version 1.0.0

Elastic mechanism

🌟 Assume that you have 5 consumers and you are running 10 instances of version 1.00 of service A. when the consumers are migrating to version 2.0.0, you can increase instances of version 2.0.0 and you can decrease instances of version 1.0.0. With this method, you can manage your services without breaking any consumer.

Authorization and authentication mechanism

Source: https://afteracademy.com/

🌟 If every service try to validate the user that could add more and more latency to your round trip

Ex: Assume when a buyer is trying to ship products to the customer, shipping service invokes other 3 different services. You use the OAuth token to validate the consumer (assume it takes 20ms to validate the token). When the request comes, it goes through 3 different services and if all 3 services are trying to validate this token separately that would add 60ms to the round trip.

🌟 So, what you can do is you can have a separate identity validation service and whatever the request that reaches the service layer you can direct to the identity service and if it’s successful you can direct it to the rest of the path. So, this the how to force forwarding avoids latency.

🌟 Also, with force forwarding, you can change the validation process at any time based on your requirement.

Dependency

🌟 Dependency among services is something that should be avoided.

🌟 Assume you have 3 different services named A, B and C

🌟 You want to deploy A and if you fail to deploy B and C together, then they can’t be considered as independent services. Services must be independent. We should be able to deploy service A, B or C separately without worrying about others.

Make executable contracts

🌟 In usual applications (not microservices) users are the consumers. So, we have UX (User Experience/Consumer Experience mechanism) to make sure user are happy with the application. Same as them, services also have users. Those are consumers. Could be frontend applications.

🌟 So, it’s your responsibility to make them happy. So, in your A, B and C service structure, when you deploy B service if some of the consumers are breaking you haven’t done a good job. So how to avoid this.

🌟 Think about the contract (ex: API specification) with the service and the consumer. How can you make sure that this contract is not broken?

🌟 You can convert this contract to an executable one. But how do you do that?

🌟 Well, you can prepare some test cases, test scripts, or a request to execute (to be executed by a CI/CD tool) whenever you make a build of a particular service (a build from one of our services A, B or C). So, if these tests did not fail, you haven’t broken your consumers.

Fault tolerance

🌟 Now that you have multiple services with microservices architecture, you also have multiple possibilities to have failures. So, you need to properly manage your services.

Service B failing

🌟 Assume that your service A calls service B and Service B calls service C. If service B is timing out (Service B is slowly failing/ service B is taking a long time to respond) then you need to make sure you fail very fast. Because when you are waiting for a service to respond you might be creating a queue behind you.

Documentation

Source: https://www.moesif.com/

🌟 Well, most of us developers may not like to write documentation but unfortunately, there no easy way out from that. Anyway, lazy developers like us who do not like writing documentation have an alternative and that alternative is Swagger.

🌟 With Swagger, you don’t need to write everything from scratch. Since Swagger provides user interfaces also, users can go through the documentation and try out the services as well.

So that’s all for this article. Next, let’s talk about some of the design patterns you can use with microservices. Please comment your thoughts and ideas below. Thank you.

References

--

--