“Architecture is the decisions that you wish you could get right early in a project.”
Software architecture is the fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles and guidelines governing their design and evolution over time.
An architectural style is the combination of distinctive features in which architecture is performed or expressed.
The Microservices Architecture (MSA) style, described in detail in part 1 of this series, is a distinctive approach to architecture that structures a large, complex application as a suite of single-function, loosely coupled, independent component services.
Today, much of the talk about microservices architecture is based upon some of the earliest experiences and encounters with the style. In the early 2000s, Amazon made the now famous transition from the Obidos monolithic application to a service-oriented architecture characterized by encapsulated data and small, “two-pizza” teams.
“For us service orientation means encapsulating the data with the business logic that operates on the data, with the only access through a published service interface. No direct database access is allowed from outside the service, and there’s no data sharing among the services.”
— Werner Vogels
Despite Amazon never having described their transformation with the term “microservices”, a lot of inspiration has been drawn from their experience. ACM Queue 2006 interview with Werner Vogels, remains the best source of information on how they went about it.
Slicing and Dicing
Microservices are derived from a single large monolithic application by slicing and dicing it into many loosely coupled, independent and autonomous services that work and communicate with each other through standardized APIs to solve a larger, complex business problem; a microservices ecosystem. They also come to be as pluggable add-on components to work along existing systems either as new services or as green field projects.
One of the foremost architectural decisions whilst carving up microservices is that of scoping and granularity; defining what a microservice should do. Put more concisely; as a question:
How big is a microservice and what is the breadth of functionality it should implement?
To put this into perspective, we revisit one of the biggest design drawbacks of monolithic architectures: the amount of code in them! There’s so much code implementing such widely differing and far apart functionality that any modification, however small, requires the coordination and input of a few more people than is necessary to ensure that at the end of it all, everyone’s code continues operating correctly. As a result, developers often spend more time on integration and regression testing than on delivering new business capability.
Determining the right granularity of microservices is a case of art meets science. Taking too coarse-grained an approach can result into “mini monoliths”, while taking too fine-grained an approach can result into “nano-services”; an anti-pattern. On their initial foray into microservices, most people wrestle with the scoping and granularity quagmire. In itself, the term microservices puts a lot of emphasis on the size of these services, a point that most find to be rather unfortunate.
“On the other hand, trying to do a big design upfront dividing the Business Domain for a large number of Microservices is not practical either. It is always challenging to find the golden number of Microservices to start with but its better to start small and divide later.”
In pursuit of the right level of granularity for microservices, it is imperative for organizations to set up general rules of thumb and guidelines that inform and support decision making; principles.
Defining Principles
“Principles are general rules and guidelines, intended to be enduring and seldom amended, that inform and support the way in which an organization sets about fulfilling its mission.”
Architectural principles are one element in a structured set of ideas that collectively define and guide an organization, from values through to actions and results. They reflect a level of consensus across an organization, and embody the spirit and thinking of the architecture while acting as guidelines to assist with decision making. They should be high level and not written in stone.
An example of a good principle would be: Technology agnostic interface APIs . In contrast, a bad principle would be: SOAP web services interfaces.
The earlier example embodies the essence of a good principle. It is high level, agnostic of technical details or technology and can be applied in a number of different scenarios. It helps guide individuals in their thinking and tells you what to look for in an API.
The later is more a standard than it is a principle; it mandates an API mechanism. A standard mandates what ought to be done while a principle focuses on the ‘why’. There is however a natural relationship between principles and standards. Ideally standards should arise from principles.
Microservice Architecture Principles
Loose coupling and high modularity is the pièce de résistance of microservices architecture. By applying as much loose coupling and modularity as possible, microservices overcome most of the shortcomings of monolithic applications opening up a new world of possibilities.
Principles are an integral part in the efficacy of microservices architecture. They reinforce its strengths while addressing its shortcomings.
Below are some of the key architecture and design principles associated with microservices:
1. Single Responsibility (Bounded Context)
Each microservice must cover and be responsible for a specific feature or functionality or an aggregation of cohesive functionality (single bounded context) encapsulating a functional domain. The rule of thumb in applying this principle is:
“Gather together those things that change for the same reason, and separate those things that change for different reasons.” — Robert C. Martin (Uncle Bob)
Robert C. Martin in coining the phrase draws the principle’s origin from two classic papers out of the 70s, a fertile time for principles of software architecture. “Structured Programming and Design were all the rage. During that time the notions of Coupling and Cohesion were introduced by Larry Constantine, and amplified by Tom DeMarco, Meilir Page-Jones and many others” he says.
In the first of these classic papers, that of David L. Parnas On the Criteria To Be Used in Decomposing Systems into Modules published in 1972, Parnas compares two different strategies for decomposing and separating logic in a simple algorithm. He concludes:
“We have tried to demonstrate by these examples that it is almost always incorrect to begin the decomposition of a system into modules on the basis of a flowchart. We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.”
Edsger Dijkstra, true to his usual wits and bluntness, introduced the term: The Separation of Concerns in his 1974 paper On the role of scientific thought; the second of the classic 1970s papers that Uncle Bob references. It’s a widely applicable concept in architecture and design patterns today that has been hailed to be both one of the most important and one of the most difficult to describe; because it is an abstraction of abstractions.
The Single Responsibility Principle (SRP) is the most foundational principle of good design and states that each software subsystem, module, class, or even function should have one and only one reason to change which aligns with Parna’s formulation. However it begs the question:
What defines a reason to change?
Enter DDD.
2. Domain Driven Design
While attempting to answer the question “What defines a reason to change?”, you might have wondered whether a bug-fix qualifies as a reason to change. You might have also wondered whether refactors are reasons to change too. Well, these questions can be answered by pointing out the relationship between the terms “reason to change” and “responsibility”.
Certainly the code is not responsible for bug fixes or refactoring. Those things are the responsibility of the programmer, not of the program. But if that is the case, what is the program responsible for? Or, perhaps a better question is: who is the program responsible to? Better yet: who must the design of the program respond to?
Microservices are organized and built around encapsulated business capabilities. A business capability represents what a business does in a particular domain to fulfill its objectives and responsibilities.
Incorporating Domain Driven Design allows the architecture to isolate system ability into various domains. Designing around real-world domains translates to software that represents the real-world problem you are trying to solve.
3. Encapsulation (Black Box)
A service should only be consumed through a standardized API and should not expose its internal implementation details (composition, business logic, persistence mechanisms e.t.c) to its consumers.
Not exposing implementation details encourages loose coupling and allows greater flexibility when making modifications to the service.
Consider the following:
- Team A builds a service that requires its consumers to know its entities database primary keys.
- Team B builds a service that allows generic entities lookup while returning surrogate keys for exposed entities to its consumers.
In the event that both teams need to switch to a different persistence mechanism, the consumers of Team B’s service will remain unaffected while those of Team A’s service might find that all the previously used keys have become invalid.
Other common examples that break the hidden implementation principle include:
- Service interfaces requiring or exposing internal parameters.
- Services that expose parameters that are directly tied to the database columns or tables.
- Services sharing database access
- Services that pervade physical deployment awareness on their consumers; e.g. expecting a consumer to load balance across multiple instances of the service.
4. Location Transparency (Service Discovery)
Services should never be exposed directly to consumers. Service consumers should never be made aware or dependent on the exact address of the service. They should instead use some form of indirection when locating or invoking services.
The implication of this principle is:
- A service may change its own url without affecting the Consumer.
- Services can be deployed to new infrastructure without affecting their consumers.
- For scaling purposes, services can be deployed multiple times without requiring any reaction from their consumers.
Consider the following:
- Team A exposes two IP addresses for their service: a primary and a failover IP. Consumers are expected to change addresses in the event of a failure scenario. The consumers are coupled to the service addresses.
- Team B exposes two IP addresses for their service: a primary and a failover IP. Consumers are expected to load balance across the addresses. The consumers are coupled to the service addresses. The service providers are unable to scale horizontally without requiring reactions from its consumers by supplying additional IP’s to them.
- Team C exposes their service’s VIP address for their HA configuration. Consumers are expected to always call the VIP, which could point to either the primary or failover address. The consumers have been decoupled from the exact service address.
- Team D exposes an IP address of their load balancer. The load balancer balances across a number of configurable service nodes. Consumers are expected to alway call the VIP, which in turn forwards the request to a service node. The consumers have been decoupled from the exact service address.
- Team E exposes an IP for a service registry. Consumers of their service are expected to first lookup the service address. The consumers have been decoupled from the exact service address.
Teams A and B have consumers coupled to their services’ addresses in the examples above while teams C,D and E have implemented different forms of location transparency.
5. Decentralisation
Microservices embrace loosely coupled resources. To achieve autonomy, an organization should strive to push power out of the center, organizationally and architecturally. Each service handling its own responsibility should be favoured over having a single orchestrator that holds all business logic in one place. Within a microservices environment there should be no resource centralization.
6. Independently Deployable
Microservices should be independently deployable as components of the application’s ecosystem. This principle enables a change in one service to be deployed without requiring any other services to be deployed.
In a monolithic application, any change to the system requires a new version of the entire application built and deployed. This increases the complexity of deploying and with it the risk of a failed deploy. Moreover, scaling the application by adding instances implies that the entire application has to be replicated with every new instance. If a container or instance becomes unhealthy, the entire application is affected.
In contrast, the complexity and risk that a deploy will fail is significantly lowered in an independently deployable microservice environment. New features can be deployed in the service they belong to without affecting any other services. The service is also independently scalable and failure of the service only affects functionality specific to it, leaving the rest of the application unaffected.
7. Culture of Automation
Slicing up a monolithic application into a suite of component services in a highly decomposed ecosystem is bound to result in a noticeable increase in independent deployable units.
Embedding automation into the culture of an organization and investing in tooling to support it yields high returns in a microservices environment by dealing with the maintenance overheads an increase in deployable units brings along. It’s also quite effective in establishing a uniform way of accomplishing cross cutting concerns such as builds, testing and deployments across microservices teams.
8. Standardized API Mechanism with a Published Contract
A services’s interface should be a well-defined, standardized and widely adopted mechanism that is published and available to its consumers. This API is consistent across other microservices, to encourage re-use and avoid breakages (point-to-point integration).
Using a standardized and widely adopted interface mechanism allows the service to be more easily consumed across different systems and languages and by far more consumers in a shorter period of time.
Consider the following:
- Team A exposes their service using the latest web hook framework supported by their language.
- Team B exposes their service using a proprietary mechanism supported by their application server.
- Team C exposes their service using REST web services.
Consumers of the interfaces of Team A may find limitations in consuming the service as the web hook framework lacks support for multiple languages. Similarly, to use the proprietary mechanism, consumers of the interfaces of Team B might need to incur licensing costs.
9. Interface Backwards Compatibility
Interfaces should always remain backwards compatible. Breaking changes to an interface that will force commensurate reactions from a service’s consumers should not be permitted.
Consider the following:
- Team A exposes a service via a simple interface. Whenever modifications are needed, they simply change the interface and notify consumers of the change.
- Team B exposes a service via a simple interface. Whenever modifications are needed, they create a new version of the interface and notify consumers that they need to plan a migration to the new interface; possibly with deprecation timelines.
Consumers of Team A’s services will find that suddenly the interface no longer functions and are forced to make unplanned changes. Consumers of Team B’s services will be notified of a new interface version along with the deprecation timelines for the earlier version. They can plan the migration to the new service whenever they have capacity.
10. Interface Version Dependency
Dependency on services should be based on the interface version and not on the version of the application exposing the interface. Systems should be dependent on the version of the interface they are consuming and completely unaware of the version of the application that exposes it.
Consider the following:
- Team A distributes generated stub code whenever they build and release a new version of the application. Consumers are expected to communicate with the latest stubs.
- Team B distributes generated stub code for each exposed interface. They only supply new stubs for new services or new versions of existing services.
Consumers of Team A’s services have their development and deployment cycles highly coupled to those of Team A’s services. Whenever Team A releases a new build, they are required to update their code, even if there is no change in the interface they are consuming.
Conversely, consumers of Team B’s services are independent of the development and deployment cycles of Team B. When Team B deploys new versions of their application, consumers of existing services remain unaffected. They only have to update their code when they feel most comfortable to move to a new version of the interface they are consuming.
11. Highly observable
Maintaining a healthy production environment where a fleet of microservices forms an application’s ecosystem, requires that the component services are highly observable. The environment must be discoverable. It should provide enough information to ascertain the state of the system.
“Monitor. In fact, monitor everything.”
We get away with simplistic and manual monitoring in monolithic systems but this does not scale well with microservices. Looking for failures in monolithic systems is based on very basic approaches: 100% cpu usage suggests an error.
In a microservice environment, high cpu usage on one container or instance does not necessarily imply an error. Informed system health decisions are based on aggregated data. A service should be easily monitored and proactively alert in the event of a detectable risk. Services should be developed with observability and monitoring in mind.
Consider the following:
- Team A makes use of a service heart beat and exposes a health check interface. Consumers of the service will be immediately made aware whenever the service is unavailable if a heart beat is missed.
- Team B exposes a smoke screen test interface that checks all its dependencies when invoked. Health checks are made easily both after deployments and while the system is running.
- Team C triggers a batch validation process prior to running their COB overnight batch job. They do so to detect and fix any anomalies before the lengthy batch job kicks in.
12. Resiliency and Failure Isolation
Microservices architecture is not a silver bullet. It does not automatically make your systems more stable. Considerable effort is required to isolate system failures within the services they occur in to avoid burdening consumers of a service with handling its failures.
Failures should not propagate to more components within a microservice as well as to other services and the entire system should stay responsive in the face of individual service failures. Components should be isolated from each other with each component containing its own failures thereby ensuring that parts of the system can fail and recover without impairing the system as a whole.
Consider the following:
- Team A deploys their service onto a single application server. Consumers use the service by calling a hardcoded URL that routes directly to this application server.
- Team B deploys their service onto multiple application servers. Consumers use the service by calling a URL that routes to a load balancer that in turn routes to one of the available application servers.
- Team C deploys their service onto a cloud based infrastructure service that allows for multi-region failover. If the service is unavailable in one region, a request is automatically routed to an alternative region.
13. Appropriate Security
A service should implement the appropriate level of security based on the domain it encapsulates. The implication of this principle is that different services may implement different security mechanisms as the level of security applied is dependent on the functionality the service exposes.
Consider the following:
- Team A is working on a service that stores customer information. Given an upcoming data privacy legislation, the service exposed should implement data encryption in order to protect the information.
- Team B is working on a gateway service that facilitates international payments. Given the domain the service encapsulates, the service should make considerations for using trusted third party supplied certificates for authentication and encryption to secure the service and its interfaces.
- Team C is working on a service that returns publicly available currency information, such as a currency’s ISO code and its precision. Given the public nature of the data their service handles, security would not be a serious consideration for team C.
Summary
Principles are an integral part in realizing the efficacy of microservices architecture.
They reflect a level of consensus across an organization, and embody the spirit and thinking of the architecture while acting as guidelines to assist with decision making. They ought to be high level and not written in stone.
While principles address the ‘why’, standards mandate what ought to be done. There is however a natural relationship between principles and standards that takes the form of standards arising from principles. Standards are the focus of part 3 of this post.
Thank you for reading, and I sincerely hope you enjoyed it as much as I did writing it.
You can catch me at:
GitHub: kwahome
Twitter: @kwahome_
References:
- http://pubs.opengroup.org/architecture/togaf8-doc/arch/chap29.html
- https://www.opengroup.org/soa/source-book/msawp/p2.htm
- http://www.in-gmbh.eu/uploads/media/whoNeedsArchitect.pdf
- https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html
- https://martinfowler.com/articles/microservices.html
- http://www.infoq.com/presentations/migration-cloud-native
- https://queue.acm.org/detail.cfm?id=1142065
- http://philcalcado.com/2015/09/08/how_we_ended_up_with_microservices.html
- https://developers.soundcloud.com/blog/building-products-at-soundcloud-part-2-breaking-the-monolith
- https://smartbear.com/solutions/microservices/
- https://www.youtube.com/watch?v=hsoovFbpAoE
- https://vimeo.com/29719577