Although working with microservices delivers many benefits, experience has shown that there are also some problems associated with their use. It is important to be aware of these issues so that their impact can be minimised.
The series 'Microservices from the trenches' puts together all those lessons learnt in real life projects.
Getting right the boundaries of the microservices has proved to be one of the most difficult tasks. It is not the first time I have found myself moving code from one service to another.
Admiteddly, if each microservice had a single responsibility, this problem would not be as prevalent. However defining the scope of a single responsibility is not easy. For instance, let's consider a payment service whose responsibility is to take payments on our e-commerce platform. Let's suppose that, initially, there are only 2 payment methods, card and vouchers, and both methods are implemented in the same service. Is that ok? Is "taking payments" a granular enough responsibility or should we create different services for each payment method? And why to stop there? Why not create a different payment service for each type of card (Visa, Mastercard, American Express)?
Knowing some of the future requirements could help make a more informed decision but normally that is a luxury that very few developers have. Therefore, based on the information currently available, the team decide to have a single service responsible for taking payments.
A few weeks down the line, new requirements come up to accept payments with Paypal and Apple Pay. Now it feels like implementing these new payment methods in the existing service will make the service grow too big and have "too many responsibilities". In the light of these new requirements, a decision is made to split the responsibility to take payments into different services according to the payment method.
Clearly, payments with Paypal and Apple Pay will be implemented in separate services, but what to do with the existing service that implements 2 different payment methods? Given that now the responsibility is defined at payment method level, the original payment service is not consistent with this new approach. As a consequence, the service needs to be split into two new services.
Whereas in a monolithic application, the above change would amount to a code refactoring, in a microservice architecture the code is to be moved into a new service, what would normally imply: new code repository, continuous integration/delivery pipeline, environment configuration, cloud computing/storage resources, etc.
As the scope of the responsibility of each service changes, even names get out of date. In our example, assuming that the original service was called 'Payment', the name became obsolete as soon as the responsibility was newly defined at payment method level. In a platform where there are services named 'Paypal' and 'Apple Pay', what does the name 'Payment' represent?
In an ever-changing environment, it is inevitable that some services' responsibilities need to be redefined over time. There is no rule about how big or small the scope of a responsibility should be and therefore the size of the corresponding microservice.
Based on the experience, it is better to use a more pragmatic approach to decide what goes into a service. For instance, if there are some parts of the service that change very frequently and others that barely do, that is a good indicative that the service could be split in two.
Parts of the code that require specific testing or take longer to test are also better off in a service of their own so that their lifecycle does not interfere with other elements of the application.
Complex pieces of code should have their own service as well to avoid undesired interactions with other elements of the codebase that could cause bugs difficult to identify.
Code to access external resources like databases or queues should be encapsulated on its own service too. You would not want to have to configure your local environment with those external resources just to be able to start the service and use some functionality completely unrelated to the external resources.