Microservices Architecture — Basics, Key Design and Build Considerations
The root of Microservices architecture goes back to the basic principle of solving large complex problems by breaking it down into manageable smaller units.
It is about making the architecture at component level work the way good software development practices do with modularization and decomposition.
Decomposition: Breaking a software problem into smaller pieces that are easier to understand and solve.
At its core Microservices is about decomposition at a system level:
System Decomposition: Breaking a system into smaller units that make it easier to solve series of large system problems.
Web Services evolved from
- N tier architecture
- SOA and now
- Microservices.
Some of the key considerations to account for when starting with Microservices are:
- Services — The individual domain specific services itself and the definition of the service boundary
- Communications — The service to service calls. Leverage asynchronous Communications as much as possible, using message bus/events based communications pattern to decouple the calls and reduce latency
- Tracing and Logging — Passing co-relation id (or token) to trace the services calls along with standardized structured logging.
- Distribution — Deployment and Scalability of the services based on load
- Database and transactions — ACID transactions vs eventual consistent transaction.
- API Layer — A proxy for the service or set of services to hide the implementation details
- Agility — Continuous Integration and Continuous Delivery and a strong DevSecOps culture from start
Microservices Key points:
- Adheres to the Global Distribution, Agility and Scalability Patterns
- There is no real size requirements for a Microservices component, but generally accepted practices is to adhere to Unix style of programming, do one thing but do it well.
- It is about building the services the right size for the specific use case.
- It embraces the concept of protocol-aware heterogeneous interoperability to handle all communications.
- Every calls within the service boundary is solved via technology agnostic HTTP REST calls.
- Microservices are just as much about the communication as about the services themselves. The communication pattern of Microservices lends itself to a truly distributed model.
- In a purely Microservices architecture each unit of work can be called by any other unit of work within a system.
- It lends itself to continuous delivery and continuous deployment, since a change to a smaller part of the application system requires building one or few smaller domain related services.
- It supports the polyglot development as long as the underlying language ecosystem supports some mechanism for creating, responding to and executing against RESTful endpoints. (though in real projects considered standardization to ensure sharing code and common code patterns)
- One can leverage the OSS (Open Source Software) to build the services and run it cheaply, either on premise, cloud native, or hybrid cloud infrastructure/environments. Though these days Microservices are built primarily on cloud native or hybrid cloud infrastructure/environments.
- Microservices architecture provides development lifecycle agility (develop, test, deploy, scale), ability to globally distribute the system, and independently scale it, and so the critical reasons for its success.
- The ability to distribute Microservices within and across data centers due to the inter services communication patterns over HTTP and the ability to scale individual services as needed (add compute capability as needed) are two of the key things that makes it highly scalable solution. This has benefits and costs (network latency, gridlock) associated with it.
Every Architecture has its tradeoff, its positives and negatives, and so does Microservices Architecture. To adopt it, one should really do the cost benefit analysis, for the specific needs.
Some of the key tradeoff to be considered related to Microservices Architecture are below.
Microservices Trade Offs:
- Complexity — Few deployed artifacts (in monolith) vs many deployed architects (in Microservices architecture pattern) and the associated development lifecycle management. This can be somewhat mitigated by Test Driven development and adoption of automated continuous deployment and delivery tools and services (will discuss further on this in a future article)
- Costs — Multiple code repository, deployment and underlying code runtime operation infrastructure can leads to higher operational costs. The ability to distribute services within or across data centers can lead to network latency.
- Every single Microservices invocation is over remote network call, which lends itself to connection setup, tear down and wire/network latency on each call resulting in risk of response time on load.
- Distribution Tax — A dramatic increase in network communications between individual Microservices calls, which can increase the total latency of calls across the network as a whole. A single call in the stack can lead to thread blocking which can impact other calls. It can somewhat be improved by moving to reactive technologies but the distribution tax will still exist.
- Reduced Reliability — more moving parts (components) in the system can leads to the decrease in the reliability of the overall system, but modern monitoring technologies and automated processes can help with this.
- Overall System can be in partial state, some components may be down or slow.
- On going monitoring and tracing the calls from its initial point is key. Leverage monitoring/logging tools to answer the question, Who (or which call) did What (called another service), When and How, along with what was the overall latency and individual call latency.
Microservices Design/Build Considerations:
- A micro service should handle one set of related functions with no cross domain operation. It should adhere to Domain Driven Design. Always start with a strong data domain and domain boundary when starting with a new Microservices architecture pattern.
- Consider carefully what is synchronous calls and what can be asynchronous operations, design services to be asynchronous.
- Consider the need, enterprise (intranet) or internet consumers
- Like a class in OOP (Object Oriented Programming) which handles one type of things and exposes the operations related to that class, a micro service should ideally handle one thing and expose the operations that can be performed related to it.
- A micro service should ideally provides domain specific CRUD operations, on the domain object, A domain can have one or multiple data objects. Each individual service should be independent of other systems. This gives you the ability to scale individual services based on load.
- Consider the Domain carefully, comprise between “too fine grain vs not fine grain enough”
- Ensure strong bounded context, i.e., service boundary contracts and domain driven design pattern when decomposing large systems into smaller components/services
- Careful identify the traffic pattern based on the use case of the system and then determine the bounded context for each domain to reduce cross domain calls when appropriate.
- While considering your domain ensure that there is no distributed transactions across the data access layer.
- Microservices does not work very well with ACID transactions where data changes occur frequently and must be available immediately as in monolith enterprise applications, but it works well within the Eventual Consistency model. In this model we are not guaranteed isolated immediate consistency of transactions but eventual consistency. if you need a ACID transaction then identity and wrap a service boundary around a set of Microservices and also evaluate if you really need ACID transactions.
- To achieve above, first break the service layer and identify the domain boundaries while still connecting to the same monolith database and then try to see if you can break the monolith database into clear domain boundary to ensure that there are no distributed transactions.
- Size is not as critical as the operation (speed, scalability, latency etc.) of the services. Start small and scale individual services as needed. Design for the ability to scale individual services on load. Ensure global distribution and operations. Service monitoring and performance should be considered carefully. Consider the day 2 growth from day 1
- Inter service communication — Protocol aware heterogeneous interoperability. All communication between individual services in a Microservices architecture is over HTTP RESTful services. This allows for the use of any coding language ecosystem or framework as long as it supports RESTful services
- Carefully consider the network latency and inter service calls while designing, since problems can occur from all of the network calls when each service can call any other service. When needed make services globally available or for most services ensure that they are within the same data centers or networks.
- Orchestration across service calls is key, each service must maintain a certain level of passivity in its API’s and solid versioning strategy or risk system failure
- Ensure that the circular dependency of services, when a service A calls service B which in turn calls service C and which in turns calls service A does not lead to gridlock. Ensure that the overall design considers the latency and the gridlock. Ensure a proper timeout and circuit breaker pattern in case of gridlock.
- Consider a solid API proxy layer for services exposed to third party or consumer facing services.
- An API layer within a purely Microservices architecture should just be a pure proxy to the aggregated list of your service offerings.
- API Layer should shield the structure, organization and the exact service end points that is exposing a particular operations of your set of Microservices.
- At the API Layer, one should avoid doing any transformation or business logic
- A DevSecOps culture breeds more successful Microservices platform and a more successful platform breeds a deeper DevSecOps culture. DevSecOps Culture is important for a Microservices architecture, the two complement each other
- DevSecOps culture brings the conversation between development, security and operations in the same sphere.
- Continuous monitoring and automated response is necessary part of the DevSecOps culture and so ensure that there is proper monitoring of the services and service calls.
- Invest in and automate the CI/CD process from the start.
- Ensure Edge Services/Abstraction Layer for outbound and inbound services, so that any breaking changes in third party services are managed at the abstraction layer or edge service layer. It leads to one more hop but had huge maintainability advantage and ensures agility when the third party services change.
- Plan for uniform logging strategies, structure and format early in the process across your entire platform, this will help with the following: day to day operations, troubleshooting, maintenance, investigation and general operational tasks
- Ensure that the service call chains can be traced using a co-relation id or some token that is passed from first call to each of the subsequent calls to other service or services
- Move from file based logging to log aggregators like: logstash