Enterprise Microservices Design [Part 5: Cross-Cutting Concerns]
Any system has a series of cross-cutting concerns, preferably addressed through consistent and common solutions that result in easier maintenance and lower cost. There are mature tools, frameworks, and services that address such concerns and continue to be useful in various architectural styles.
The distributed and modular nature of microservice architecture creates new priorities and raises specific concerns that are not always adequately satisfied by traditional and available solutions. The nature and specifics of these cross-cutting concerns will depend on the modularity of a microservice architecture adopted by the organization. A highly modular and distributed MSA environment introduces requirements that have not always existed in other architectures and may not have established and mature solutions tailored to a particular organization. A well-defined microservices framework for an organization would thus provide value by filling in the missing pieces, lower development cost, and promote best practices.
5.1 Microservices Parent POM
Dependency Management can be tricky when it comes to microservices. Without proper guidelines in place, each microservice can define different versions of the same dependency which can lead to poor maintainability of these dependencies. Imagine a situation where we have 20–50 microservices in the organization which we have discovered having dependencies with vulnerability and now have to individually visit all these microservices to fix them.
Maven supports inheritance in a way that each pom.xml file has the implicit parent POM, it’s called Super POM and can be located in the Maven binaries. These two files are merged by Maven and form the Effective POM. The significant advantage of using maven inheritance is that we reduce duplication of declaring common dependencies across multiple microservices, greater control, and uniformity of the dependency versions.
Microservices parent POM declares dependencies that are required by all microservices projects. Once included as a parent pom in microservices, individual microservices will automatically include some of the common dependencies and need not be declared explicitly on each microservices
Some common microservices dependencies that can be added to java based microservices parent pom are
- Spring Boot
- Spring Cloud
- Lombok
- Dozer
- Karate
- Jackson
- Apache Commons
- Feign
5.1.1 Versioning:
Microservices Parent POM follows the following convention for the version
<Major Version>.<Minor Version>.<Build Version>
Example: 2.0.1 (Major Version = 2, Minor Version = 0 , Minor Version =1)
major
is incremented when a dependency is added/removed or version updated that needs significant changes at the microservices consuming the changes.
Example: Updating Spring cloud version from Greenwich.RELEASE to Hoxton.RELEASE needs changes at Microservices end
minor
is incremented when a dependency is added/removed or version updated that doesn't impact the microservices consuming the changes.
Example: Updating Dozer from 5.5.1 to 5.5.2 version
build
is incremented when you make changes that doesn't change the dependencies in the pom
Example: Adding release management or sonar plugin to be run during the build process.
5.1.2 Best Practices:
- Always update the version of Microservices parent pom when the pom is modified. Example if the current version is 2.0.1 and we have decided to add a new dependency to the parent pom, update the version to 2.0.2 (based on guidelines in versioning section)
- Make sure the version is defined as properties and not hard-coded at dependency level
- Keep all related dependencies grouped logically and appropriate group comment label added for easy maintenance of dependency
5.2 Microservices Framework
The key requirements of an MSA inner architecture are determined by the framework on which the MSA is built. Throughput, latency, and low resource usage (memory and CPU cycles) are among the key requirements that need to be taken into consideration. A good microservice framework typically will build on lightweight, fast runtime, and modern programming models, such as an annotated meta-configuration that’s independent from the core business logic. Additionally, it should offer the ability to secure microservices using desired industry leading security standards, as well as some metrics to monitor the behavior of microservices.
The Microservices Framework project is designed based on Microservices Chasis pattern to implement cross-cutting concerns such as logging, security, audit trails, exception handling, configuration management etc.
Below is the list of cross-cutting concerns that can be addressed by the framework
- Distributed Logging
- Log Tracing
- Log Masking
- Error Handling
- Microservices Security
- Properties Encryption
- Common Utilities
- Configuration Management
- Dozer Configuration
- Feign Configuration
- Jackson Configuration
- Date Time Format Configuration
Java Microservices framework : [link coming soon]
5.2.1 Distributed Logging
The logging concern is one of the most complicated parts of microservices. It is quite common in large organizations to have multiple microservices log using different log frameworks and with different log patterns, generating excess logs in some services and inadequate at others. This creates a lot of confusion during the log aggregation process resulting in poor maintainability of microservices in production.
The Microservices framework helps all the microservices to inherit the same logging framework and follow a consistent logging behavior across all microservice which is achieved by using Spring AOP, SLF4J and Spring log back.
5.2.1.1 Log Tracing
By using the Logging AOP feature of the framework we will be able to identify which service generated the log event and were in the service the event was generated. The problem now is finding out which actions led up to the event. We need a way of tracing a series of events back to the source, even if it means traversing multiple services.
A common solution to this problem involves attaching a unique identifier to requests as they enter the microservice. This request identifier, which only persists for the lifetime of the request, follows the request across each service and is appended to new log entries. By filtering on the identifier, you can easily follow a trail of events from the entrypoint all the way to the error.
To provide traceability and correlate events, the microservices framework uses Spring Sleuth to provide the following behavior for microservice without the need of additional code at the microservices end.
- Adds trace and span ids to the Slf4J MDC, so you can extract all the logs from a given trace or span in a log aggregator.
- Generate and report Zipkin-compatible traces via HTTP.
5.2.1.2 Log Masking
By using the Logging AOP feature of the framework we will be able to generate good logs at required log levels but unfortunately due to the generic nature of the implementation, the AOP doesn’t differentiate between sensitive protected information and unprotected information that can be logged
To overcome this, the Log Masking feature can been added to the framework that masks any sensitive information in the logs. The framework currently support masking protected information coming from the following sources
- Feign
- JAXB
- Lombok POJO classes (toString() method)
- Application logs
5.2.2 Error handling
If an exception occurs when processing an HTTP request in microservice, you should return a 4xx
or 5xx
response with a precise body. A naive way to do so would be to catch the exception in your controller or service and return an appropriate ResponseEntity
manually.
The Microservices framework provides an efficient way to handle the exceptions at the microservices and effectively communicate with the error to the caller using Spring AOP. This promotes consistent behavior of all microservices in the enterprise and a standard Error Response structure. Using this feature, developers just need to throw an exception in their code and framework will handle this exception and respond back to call in a standard response structure automatically without additional lines of code
5.2.3 Microservices Security
Security is a border term when it comes to Microservices Architecture (MSA). Security needs to be implemented at every point in the architecture (API Gateways, Cloud Infrastructure, Containers, Image Registy, Keystore, Application level etc) .
There are many known security protocols that provide microservices authentication and for reference microservices framework we are supporting Basic Authentication security protocol which is implemented by default in the Microservices Framework. and can be overridden as per authentication needs of individual microservices
5.2.4 Properties Encryption
Whenever we need to store sensitive information in the configuration file(yaml or .properties file) — we’re essentially making that information vulnerable; this includes any kind of sensitive information, such as credentials, but certainly a lot more than that.
By using the microservices framework, developer can provide encryption for the property file attributes and the framework itself will automatically do the job of decrypting it and retrieving the original value during application startup. The microservices framework does this with the help of Jasypt Encryption framework
5.2.6 Configuration Management
Microservices framework automatically configures some of the Spring bean classes as per enterprise standards so that they can be autowired where ever necessary in the microservices code removing the need to configure them again and again in individual microservices
5.2.7 Versioning
Microservices Framework follows the following convention for the version
<Major Version>.<Minor Version>.<Build Version>
Example: 2.0.1 (Major Version = 2, Minor Version = 0 , Minor Version =1)
major
is incremented when framework changes needs significant changes at the microservices consuming the changes.
Example: if we updated the configuration path of “log-config.xml” from logging.config to logging.config.file which needs changes in all microservices using the logging
minor
is incremented when framework changes doesn't impact the consuming microservices
Example: Adding a new Utility Class
build
is incremented when fixing minor bugs or code quality issues that doesn't change overall implementation or impact the consuming microservices
Example: Formatting the code
5.2.8 Best practices
- Even though the framework generates the necessary logs, Please feel free to add necessary inline logs in microservices as per your need.
- Add any utility classes you feel appropriate to the framework so that they can be reused and readily available across multiple microservices rather than duplicating the code
- Always encrypt sensitive information in your application