Microservices Chassis Pattern
Microservices Chassis Pattern
We take a look at the chassis pattern for microservice development, how it helps devs, and how to implement it using Spring Boot.
Join the DZone community and get the full member experience.
Join For FreeNowadays, every developer is talking about implementing microservices and a whole new set of microservice patterns. It really is important to know whether the service that you are writing is a microservice or not. You may end up writing a fat service or a monolith if you do not identify the boundaries of the service and know when to break it into a smaller set of services. There are certain best practices which you should keep in mind when you start writing a new service.
- How granular can you make your services?
- What additional components do you need to create your service?
- How does inter-service communication happen?
- Are there any cross-cutting concerns?
There are additional things like scalability, domain-driven design (DDD), SOA, polyglot programming, high velocity, etc., which contribute to design and thought-process of creating a new service. I would like to confine the scope of this topic to the last point above, that is, "cross-cutting concerns."
What Is a Chassis?
In general, a chassis is the base frame of a car or it can be thought of as a skeleton. Similarly, in a microservices context, it can be the base framework or even another service which can be reused across different services.
This is not something new. Reusability is something we learn in at the very beginning of our developer lives. This pattern cuts down on the redundancy factor and complexity across services by abstracting the common logic to a separate layer. If you have a very generic chassis, it could even be used across platforms or organizations and wouldn't need to be limited to a specific project. It depends on how you write it and what piece of logic you move to this framework.
Chassis are a part your microservices infrastructure layer. You can move all sorts of connectivity, configuration, and monitoring to a base framework.
Why Do You Need a Chassis?
When you start writing a new service by identifying a domain (DDD) or by identifying the functionality, you might end up writing lots of common code. As you progress and create more and more services, it could result in code duplication, or even chaos, to manage such common concerns and redundant functionalities. Moving such logic to a common place and reusing it across different services would improve the overall lifecycle of your service. You might spend some initial effort in creating this component but it will make your life easier later on.
How Do You Implement It in Your Microservices?
Before creating or designing your primary service, think of chassis as how to address the common concerns. When you start designing your architecture and use a whiteboard, pull out all the common concerns which can spread across your different domains. If you follow a DDD approach, each domain or subdomain can be a service out of which you can build a chassis framework which works across all your domains.
A few of the notable concerns:
- Logging
- Exception handling
- Interception logic
- Authentication/security
- Some common backend services you connect across services (databases, external calls, MQ, etc.)
The base framework helps in creating new services easily and ensuring that they're maintainable. You can only concentrate on the service boundary and let the chassis deal with all the other common infrastructure functionalities.
So, how do you start writing a framework? There are no hard and fast rules for creating your own custom framework. You can just build a framework with a snapshot version uploaded to your repository. All other services can just pull it from the repository and use it. Use a SpringBoot autoconfiguration with a starter parent to build a library.
Let's say you came up with the chassis as a library of cross-cutting concerns — you can think of something like a Spring custom starter. I work with a lot of Spring Boot projects and hence prefer writing a custom starter project with Spring autoconfigure. Add all the dependencies to the starter project and let the main project be plain and simple with its own required dependencies, so that it gives you a kickstart to your main project. The example below shows the pom.xml with a custom starter.
<artifactId>test-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test-spring-boot-starter</name>
<description>A custom starter that autoconfigures everything and keep the initial setup ready</description>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.1.5.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>
As seen in the custom starter pom.xml, Spring autoconfigures the dependencies and manages the latest version. You can inject this starter in the same way you add a Spring Boot starter parent to your main project.
The best example is AOP in the Spring world. Spring addressed this situation by providing answers to cross-cutting concerns well ahead and gives it as a usable feature to the developer. We can leverage AOP as one such framework to build a chassis:
@Aspect
@Component
public class ExampleAspect {
private Logger log = LoggerFactory.getLogger(ExampleAspect.class);
@Around("@annotation(LogRequestResponse)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
Object proceed = joinPoint.proceed();
log.info("Method Arguments: {}", joinPoint.getArgs());
return proceed;
}
}
In the above example, I have just added a custom log annotation to the framework which logs the method arguments upon using this annotation above a method. I haven't done anything fancy here, but tried to outsource the common logic, like logging, to a framework.
The other way of looking at a base framework is to expose REST APIs and run them in their own container. Let all the primary services talk to this framework's APIs and identify what is required from them. This approach has its pros and con,s like additional latency and additional build lifecycle, but gives you a clean contract to adhere to.
Some Do's and Don'ts
- What can you put into the chassis? Anything which is common across services, configuration, etc. can go into it. This will make sure your service will have a single responsibility model.
- Make sure you develop the chassis language agnostically so that at a later point in time, if you change your service, for example from a Spring Boot to a Node, it shouldn’t alter the behavior of the system.
- Do not move all the logic to a chassis like critical transactions, production monitoring, etc. Also, it involves thorough regression for any change in the framework so that you may have to test the entire application and in fact all the services which depend on it.
- Do not make your framework bulky and a network latent application by adding irrelevant features to it.
- As microservices deal with decentralizing responsibility of data, it is your choice to move your database connectivity to a base framework or not. It depends on whether you have a database setup for each service individually or if you have a single database with multiple schemas distributed across services.
Sidecar vs. Chassis for Microservices
When I first heard about a chassis, I thought, wait what? Isn't it similar to the sidecar pattern? Isn't it doing the same thing that a sidecar does, i.e. creating a segregation of responsibilities? And, yes, they both are similar and do the same thing. It only depends in which context you are talking about these patterns.
Implementing cross-cutting concerns in a sidecar process or container that runs alongside the service instance is a sidecar pattern. Sidecar provides isolation with encapsulation. It provides supporting features to an application and is hence named sidecar. It will share the lifecycle of the main application. A sidecar can be applied in designing a distributed system architecture or a service mesh with a sidecar proxy or, for that matter, even in a microservice context as well.
A chassis does exactly the same in a microservice context.
Advantages of the Chassis Pattern for Microservices
- One of the biggest advantages is decoupling your business logic or primary application logic with the rest of the system infrastructure.
- Once you build a framework and stabilize it, you wouldn’t test it multiple times unless there was a change in the base framework. Anything related to configuration, logging, or any common functionality need not be tested and it improves the test coverage in your main service.
Although I didn't mention much about the implementation part, I just tried to express my views based on the problems I faced while working with microservices to address such concerns. Please feel free to drop your comments and any other ways of how you created a chassis.
Opinions expressed by DZone contributors are their own.
{{ parent.title || parent.header.title}}
{{ parent.tldr }}
{{ parent.linkDescription }}
{{ parent.urlSource.name }}