Microservices form part of a software architecture that has become popular alongside the rise of distributed or ‘cloud computing’. The idea is a system domain is split out into modular components that are responsible for explicit parts of functionality.
This approach to structuring systems can seem a bit overwhelming when compared to a more traditional, ‘monolithic’ system comprising of a single database, backend, frontend and deployment pipeline.
Microservices make a lot of sense when developing complicated systems with long term maintainability, scalability and resilience in mind. Each service is essentially a small system that is responsible for an area of behaviour for the larger picture. This service will typically persist data, manipulate data within business logic and input/output the data to be consumed either by a user or by a dependent service’s code.
Bearing single responsibility in mind we continue this thinking throughout the service by adhering to an ‘n-Tier’ architecture; using the repository pattern to interface with persistence while keeping the business logic in Service layers. We also utilise design principles such as Dependency Injection in order to minimise coupling as much as possible.
Data will move throughout these layers using multiple models designed to carry and mutate it as required throughout the service. Finally, the input and output for the service can be directly from/to a user using a UI or from/to another service creating HTTP requests to call API endpoints; other communication can be achieved via web sockets or using Queue technology hosted on cloud resources such as Service buses in Azure or Simple Queue Service in AWS.
The main point of all of this though is that the service is responsible for one ‘thing’. It should perform its work as required without having a direct effect on anything else unless being explicitly told to do so. This ‘decoupling’ of responsibility to other services means long term maintainability becomes a prime focus in the short-term. A service can be worked on with absolute minimal disruption to the rest of the system, it can then be deployed to a server on its own – which may cause a period of downtime for a feature dependant on that service but not for the whole system.
Managing with a document database
One thing to consider when working this way is that each service will have its own persistence technology to store its data in a way that makes sense. So, if you want to model something that has a level of complexity but not necessarily complicated relationships you can use a document database such as CosmosDB or MongoDB. If, however, you have a lot of relationship complexity to cater for then a relational database such as SQL or Oracle or even a graph database such as neo4j may make more sense. If a developer is not used to working this way it can perhaps seem quite counterintuitive compared to having all data stored in one place. The gain here is that the databases are very small and easier to maintain and change to accommodate new functionality. The data in the database is concerned only with its relative service, so it is lightweight and as such more performant when it comes to data retrieval. This in turn with using the technology best suited to the use case of the service allows a lot of flexibility regarding how data is treated.
Although the service keeps its behaviour as decoupled from the rest of the system as much as possible, that isn’t always the case with data itself. An Id that exists in one service will more than likely need to be present within another service, a good example of this is userIds. As such you do get data replication and it may be the case that if it gets mutated then another service needs to update its database accordingly. This is where Messaging queues offering FIFO functionality can come in or (in Azure) Topics which follow the ‘PubSub’ pattern. In Azure these mechanisms are hosted in cloud resources called Service Buses. A Service Bus can be deployed along with your service and can communicate messages to other services that subscribe to that Topic. The subscribing services can then interpret the message and react as appropriate, to modify data and behaviour in line with what the publisher service has asked it to do.
Is having to do all this more efficient than if we went back to the monolithic approach of having all our data in one place? Efficiency has now become more a network problem and not necessarily a data retrieval one. It is simply a payoff for having a distributed system. To help combat this a concept known as ‘Eventual Consistency’ is utilised. This means that high availability with a distributed system’s models is guaranteed and as such these same models are returned if no change is performed against them. This is notably a benefit of state management technologies such as Redux and functional programming in general. We utilise these in our frontend code to ensure a system state is kept immutable. This means that it must be updated for behavioural change to occur and therefore the most up-to-date models are always returned. Using this as well as asynchronous function calls allows our frontend to help take some of the latency burden from the backend, providing a more succinct user experience.
Over time if you need to bring in new functionality you can horizontally expand the system with additional services. This is a far more desirable outcome then having to adjust a singular database schema and business logic code as a compromise for something that wasn’t initially designed in the first place. Additionally, the persistence technology used can be replaced completely without having to change the API behaviour or break any other services depending on it.
Deployments are also made more complicated as each service becomes a dependency on another one calling it. We currently have 2 stages of deployment – Test and Production. In
This makes good communication (as well as using automated deployment technologies such as Octopus)…a must.