11 February 2020
Monolithic vs microservices: How we’ve successfully migrated our app (1/2)
For a few years now, microservice architecture has been one of the hottest topics in the IT world. When you look for some information about it, you can find plenty of articles regarding the architecture and comparisons like “Monolithic vs microservices”. Those texts cover the most popular patterns or projects which use microservices as their leading architecture. Some companies praise this trend and try to encourage everyone to migrate to microservice architecture. Others, on the contrary, point down some threats connected with this architecture and discourage developers from using microservices. However, there are not many texts about the migration process itself. That’s why, in the article below, I decided to let you know where to start with and how to prepare for this transformation.
The idea of this article is to show the process of migration from a monolithic app to microservice architecture, basing on a simple scenario of the mockup application. Below, I will present some of the available options and solutions you can use, depending on your projects’ needs.
Let’s imagine that you are assigned a new project. You have to talk to your client and gather all the necessary information. You make an appointment, speak to the client and write down the notes to outline a domain of the project.
Let’s assume that your results look like the ones below.
- The project is the existing platform for providing live text relations from football games.
- Administrators have the database of teams and fixtures.
- Planning the relation, admins take two teams and set the time of their game.
- Only logged-in users have access to the commentary, and the observer (a person working for the company) can add textual updates about the events on the pitch.
Also, you were told that the application’s development continued for a few years, but it’s challenging to implement any improvements at the moment. Developers engaged with that project complained about the growing technological debt and the shape of the code itself. According to them, it lacks quality, as it has undergone several changes throughout the ever-changing needs of the project.
After getting access to the repository with the code, you were able to analyse it thoroughly.
Client’s fictional application was developed with the use of a framework. Business logic is wrapped by the services which are injected using dependency injection. Additionally, controllers are also treated as services to simplify the logic and use the same injection method. Finally, everything is tied together with autowiring. The whole communication is performed through the REST API with the JWT authorization.
Client also reported two main issues – the first one is the problematic and hindered development of the app; the second one is the app’s poor performance. The latter problem occurs when the popular football teams play against each other – the application occasionally encounters some difficulties handling heavy traffic.
Knowing the details, you can start the implementation of the first tasks. Before you begin pivoting project, it is worth addressing the problem of the legacy code maintenance. At the end of this process, readability of code should increase significantly, easing later development.
The first thing to do is cleaning and refactoring but without significant changes to the architecture. There are some elementary rules and good practices you should follow when refactoring the code.
- Always perform small changes and test the app’s functionality afterwards.
- When performing changes – don’t add new functionalities.
- Revised code should be more readable after changes.
In terms of testing – it’s one of the most crucial parts when it comes to the refactoring. You can’t implement proper changes without it. If there were no automatic tests introduced before – unfortunately, you have to write them from scratch. Even if there were some test scenarios written already – you should check if all the significant parts of the app were covered. If you need to add any new tests – remember only to perform high-level ones – some parts of the code and functionalities will probably change, so Unit tests are not that important.
See also: Introduction to microservices
Cleaning up the code before migration
When you start cleaning the code – you should focus on the parts which are long, duplicated or generally look messy. It’s essential to take a closer look at long classes in which the number of code lines easily comes in thousands. Make sure you analyse all the comments which try to describe over-complicated logic. It is of common knowledge that appropriately named functions and variabilities should be self-explanatory and can be treated as a form of the documentation. Additionally, analyse: repeatedly appearing fragments of code in different places of the app, potentially dead code which turned to be unnecessary at some point of development or was written “just in case” (YAGNI break), long lists of parameters or some extra class constants and variables.
Proceeding further with the code refactoring, you need to take a closer look at how the classes behave. You should check if:
- Methods use the data from other objects more often than their data?
- There are any call chains which make code dependent on the structure of a particular class?
- Classes, which are not the implementation of patterns such as strategy or visitor, have two-way dependencies?
- Methods in classes are used only to induce other methods which are not an implementation of a proxy pattern?
- The functionality of several classes depends on each other, and they weren’t meant to use delegations or composition?
Those are just a few examples of, so-called, Code Smells which you can find in code. If you are looking for some more information about improving the quality of the code, I can recommend an excellent book called Refactoring: Improving the Design of Existing Code by Martin Fowler.
So, how you can use the theory in practice? Let’s focus on one of the controller’s methods which allows you to edit the football match information (FootballMatchController::editAction). In the body of the method, you can notice that a lot is happening in there. The application checks the access to the endpoint, prepares a query object, performs validation, updates a match and finally returns its current state.
Looking at the method, you can quickly spot a few schemes.
- The creation of the request model happens every time the request is sent to API; it works in the same way for a variety of endpoints.
- Editing or creating a resource needed to go through the validation process; these are repeated in every similar method.
- Getting the teams’ IDs require a reference to another data structure.
- Lists of parameters are long, and in most cases, you get the data from the same object.
So, now when you know what to look for during the refactoring process – fixing those shortcomings shouldn’t be that difficult. Most often, code fixes will be about moving logic to suitable classes or adding a new method. So then, to adjust our method accordingly, you should:
- Modify the constructor of the class FootballMatchRequestModel in the way that it takes a Request object as the only parameter.
- Create a base class for controllers and a method which will handle validation errors inside.
- Shorten the list of variables of the method which updates the events service to the ID and FootballMatchRequestModel model.
- Use the whole event rather than a long list of the fields to create the object FootballMatchResponse.
Besides having knowledge of the best practices connected to the improvement of code clarity, it is nice to know a tool you have to work with. In our project, it’s Symfony with the FOSRestBundle module. The first advantage you can use is Symfony’s Security component which allows you to define the access to the method in controller using annotation.
💡Read more: Symfony documentation (Security)
Another simplification that you can introduce would be getting rid of the multiplied manual creation of the request object scheme. This data will be later verified in terms of correctness to make sure it can be used safely in the subsequent parts of the application. Luckily, the creators of FOSRestBundle foresee the possibility of automated attribute filling of the indicated object. What’s more, they allow running a validation process for the supplied model. This option isn’t enabled by default. That’s why, if you want to use it, you need to apply some changes in configuration. There are, however, only a few more lines of code to be added.
💡Read more: FOSRestBundle documentation (Validation)
Eventually, in the method’s comment block, you should have annotations for @Security and @ParamConverter. The latter needs the addition of some extra method’s parameters to work correctly. Those parameters are: a model to which the data is converted and a variable which contains the list of potential validation errors. All those actions and improvements helped slimming down method’s body, leaving only 4 lines (out of previous 40!).
As you can see in the app controller fragment above, you can significantly improve the quality of code with some straightforward refactoring techniques. From now on, whenever you change anything in the application, you don’t need to look through the whole project and fix all the duplicated code fragments which use particular class or method.
Architecture transition phase
Once you’re done with cleaning, you can step into the architecture remodelling. Proceeding with your work, you should always remember the aforementioned “baby steps” rule to make sure the app works fine.
Also, you need to avoid some radical modifications in the project. That is why I introduced a transition structure between monolith and microservices. To achieve that, it is necessary to use a style of architecture which will resemble a microservice one. It turns out that a concept called Bounded Context can be helpful here. It’s derived from DDD (Domain-Driven Design) methodology. Bounded Contexts help with dividing a complicated business domain into smaller contexts. One of the essential assumptions of this concept is that all the contexts boundaries must meet a specific restriction: contexts can’t affect processes which happen in other contexts.
💡Read more: Bounded Contexts
But how to divide a domain into the contexts? Unfortunately, neither DDD nor Bounded Context provides detailed hints of how to do it. You need to do it by ourselves. Let’s get back to the business domain and try to figure out what kind of coherent group of behaviors or data sets occur there naturally.
Our client’s project does not seem to be too extensive and overly complicated. However, you can derive from the business domain the following contexts:
- users context – administrators and registered users of the platform,
- teams context – list of the teams available in the app,
- events context – a game between two teams which takes place on a specific date,
- commentary context – commentary of the football matches.
Doesn’t it look a bit like microservices at that point?
Another architectural pattern which is often used alongside Bounded Context is Command Query Responsibility Segregation (CQRS). The pattern’s role is to separate responsibilities of methods that represent the intent in the system (command) from methods which return data (queries). To get a better understanding of this pattern, I can recommend reading a series of articles about CQRS and event sourcing implementation in PHP written by one of my colleagues.
With the use of the CQRS and it’s reading and writing separation of logic, you will get short, specialised commands and queries in your code base. Data classes and structures prepared according to that guideline can be placed in namespaces which correspond with the created contexts. Thanks to that, you can establish the first borders of contexts.
Knowing what kind of patterns you can apply, it’s time to think about how to adjust the codebase to CQRS. Let’s start with the proper implementation of a part related to commands. Tactician library will help us with this task. Tactician aims to ease the implementation of a behavioral command pattern, and its utilization. Usage of this library focuses mainly on three basic classes:
- Command – encapsulates input data;
- CommandHandler – executes the logic of connected command;
- CommandBus – invokes CommandHandler connected to an individual command.
Again, I will set a method which is used for editing planned matches (FootballMatchController::editAction) as an example. Before, you had to use a dedicated service which is corresponding to a match if you want to edit it. With the use of CQRS, the intention of changes should be enclosed in command and editing logic should be placed in proper handler.
Adapting to the new requirements, you have to envelop the data from query object in command (it’s just a simple DTO). Command will be delivered through CommandBus to the handler class, which then applies the logic connected with the update.
Here is the example of how to modify editing method and related classes – step by step:
- First, you have to create a command which will deliver all the necessary data to a handler.
- Then, you have to take a logic responsible for the event’s update out from the service.
- In the end, you can inject CommandBus into the controller. Then in the body of the method which updates a match, you need to encapsulate a query model with a command. Lastly, that command should be passed for execution.
So far, everything has been going as per plan but now we face an issue. The test of the method related to a match editing turned red. It means that our application does not work as it should.
In one of the steps of a testing scenario, we check the correctness of the returned data from the query sent to API. Before changes, the update method of the event service used to return the object of a particular match. The problem here is induced by Commands design as they don’t return data.
If you want to return the current state of an object in the response – you have to get it yourself. It’s the perfect moment to add in application the second part of the CQRS pattern, which is Query. It’s a kind of interface which allow accessing data (CQRS equivalent of Repository pattern). The data returned from Query needs to have a proper structure, so additionally I will introduce View objects. Purpose of the view object is to guarantee data immutability.
Let’s create a Query responsible for getting data associated with a match. The class to work will require the object of a database connection – You can use the one from Doctrine. Doctrine’s database connection will give you access to the DBAL (Doctrine Database Abstraction Layer) query builder, which will allow you to create queries conveniently. In our example query object, I did not use repositories because of a few reasons:
- a repository will add an unnecessary layer of abstraction and complicate the code;
- the closer we get to the source of data, the lower overhead of Doctrine ORM we get, thus the efficiency of a read operation is going to improve;
- a typical repository besides data reading also realizes some other functions like data persistence. If we misuse them, they can cause some side-effects.
Additionally, you should add an interface to the query. This will provide class interchangeability if you ever have to change the source of our data.
The last thing to do is to add and use the FootballMatchQuery inside the method which updates a match.
At this point, event editing endpoint modification is completed. All the tests turned green, which means that when rewriting architecture – other API components didn’t stop working.
Similar changes should be introduced in all of the remaining controllers and methods in the project. Also, you shall always keep in mind to run automatic tests after each of the more significant changes. It will help you ensure that anything broke by accident.
The code after all the changes presented above can be found in this repository.
See also: Microservices design patterns
An example given above illustrates a simplified process of creating an intermediate architecture before transitioning to a microservice one. This in-between state achieved using proposed CQRS architectural pattern allows easier app maintenance and further development.
By implementing CQRS in the system, project gained the following things:
- simplified models in the client’s domain,
- a clear boundary between read/write operations,
- ability to present data easily with the use of different view types,
- improved flexibility for changes.
Moreover, with proper planning, it is possible to implement changes in a few stages that won’t affect the elements of the system that has not changed yet.
There is a possibility that a client, seeing benefits coming from migration to an architecture utilizing CQRS pattern will stop the development at this point. We won’t. That’s why, in the next part of this article, I will present you how to extract first microservices. Additionally, I will cover the topic of infrastructure and the subject of launching a project in a new architecture using Docker. The second part of the article “Monolithic vs microservices: How we’ve successfully migrated our app” is available here.