Monolithic Decomposition and Implementing Microservices Architecture
"Microservices" is one of the most popular buzz-words in the field of software architecture. While our first article talks about the fundamentals and benefits of microservices, in this article we will explain how enterprises can implement microservices in real-world use cases by leveraging key architectural principles.
How to Start with Designing Microservices-Based Cloud Solution Architecture
Microservices-based solution architecture is not always the best fit for all use-cases, and using a one size fits all approach has several drawbacks. Before designing a microservices-based solution architecture, enterprise solution architects must address the following questions.
- Is microservices architecture a good fit for the solution?
- How should one define the microservices architecture?
While building microservices architecture for the first version of an application, we suggest going for a “monolithic” approach. This means you build your application in a simple way to validate your idea first. Then, you apply the principles included in this blog to scale and evolve your initial monolith into a microservices-based solution architecture.
There is no value in creating architecturally pure microservices that do not offer value back to the business. Monolithic Architecture patterns will help you to understand several issues and limitations about large and complex systems (that can possibly occur with Microservices architecture).
Once you have implemented your application using monolithic architecture, you need to consider the following development and operations patterns that you can apply in undertaking a microservices transformation.
1. Decomposition of The Application Into Services
Microservices architecture is a set of loosely coupled services and decomposition of the application into services plays a key role in microservices architecture implementation, deployment, and CI/CD.
Solution architects can define the decomposition methods based on need & solution, there are no “best” methods for decomposition but there are common methods, which can help you to decompose your solution in several services as mentioned below.
To apply decomposition, you need to understand the need & role of each component, weight/links between several components, and more factors for each component of the entire solution.
Decomposition Strategies
- Decompose by module/business capability: This method suggests defining each component for each module or feature i.e. messaging, logging, device communication, user management. This helps you to assign an entire feature/module to separate teams, where respective teams will be responsible for a module/feature.
- Decompose by domain: This method suggests defining the region where your solution is going to be deployed, and further defining the services to that region. Define services corresponding to Domain-Driven Design (DDD) subdomains. DDD refers to the application’s problem space - the business - as the domain. A domain consists of multiple subdomains. Each subdomain corresponds to a different part of the business. E.g. User Management, Device Management, Device Communication.
Things to consider while decomposition:
- Find boundaries for each service and align them with business capabilities
- Stay focused on defining the scope of the microservice, and not just shrinking the service. The (right) size of the service should be the required size to facilitate a given business capability
- The Service should have very few operations/functionalities and a simple message format.
- Make sure the microservices design ensures the agile/independent development and deployment of the service.
- Each service must be testable and deployable individually.
- Services must be cohesive. A service should implement a small set of strongly related functions.
- Each service should be small enough so that it can be developed by a small team of 6-members
- The application must be easy to understand and modify
- The Service must be scalable in load balancer infrastructure like ELB
2. Microservices Discovery and Registration
The microservice architecture uses the service registry to maintain the location of service to send requests, and this registry can be managed on the server-side or client-side. The Service can register itself or via a third party (deployment scripts) can register the service. Each service should register itself on the registry on service bootup with a health check interface.
Health check interfaces help the registry check for service availability. While defining the service registry you must implement a mechanism that enables the clients of the service to make requests to a dynamically changing set of ephemeral service instances.
3. Microservices Communication
For this, the architecture must allow each service to communicate with each other. Here are multiple ways to define inter-service communication:
- Remote Process Invocation: Remote Procedure Invocation applies the principle of encapsulation to integrating services. If an application needs to modify the data of another, then it does so by making a call to another. Each service can maintain the integrity of the data it owns. Furthermore, each service can alter its internal data without having every other application be affected. You can use grpc, Apache Thrift and REST for such communication
- Messaging: Use asynchronous messaging within inter-service communication using tools such as Kafka, RabbitMq, etc. This will work when each request is independent and does not require any callback.
- Domain-Specific call: Use domain-specific protocol for inter-service calls e.g. SMTP or IMAP for email, RTSP, RTMP, HLS or HTTP for media streaming
4. Microservices Observation
Observation is another key point for microservices frameworks. This allows you to debug & monitor each service. There are several aspects that need to be addressed while designing microservices.
- Log Aggregation: Use centralized logs from each service instance. The user/Reviewer can search & analyze the logs. They can configure alerts on several critical errors and check the number of errors by categories. This can be achieved by integrating logstash as the central log server.
- Health Checks: Health check interfaces help the user/reviewer to check whether a service is available or not. This can be achieved by creating a REST interface with a static response and integrating a load balancer which periodically checks the health of API and updates the status of service.
- Application Metrics: It provides service status metrics including information for example - How many calls per API? Where are the maximum requests originating from? This service runs in the background and interacts with each operation of the server, so the service you choose should take minimal runtime overhead. e.g. Appmetrics for a node, Coda Hale for JAVA
- Log Deployments & Changes: It is useful to see when deployments and other changes occur since issues usually occur immediately after a change. E.g. Enable notifications for deployment status, enable notifications on application crashes
5. Microservices Database Management
Most services need to have persistent data in a database. For example, the Device Service stores information about devices, and the User Service stores information about users.
Database Management Strategies:
There are multiple ways to manage databases in microservice frameworks.
- Database per service: Keep each microservice’s persistent data private to that service, and accessible only via its API.
- Shared database for solution: Use a (single) database that is shared by multiple services. Each service freely accesses data owned by other services using local ACID transactions.
- Hybrid database: Create common shared database and service-specific database separately for e.g. In a typical IoT Cloud Platform-as-a-Service, architects may choose to store request timeout, wait period for each service in a common database and store user, device and other module-specific information in a specific database which will be accessible only through specific service exposed for that module.
Things to consider while managing databases:
- Some business transactions must enforce invariants that span multiple services.
- Some business transactions need to query data that is owned by multiple services. For example, to retrieve user devices, it will request details from user service and device service
- Some queries must join data that is owned by multiple services
- Databases must sometimes be replicated and sharded in order to scale.
- Different services have different data storage requirements. e.g. Log service will use LogStash, user service and device management will use MongoDB
6. Microservices External Interface (API Gateway)
An external interface is a gate from where users/applications interact with microservices. Implement API Gateway to enable a single entry point for all service requests from clients. API gateway will authenticate requests and proxy/route to actual services.
API gateway implements security (include Access Token in header or query parameter) for secured endpoints. Enterprise architects should design API gateways to take care of security, applications data protection and a number of request limit (per user, per IP or per application) to prevent DDoS attacks.
7. Microservices Testing
When trying to test an application that communicates with other services, one could do one of two things:
- Deploy all microservices and perform end to end tests: Simulate production in your test environment and run end-to-end tests before deployment. This method will test real use cases and ensure service quality. The disadvantage of such tests is that they are time consuming and debugging is extremely difficult
- Mock other microservices in unit/integration tests: Mock the external services and run unit and integration tests. This approach is very fast but it cannot guarantee that production is safe.
Common advice for testing microservices is to use combined integration tests & unit tests. Run some of the tests like unit tests, and some of them as integration tests that can ensure the required quality within the solution.
While planning the test you must include service component tests & service integration contract tests as part of the testing process. You can use several Testing Tools/frameworks in development, such as Junit, Spring Cloud Contract for Java & Mocha, Chai, Sinon, Proxyquire, etc., for Nodejs. You can generate HTML reports by check style to validate the test report & code coverage.
Ideally, 80% of code coverage is recommended for any source code.
8. Microservices; Continuous Integration & Deployment
Each service is deployed as a set of service instances for throughput & availability.
CI/CD Strategies:
- Multiple services per instance: Run multiple services in the same host (physical or virtual). E.g. deploy all NodeJS services on an EC2 instance as separate services. This will work when you are in development or you have a small number of users accessing your application. As users grow, this method leads to problems like resource conflicts, Memory & CPU utilization issues, and insufficient monitoring of service behavior.
- Service instance per host: Deploy a single service on each host. This overcomes issues of multiple services per instance with an effective way to balance the load, such as when a load is high for a particular service. In such cases, you can scale your deployment to multiple instances for a single service. This pattern also has one drawback; consider you have one service which does not have frequent usage i.e dispute of orders, still, this will be deployed on one instance and you can’t utilize CPU & memory resources from here for another service.
- Serverless deployment: Use deployment services that remove server and infrastructure management. It allows you to zip your package, deploy it on application services, and charge you on a request basis. In this method, you need not worry about resource management and load management. Design your service to run without the server using public resources like S3 or Azure storage for file storage, Dynamodb, or Azure database as a database, SNS, or Event Grid for communicating between two services, SES as email service. Popular framework components include AWS lambda, Google Cloud function, Azure functions
Things to consider during CI/CD:
While defining the deployment microservice architecture it is ideal to check the following things.
- Languages, frameworks & framework versions used in the development
- Each service is working and is individually deployable & scalable (e.g. Deploy 4 services for device management)
- Each service is isolated from another
- Add constraints for CPU & memory for each service
- Monitoring for each service
- Check the cost of deployment
9. Microservices Deployment platform
You can also use a deployment platform to automate the deployment of your microservices for both serverless and server-based models.
If you have a huge system with multiple integrated services that self manages server instances and services, is generally a cost-effective solution. You can use AWS cloud formation & task definitions with Docker swarm mode & Kubernetes to automating deployment, scaling & manage all applications centrally. AWS cloud formation allows you to use a single file to model & provision infrastructure, and task definition allows you to define several Docker images for your environment. Once the environment is up, the task definition takes care of all services i.e if your service crashes it would launch a new Docker instance automatically.
If your cloud solution is small but requires a service, then you can deploy your services to PaaS platforms like AWS Elastic Beanstalk which allows you to deploy and scale web applications and services developed with Java, .NET, PHP, Node.js, Python, Ruby, Go, and Docker on familiar servers such as Apache, Nginx, Passenger, and IIS. It charges based on the resources you use. To deploy serverless code, one can use a tool like claudia.js to automate AWS lambda and API-gateway deployments