My story is about microservices, and my work in this area is how I started my career as a developer, which is what I’ve been doing for the last four years. It gave me the opportunity to learn a lot in relatively short timeframe, as well as deal with challenging situations and business requirements. I also learnt to appreciate good practices and had the opportunity to understand the importance of design patterns. But most of all, I learnt how good team communication and collaboration with a client can make or break a microservice dream.
The what-microservices-are-all-about pattern
DDD or domain-driven design and the decomposition approach go hand in hand and make up the core of microservices architecture. It’s safe to say that you cannot have the latter without the former. Sometimes, at the beginning of a project, the project will be thought-out as a microservices ecosystem. But sometimes, as in my team’s example, it’s something that you and your client grasp together along the way. Business requirements compel developers to make the impossible possible, and slowly but surely you come to the realisation that what you need lies in the distributed approach to the architecture.
If you’re lucky, you start your exciting journey by throwing yourself into DDD and decomposition patterns because you know microservices are waiting for you and you’re well-prepared. If your story is more like ours, you and your client will hopefully recognise, soon enough, what you’re dealing with and start applying microservices application design patterns. Following them sometimes looks like stagnating and doing things that don’t have any immediate benefit. But if the strategy and roadmap are well defined and thought out and communication between teams flows, as the system becomes bigger and more complex, the benefits become more obvious.
Microservices are all about creating smaller units of functionality and business logic that are autonomous and modular, which leads to consistent and reliable communication between such units. A decomposition pattern shows us how to divide an otherwise monolithic application into subdomains or microservices, with each microservice revolving around its domain. That domain represents a finer segregation of business processes that your client wishes to offer to the public. For instance, we can have one microservice managing all general things regarding IoT devices, another microservice responsible for providing weather forecasts, another managing users, and another one identifying them, while another one defines users’ rights. There are many more examples, but hopefully it’s clear here what a business domain can represent. Right now, our count is on around fifteen microservices, and we’re expecting more to come. The diagram below represents how a few of them look like and how they communicate with one another.
DDD and decomposition pattern
Strangler or vine pattern
If you’re dealing with migrating an existing system to a microservices architecture, you can use a strangler or vine pattern. We didn’t have to take this approach as we’re replacing an old system by building a new one from scratch. Sometimes the slow replacement of an old system is necessary, and, in such cases, it’s recommended that you replace pieces of the old system step by step, implementing new parts and shutting down the old ones. With this approach, there are fewer risks and greater damage control. But of course, as with standard microservices creation, it needs to be well thought-out.
Of course, in our case, there are small parts of the old system that would take a lot of time to migrate to the new technology stack, and our client wants us to leave them as they are for now. Microservices architecture allows us to do just that. We can create a layer on top of the old system so that all the parts of the new stack can communicate with it if they wish, without creating changes that would compromise the new architecture.
A microservices architecture most often comes as a response to dissolving monolithic systems. As we’ve touched on, it involves dividing a stretch of services, concerns and communications into smaller components that still need to be able to work together in a persistent, consistent and secure manner. Most often, divided microservices will also have separate databases, so here comes the challenge when an endpoint needs to provide aggregated information that is not contained in just one data source. Because of this, an aggregator pattern can be implemented to mediate between microservices and make collating data possible. The diagram below represents an arrangement where Microservice A contacts both its own data source and Microservice B’s database to finally publish necessary data on its (Microservice A’s) public endpoint.
Implementing this pattern is something we do as the first step before embracing API Gateway – an advanced version of an aggregator pattern. This approach overcomes the multitude of challenges that microservice architecture can bring, and it’s also called ‘back-end for front-end’ because it sits between microservices and client apps, acting as a proxy while keeping a client’s app needs in mind. While we’re on the topic of aggregation, this approach can also take over, on its own , authentication and authorization, service discovery, response caching, retry policies, load balancing, logging, tracing and other functions. It’s recommended that one Gateway shouldn’t take a monolithic role covering too great a range of microservices; instead a Gateway per client app can be created, as can be seen in the diagram below.
Because the communication chain we’ve just discussed is a distributed system, the processing of a request from the front-end can take a long time. Between the first microservice and who knows how many more, multiple HTTP request/responses or events can be published. Because of this chained nature of communication, asynchronous messaging is another pattern often used in microservices design. In our solution, user creation is a serious business, covering multiple microservices and steps that need to be completed successfully in order for the valid user to be created. It wouldn’t make much sense for the end user to wait on the GUI until all the steps have been completed so the request is handled asynchronously. The user gets the response that the request has been successfully handled in the first microservice, from which proceeds a delegation to the other microservices. This, of course, requires the implementation of faulty correction patterns.
Imagine all of the problems that can occur with all these communications relying on a number of services, just so that one request from the client side can be processed. There can be many reasons why a needed services may be unresponsive, and that’s something we must be ready for when working with distributed systems. Luckily, there’s a pattern jumping to our rescue. Again, like with the aggregator, we have a proxy service, which this time acts as a circuit breaker. Its job is to monitor the number of unsuccessful requests in a time period, and it blocks further requests until a new time period starts. After that, it will allow only a limited number of requests to be processed, and in case they’re successfully processed the circuit breaker resumes with its normal execution. Otherwise, it falls back to the failure operation mode. Its logic can be implemented as part of the API Gateway.
This is something we haven’t yet implemented in our system, but it’s obvious we’ll need it when the time and resources call for its implementation. Until then we’ll be sacrificing performance, cleanliness in our logs and good user experience. As the production date approaches, the need to implement a solution for these issues will become greater. To work with microservices means to know how priorities are set at any given moment as well as how resources are utilised. Build you microservice architecture layer by layer, and let the robustness come over time.
Another pattern that deals with failures in the execution of applications is a saga pattern. It’s called that way because it’s built upon a sequence of steps some workflow consists of and for each step is defined a compensating transaction or rollback. This implies some cleanliness in the code. Before we start thinking about implementing sagas in our solution, we needed to do a good clean up and refactoring of classes. Apart from it being good coding practice, keeping a clean code and a good service classes structure is needed so that sagas can be implemented in a sensible and elegant way.
In the simplified example in the diagram below, we can see the process of creating a new resource in the system. The saga starts by creating a resource within the domain of Microservice A. Afterwards, the responsibility is shared with Microservice B because the resource needs to be created there too. In case something goes wrong at this step, the system must be ready to either compensate for the affected changes or to revert them. A retry policy or something else more applicable can be implemented, depending on the given situation. If everything goes smoothly, a saga can move to the next step and so on until the end of the process. After all the steps have been executed, the saga has been successfully completed.
Separate databases and event sourcing
This pattern goes hand in hand with a separate database pattern, which is often followed in the microservice architecture. The benefits are that changes that occur in each microservice stay in the relevant microservice’s domain. The changes, generally speaking, also bring fewer risks because any database-related errors are contained in only one microservice. For the data that needs to be kept synced, the events can be published so that the change is propagated from one database to another. In the more advanced stages of microservices implementation, event sourcing can be introduced. With application of this pattern, events are stored in the database regardless of whether they’re successful or erroneous, which enables the tracing of what really happened.
Communication and team spirit
This pattern is one that, in my opinion, is too often omitted in discussions about software development, especially development concerning complex architectures, such as the ones we discussed here. Timely, agile, precise communication done in good faith and with concern of other teams’ contexts is one of the pillars of a successful microservice story. When you’re working on microservices, the chances are that a great number of people will contribute and therefore increase the challenge and complexity of development. In our project, we saw that when our team suddenly expanded, the need to be agile emerged not only with technology but also in people communication. To adapt to new situations, be ready to proactively take part in your own microservices story, and ensure that you and your colleagues have one another’s back and understanding. That’s what working on microservices is all about.