You need to be this tall to go from monolith to microservices—Part2
Or what we would have liked to know before we started
This is the second part of our series of articles about our migration from monolith to microservices. Read the first article before.
Second axis: Continuous Delivery
Now, you want to deliver many applications continually and quickly. It is therefore necessary to have a robust Continuous Integration system. Of course we imagine that you already have an instance of a CI tool, randomly Jenkins. This single body, which is quite reasonable for some applications, will quickly become a burden for microservices. First of all, the frequency of builds is greatly multiplied by the number of applications, which can slow down your CI and therefore the teams’ delivery capacity. Maybe you will add the use of Pull Requests, each with its own branch and therefore its own build. The memory and CPU load on your CI will increase drastically. And, as if that wasn’t enough, already annoying problems like open port conflicts during integration tests will become even more unpleasant. If you want the build queue to remain reasonable and avoid too many false positives, you will need to scale your CI. In our case, we were able to take advantage of a passage on Docker with Jenkins launching each build in a container, all with pipelines coded in groovy and versioned on Git.
You could also use the all-new Jenkins X to take advantage of Kubernetes, competitors like Concourse or GoCD or SaaS solutions like CloudBees. The important thing is to be able to quickly scale your CI according to your needs.
To deliver functionalities, we are often used to create branches. These branches regularly cause painful moments during the merge on the main branch. This pain is proportional to the number of branches and their lifespan. To this, you add the number of applications and the number of developers, and you have a significant impact on your ability to deliver. To avoid this, the so-called “Trunk Based Development” or TBD technique is used. This practice is considered mandatory by the authors of the book Continuous Delivery to have a real continuous integration. On our side, we are less extreme. We make branches with a short lifespan to allow code reviews (technique known as “GitHub flow”) which offers a close solution with its advantages. In both cases, this requires a lot of rigor because you have to be ready to deliver your code at any time if possible without pulling a fixed bug branch (let alone having a “prod” branch and a “dev” branch).
How can we be able to deliver this unique branch at any time? By using toggles features (or “Feature Flags”) that allow you to deliver unfinished features or test them before opening to all users. This ability to enable or disable features directly in production is also an excellent way to deliver very regularly without long QA campaigns because it is still possible to disable code if it causes errors.
Delivering should be a nonevent!
Toggles features are also one of the elements that make it possible to transform deliveries into “non-events”: a non-event can be done without worries at 5pm on Friday for example. To do this, it will be necessary to set up infrastructure as code to manage the creation of environments, the installation and start-up of the application and the updates of your database schema. The objective is that your deliveries to your different environments are fully automated and executable with a simple click. If you are still shutting down your application server, putting your package on it and then restarting it manually, don’t even think about doing microservices. Start by setting up an automation tool such as Ansible, Puppet or Chef to describe and version your environments. In our case, we use Docker and its images to create services that are very easily deployable. And to manage our environments, we use “docker compose” description files versioned on Git in association with Vault for the management of secrets (passwords, certificates…) Jenkins allows us to start building the service and the associated Docker image but also its deployment in one click. You can even have automatic deployment on certain conditions or at certain schedules.
Without knowing it, we talked about an essential organizational element of a microservice architecture: a DevOps culture. If you want to achieve this automation, you will have to work in collaboration with production. And if you think that doing JIRAs will be enough to set up the tooling or deployment, well, you’re wrong.
Strong DevOps practices are the keys to an ability to deliver quickly and consistently without any worries. The most advanced are even able to deliver directly in production without any manual tests or integration environment (yes it is possible!)
Third axis: Collaboration
The organizational difficulties
Developing a microservices platform requires not only technical rigour but also an appropriate organisation. It is natural to divide the development of a microservice platform between several teams, given the fragmentation of responsibilities and the separation of the code. This is one of the great advantages of this type of architecture: to be able to grow very quickly. However, unlike the technical aspects, the organization remains one of the biggest dangers.
You are probably familiar with the expression: “nine women can’t make a baby in one month”. An expression that comes from Fred Brooks’ The Mythical Man Month, published in 1975. The author explains the drifts caused by a misinterpretation of the unit of development cost: the man-month. Thus, doubling the number of developers will not reduce development time by half.
And for good reason, the training of newcomers, the various meetings, the sharing of information through slower lines of communication are factors of slowdown. And so adding people to an already late project will increase its delay!
In addition, the way the organization communicates will have a significant impact on the product code. This fact is explained by Conway’s law, which states that the communication structure will inevitably be replicated in the code. Thus, if two teams produce a platform, it is very likely that it will be divided into two different subsystems that are quite heterogeneous!
On our platform, there are two types of teams with different motivations: feature teams and component teams.
The feature team aims to develop the functionalities of a platform. It operates on several microservices in order to best meet the need. Like a microservice, this team is autonomous and requires a composition with various roles: developers, testers, designer, product manager or even a manager. This will ensure that this team is perfectly aligned with the business need. However, be careful not to exceed a certain size in order not to lose efficiency! The concept of the pizza team illustrates this fact well: a sufficient size to feed a team with two pizzas, i.e. a maximum of 8 people.
Conversely, the component team is specialized on a specific component or role. It will have the advantage of controlling its perimeter and making changes or tasks quickly. They can operate in different areas such as support, integration and continuous deployment tools or server maintenance. This can make sense when the component is centralized and standard to an entire platform.
Nevertheless, a question arises regarding the maintenance of a microservice. Considering that a feature team can modify any microservice, who is responsible for maintaining them, in terms of design or in case of problems. This question has generated a lot of debate on our platform.
The first attempt was to give responsibility for a microservice to the team that initiated it, which is problematic when the team or its original members are no longer there.
The most interesting solution comes from open source with the concept of Trusted/Untrusted Commiters. Thus, the people in charge of a microservice are those who initiated it or work on it the most and become guarantors of design, quality and initial intent. As for the other teams, any development will go through a validation mechanism (Pull Request) aimed at reviewing the code by the team in charge of the microservice.
Software development is an apprenticeship of the profession to which it responds, on its environment and its organization. For us, designing a microservice architecture is an adventure where failures as well as successes are an integral part of our creative process. And as Alberto Brandolini says so well:
Software development is a learning process, working code is a side effect
Our architecture has been designed gradually and in an emerging way. Some attempts have been successful, others have not. And in order not to repeat the same mistakes over time, it is essential to capitalize on these experiences. All our technical and organisational choices are motivated by a specific need in a temporal, social, technical and political context. But it comes at a time when these solutions are no longer adapted due to the evolution of the project or the technology. Thus, the “Architecture Decision Record” (or ADR) can help you document these choices to justify them to your successors.
Another practice to spread knowledge is to make workshops or gatherings sacred. In our project, the “tech chapter” is a recurring point that aims to share the platform’s issues, evolutions or experiments. The topics discussed at this meeting are decided upstream by the votes of the members, via a GitHub. Thus, all our strategic decisions are presented and some of them lead to standards shared by all.
Through standards, you will be able to guarantee the consistency of your system. Because a microservice in itself is an autonomous application with its own problems and technical components, certain standards must be set, particularly at the frontier of services. For example, how to communicate via a single RabbitMQ message broker, how to manage the configuration or how to monitor the services. All these standards will limit friction points with other teams.
Finally, all these standards can be used to constitute a kind of guide or charter for the microservice and can help you to judge its extrinsic conformity. It is a kind of “good citizen’s guide” that stipulates all the prerequisites in terms of functioning and quality. And in order to apply this charter, compliance officers, automated or not, can ensure the compliance of a microservice. Of course, code review or pair programming are simple and effective ways to ensure compliance with this charter.
By dynamics we expect synergy between developers regarding the harmonization of practices. And when a platform is composed of several teams, these practices are likely to be heterogeneous. This is not about imposing code rules or a particular language. For us, this focuses more on the design process or programming paradigms.
Through katas, we maintain a technological watch by simply exercising at lunchtime. These workshops take place on a recurrent basis and are on a voluntary basis. Of course, the ideal would be to hold these workshops during working hours as an integral part of the development, as well as a new feature. You can discover a new language, a library, a framework or a new way to practice TDD for example.
Pair programming helps to maintain the technical coherence and quality of a development. This practice can be systematic for each development to be undertaken. On our side, we have chosen to apply it only when the functionality to be developed exceeds a certain complexity. And in some cases, for a POC or a new microservice, we carry out “mob programming” bringing together an entire team around the same screen, where the keyboard changes hands at regular intervals. These workshops are very effective in tackling a problem. However, be careful to prepare it well, as it brings several developers together in the same place and at the same time, and can turn into an endless brainstorming session!