Software needs to be flexible and scalable. To scale up and create de-coupled services that are easy to maintain, organizations rely on different microservices that communicate to solve their problems. At PubMatic, we have developed many best practices for microservices, and today we will talk about a few of them. The purpose of any microservice is to solve users’ problems, whether they are an internal consumer or an external consumer. Keeping that goal in mind alone will not allow microservice to scale to additional requirements, scale, or debug in case of errors. It’s like building a transport vehicle; A bicycle and car practically solve the same purpose of transporting a user from one place to another, but it’s non-functional requirements like the speed of transport, the comfort of travel, customization, etc. which distinguish these solutions. The better the answer you provide to your customer, the higher value they attribute to what you provide.
Let’s have a look at some non-functional requirements for microservices and some solutions which exist today in specific programming languages.
Observability is an essential non-functional requirement, for which every API should implement. Continuing with the earlier analogy of transport vehicles, we have indicators like vehicle speed, check engine light, etc. in a car, even more in an airplane, and very few on a bicycle. Observability means that your API should be able to report on:
- Is an API available to service the consumer? Implement a health check endpoint for your API.
- How is it performing? Indicators like requests per second, response latency, and error rate are good starting points.
- How much resource consumption is there? Indicators like CPU usage, and memory usage, are good starting points in this category.
There are many frameworks and libraries available, which you can use while writing code for your API. Few examples include:
|Java (Spring Boot)||Spring Boot Actuator|
Standardization is crucial when it comes to multiple teams developing different microservices. Imagine a scenario where each car manufacturer follows its convention for what each button represents. Each time you upgrade your vehicle, you will have to thoroughly understand the complete functionality by going through the user manual. An easier to remember and interpret naming convention allows the consumer of a service to read less documentation and understand more about the functionality that the service will provide. It also helps in the upgrade of services by deploying multiple versions of the same. This helps in maintaining releases in a phased manner, such as Alpha/Beta releases. It also allows for using documentation generation tools.
You need to start by defining a standard, which best suits your business use-case and requirements:
Verb [GET/POST/PUT/PATCH/DELETE] – <Base URL>/<Version>/<Microservice Name>/<Entity>
Example: Get all deals: GET | https://api.pubmatic.com/v1/pmp/deal
Additionally, you can also use specific standards like HATEOAS in your microservice responses.
|Java (Spring Boot)||Spring Boot HATEOAS|
Do not do premature optimization, i.e., never implement a cache blindly as a reason to speed up your API/service without understanding the actual bottleneck for response latencies. If you are using a cache, then have a keen eye on handling scenarios like avoid dirty/stale reads. For horizontal scalability, use a distributed cache. Whenever we implement a cache, we make sure that the benchmarking test-cases are updated to generate/request random data. This ensures we do not just measure cached performance but look at performance for an actual business case. For example, you might face severe degradation in service if you rely on cached results and if the cache is not available, or if there is a high cache-miss/cache-hit ratio.
|Java (Spring Boot)||Spring Boot Caching|
When everything is working as expected, it’s possible to overlook maintainability. It’s when things don’t work as planned that we start to realize the value of this non-functional microservices requirement. An easy to maintain, troubleshoot, and extend API/service is important for the organization to grow fast and be agile to customers’ needs. There are numerous aspects of maintainability. The first is SOA-based design, which allows easier separation of concerns and reusable code and makes the structure more readable and easier to maintain. The second aspect is debugging, i.e., having crisp and essential data while logging the application messages. Too much logging or too little logging is equally dangerous; the first one can degrade performance, while the second one can render your debug cycles very cumbersome.
A few best practices on maintainability:
- Interfaces: Define interfaces first and only then start implementation; this will make sure that you have thought about your service functionality first and will allow more straightforward extensions later.
- Log Level: Logging at the correct level is essential, do not log exceptions if you are throwing the same; it will only log the same message every time it is catch and thrown again. In case of any exception, include as much information as possible in the log message; this will help you reduce time in the debug cycle.
- Utilizing RAM: Logging to tmpfs or RAM disks can significantly increase the performance, but always couple this with log aggregation systems like Elastic stack to collect log at a central location. RAM Disk data is lost on a system reboot.
- Global Tracking ID: You should put a tracking ID in logs using frameworks like Sleuth.
An excellent article on micro-services patterns: https://microservices.io/patterns/microservices.html
|Java (Spring Boot)||Spring Boot Logging, Spring Boot Sleuth|
Security is like an insurance policy; you will not need it unless someone breaches your platform. It is better to have it rather than regretting later when a calamity occurs. Security is one of the most overlooked aspects because it does not provide a visible feature value to end customers. Security has two parts to it; the first one is basic API security using authentication/authorization, and the second one is protecting your API against DoS attacks, and SQL Injection. Make sure that proper authorization is implemented and that there is no elevation of privileges. To protect an API from DoS Attacks, we can implement API Gateways with proper throttling of requests per consumer. Using advanced library functions can help you mitigate risks from SQL Injection. There are a lot of tools available to provide scanning of your code, including:
- Container security: scan you docker image for any vulnerabilities using Anchore.
- Third-party library vulnerability scanning: Use Dependency Track to automate this process.
- Static code analysis & security: Use SonarQube for code security along with static code analysis.
- API authentication/authorization: Use Spring Boot Security
- API gateways: Use an open-source API gateway such as Kong.
Shipping your code is just as important as its development. It is important that you test the binary/image and not build the code again to generate a binary/image. Promote this same binary/image version to production. Building Docker images is one such technique, which allows your shipping process and deployment process to be standardized. This will also enable you to use modern-day orchestration frameworks and virtual clouds like Kubernetes.
Some useful reference links:
- Docker: Best practices for writing Dockerfiles.
- Kubernetes: Virtual cloud solution for your enterprise.
- Java Applications: Out of box Spring Boot Docker Integration
- Artifact Repository: Artifactory, Release tagging
In this article, we did not go into detail about every aspect. We will be covering each topic above in depth in subsequent blog posts.
Open-source materials, libraries, and other reference points were listed for illustrative purposes only and these examples are not meant to imply endorsement by PubMatic. Readers should use their own judgment to identify technologies that best meet their needs.