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.
One of these problems is getting right the boundaries of microservices. This 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.
Establishing the scope of microservices
Admittedly, 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 the sake of the argument, let’s consider a payment service whose responsibility is to take payments on an 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 single responsibility or should we create different services for each payment method? And why to stop there? Why not create a different service for each type of card: Visa, Mastercard, American Express, etc.?
Knowing some of the future requirements could help make a more informed decision but normally that is a luxury that we do not have. Therefore, let’s imagine that, based on the information currently available, we decide to have a single service responsible for taking payments.
Changing requirements
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 two 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 relatively simple code refactoring, in a microservice architecture the code is to be moved into a new service, what would normally require: new code repository, new continuous integration/delivery pipeline, environment configuration, cloud computing/storage resources, etc.
Microservice naming
As the scope of the services change, 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 now there are services named ‘Paypal’ and ‘Apple Pay’, what does the old name ‘Payment’ represent?
Heuristic approach to microservice boundaries
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 a microservice should be. In absence of an optimal solution, a heuristic approach based on experience seems to be our best bet.
Here’s some suggestions:
- if there are some parts of the service that change very frequently and others that barely do, that is a sign that the service could be split in two
- parts of the codebase 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 also have their own service 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 in 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 then use some functionality completely unrelated to the external resources.
A real-life case
If the above made-up example about the Payment service does not feel appealing enough, here’s a real case.
Eurostar have been selling train tickets between London and other European capitals for 25 years. About three years ago, I worked with them to turn their monolith into a microservice architecture, so we developed new services like ‘search’, ‘checkout’, etc. Clearly, in the context of Eurostar, the terms ‘search’ and ‘checkout’ made reference to its only product, the one that had been sold for 25 years: train tickets.
Soon after the new microservices were completed, Eurostar made a strategic decision to become a travel agency and sell hotel accommodation and package holidays in addition to train tickets. New services were created: ‘hotel-search’, ‘hotel-checkout’, etc., rendering the names of the existing services inadequate and questioning their scope by posing questions like, do we need a common checkout service or should we keep them separate?
The morale is that businesses change and, as a consequence, services need to be revisited and refactored.