Today on microservice architecture implementation principles. I intentionally don’t touch the topic of whether or not one should use microservice architecture for a particular project/company context in this article. That’s a big separate topic of its own and a story for another day.
The following list consists of fundamental items which should be kept in mind over the course of microservice architecture implementation. Architecture guidelines are usually a bit vague regarding these items. At the same time they are extremely important from implementation perspective to achieve robust & maintainable solution based on microservices. Through the rest of this article we’ll dive into each of these principles in detail.
- Microservice autonomy & independence
- Independent teams ready
- Independent persistence
- Asynchronous service-to-service communications & fault tolerance
- Eventual consistency & reference data
- Microservice granularity
- Messages processing idempotency & batching
- API gateway & business requests traceability
- API versioning
- Microservice configuration
- Code sharing between microservices
Microservice autonomy & independence
This implementation principle can be considered as main & primary one. Other principles are mostly applications of this the most fundamental principle to specific areas. Generally speaking each microservice should be autonomous & independent from other microservices as much as possible. Applying that effectively results in very loosely coupled microservices. To better grasp this principle it’s good to think about each particular microservice in a particular way. Treat a microservice as a separate application which talks to 3rd party services where these 3rd party services are other microservices of the system.
Independent teams ready
Each microservice ideally should have its own code repository, continuous integration (CI) & continuous deployment (CD) processes. That allows fully independent delivery workflows for each microservice. It should be pretty easy for each such workflow to be carried out by a separate & independent development team.
Each microservice should have its own isolated & independent from others data layer. Each microservice can leverage different data layer types (SQL/RDBMS, NoSQL, files etc). Data storage type should fit best the tasks microservice is responsible for.
That said sharing a RDBMS database between microservices for instance is absolutely a no-go. That directly violates microservice autonomy & independence. Some “companion” microservice can any time introduce some incompatible changes to the database schema rendering others not functional. Allowing such situations leads directly to failure of a microservice architecture implementation.
Asynchronous service-to-service communications & fault tolerance
Preferred way of service-to-service communications is exchange of asynchronous events via message broker like AMQP. It’s again all about microservices autonomy. Direct usage/synchronous communications of/to other services can easily end in so-called “HTTP request chaining”. That hinders any possibilities for microservice autonomy.
The rule of thumb is that microservice should be able to fulfill any request to it without any direct dependency on any other microservice. An example of “direct” dependency is executing a synchronous request for instance. There could be – and would be – indirect dependencies like events exchange or reference data (more about this below in “Eventual consistency & reference data” section). But these don’t hinder microservice capabilities to serve its clients.
Implementation challenges with asynchronous communications include robust, fault-tolerant message broker with delivery guarantees. A failover cluster usually implements such capability. At the same time temporary message broker outage shouldn’t affect microservices much. Outstanding events should be persisted & the circuit breaker pattern can be leveraged to make sure no event get lost. Then microservice sends all of them when the broker gets back online.
That leads to another very important rule of thumb regarding fault tolerance. Any external dependency (message broker or another microservice) outage shouldn’t hinder microservice capabilities to serve its clients.
Eventual consistency & reference data
Because of distributed nature of microservice-based system there is no way to enforce consistency for system-wide requests in a way that is available for monolithic applications – by leveraging ACID transactions at RDBMS level for instance. That said the consistency model suitable for microservice-based system is eventual consistency. And it’s about microservice autonomy again.
This is related to asynchronous service-to-service communications. A microservice notifies other microservices about changes in itself in the form of asynchronous events/messages exchange. There is no time guarantess for these messages processing but at least we can guarantee that eventually system will be consistent.
Leveraging eventual consistency model also enables enhancements to microservices autonomy by employing so-called “reference data”. Basically a microservice should replicate in its own data layer any portions of data it needs from other microservices. For instance, it’s likely that there will be a user microservice and lots of other microservices will depend on user data. Other microservices can replicate in own datastore relevant user data pieces and even enable ACID transactions involving data if needed. A microservice keep reference data up-to-date by receiving & processing messages from other microservices. Hence data is guaranteed to be eventually consistent with the origin of that reference data.
Leveraging reference data concept is extremely important to ensure proper degree of microservices autonomy throughout the system and success of microservice architecture implementation.
There could be different approaches to microservice granularity but typical ones are the following:
- Business context/bounded context: this hinges on employing domain-driven design (DDD) approach for the system design and high level of maturity of that approach. In such case every bounded context (in DDD terms) can be implemented as a separate microservice.
- Networking: this relies on exceptional understanding of communications which are going to take place in the system. Optimization for less network chattiness takes place here by making frequent communications happen in the address space of a microservice process. That leaves rare comms to happen on the network.
- Transactions/data boundary: this depends on outstanding knowledge of data system is going to deal with. Optimization for related data processing to happen in boundaries of a single microservice takes place here. This usually also enables usage of ACID transactions for data which is sensitive for such capabilities like payments transactional data.
It’s of course possible to try to get most from all approaches by combining them is some way. Still addressing microservice granularity is a huge implementation challenge. Againt it could be good to think about each particular microservice as a separate application. That app talks to 3rd party services which are actually other microservices of the system.
Messages processing idempotency & batching
Asynchronous service-to-service communications & fault tolerance required from microservice system leads to another implementation challenge – the need for messages processing idempotency. At times the same message can be received by a microservice multiple times. It’s microservice responsibility to handle such situation correctly without producing data duplicates or something like that.
One way to address these issues could be registration of all processed messages. In case the same message is received again it’s trivial to figure out it was already processed and just drop it.
At times it’s also possible to receive events in batches. Let’s say there were a bunch of updates to some important entity like user. A microservice responsible for user data management produced a series of messages to notify other microservices about that. For some reason one microservice wasn’t online to process messages as they go and it gets them simultaneously.
It would important to figure out sequencing of these message. They can notify about changes of the same data and incorrect order of processing will result in incorrect data. It could be also beneficial to figure out result set of all these and apply them all at once rather than one by one. That’s kind of optimization which should be applied wisely, though.
API gateway & business requests traceability
For the ease of clients using system as a whole what is so-called an “API gateway” can be leveraged. That means introduction of a thin, usually mostly network-level, facade which will encapsulate underlying microservice APIs details and provide unified interface suitable for a clients class – web app, mobile app etc. That usually makes it easier for clients to communicate with the system as a whole by simplifying routing & discovery.
Another capability which API gateway enables is so-called “business requests traceability”. Whenever a client needs to actually do a bunch of requests to different microservices to fundamentally perform a single “business request” API gateway can do that for the client. API gateway can enable traceability of that business request across the board by introduction of business request ID which it sends along with requests to individual microservices. By doing that every business request can be effectively tracked down across whole microservice architecture implementation.
To achieve robust & sustainable micrservice system & maintain microservice autonomy it’s crucial to support API versioning properly. One can employ semver for instance. It’s again can be understood better if we’ll think about each particular microservice as a separate application. The app provides API to 3rd party applications which are actually other microservices of the system. Whenever API is changed it should be done either in backwards-compatible fashion or if there are inevitable breaking changes there should be two versions of API provided – current & previous.
It’s important to introduce versions maintenance window for changes in APIs which results in multiple versions of the API. Keeping multiple versions of the same API creates a development overhead to maintain all of them simultaneously. That said when some old version leaves the maintenance window space code can be cleaned up & development overhead reduced.
The rule of thumb here which again is aligned with microservice autonomy is that changes in one microservice API should never oblige immediate changes in any other microservice. Microservices catch up with changes in each other as they evolve. Maintenance windows make sure that is done on schedule suitable for the wider development team.
In accordance with 12factor app manifesto regarding configuration it’s highly recommended to keep configuration which is almost always environment specific anyway in the environment itself.
That can be achieved by leveraging environment variables which are widely adopted by most of the tools out there like ASP.NET Core configuration subsytem and versatile capabilities of Docker & its toolset in terms of that.
Code sharing between microservices
In general microservices can be created using totally different tech stacks. But in case tech stack is the same nothing stops microservice teams with the same tech stack from creation of shared libraries. That may help to avoid reinventing the wheel for each of them.
The rule of thumb here, though, is that autonomy & independence is more important even for the same tech stack. Because of that violation of DRY principle is totally allowed to some extent while it makes sense to do so.
That’s all folks!
Hope you enjoyed this longread and it’ll provide you with some great insights regarding microservice architecture implementation if you embarked on a such in your projects.