The vast majority of you are aware of what I am referring to. It's difficult to create microservices, and it's more harder to keep them operating. Observing and analysing error logs on a regular basis is a headache. Furthermore, you must consider the impact of any changes you make to a service on other services. So, what's the deal with it being so difficult? What are the most important aspects of microservices to consider?
To begin with, establishing a microservice's scope is proving to be a difficult process. When you specify your microservices' scopes at a fine granularity, they become quite noisy. By chatty, I mean that in order to fulfil a client request, the microservices exchange a lot of data. Keep in mind that each service-to-service contact has a cost - delay - that the client apps must pay in the form of a bad user experience. When you set your microservices' scope to coarse, on the other hand, you're effectively creating monolithic service silos. Short response times may improve the user experience, but you lose any flexibility or extensibility without interfering with client applications.
Second, suppose you've determined the appropriate level of granularity for your microservices and that they're properly aligned. Assume that all services are built using Microsoft's.NET framework. You'll encounter a lot of code duplication if you rigorously implement microservices as an architectural approach, particularly boilerplate code for infrastructure. Every microservice needs some database startup logic, a connection to a tracer provider, as well as logging and health checks. How can you make efficient use of all of these cross-cutting issues and their implementations that all services share? Sadly, many developers use one of the most well-known anti-patterns in such situations: copy-and-paste. This has a number of drawbacks, including the fact that similar code snippets exist in each service, as well as the fact that bug patches may take a long time to surface online.
Third, the structure of source code might become a major development roadblock at times. All service implementations are frequently included in the same solution. On the surface, it appears to be logical, right? It's easier to keep track of changes, reuse items from other microservices (particularly the models they implement), and so on when everything is in one location. Wait. Rewind. Models from other microservices are simple to reuse. This isn't going to work. Why? Because when a model that two or more services share (and utilize) has to be updated, all services must be updated. This is incompatible with the grail's pliability! So, what's the best way to get rid of these problems? What's the best way to avoid them?
I'd like to take you on a trip through how to conquer these basic difficulties in this post. I'll begin by outlining each microservice's blueprint. The blueprint, which uses a ports-and-adapters method, specifies the architecture of a single microservice, including how service assemblies interact. Following that, I'll teach you how to define the scope of each microservice using patterns and concepts from domain-driven design (DDD). Why are they appropriate in this situation? Changes (most of the time) originate from a certain business area, and the services serve that business. Changes affect (most of the time) only one particular microservice if your microservice landscape is properly aligned with the form of the company they support. I suggest that all cross-cutting issues, such as database abstraction, logging, and tracing, be addressed using the service chassis design to speed up service development in general. Because publishing trace information or logs is the same for all microservices, using this strategy greatly accelerates the development speed of your teams. The service chassis is distributed as a NuGet package, thereby decoupling services from implementations of cross-cutting issues. Finally, I discuss source code organization, focusing on how to separate microservice development to avoid reusing code that shouldn't be repurposed. Additionally, my technique offers you with a maintainable framework that allows you to simply track changes and deploy new services on a regular basis without constantly disrupting your system.
Let's get going on this adventure!
To begin, I'd want to discuss microservices in general. To be more specific, I'd want to offer you a sense of what microservices are, what features they have, and why they're a good idea to use.
Microservices are a kind of service-oriented architecture (SOA) that consists of many loosely linked services. Each microservice provides business logic that adds value to your customer's experience. Important properties of microservices in a microservice architecture include:
Microservices are self-contained: The technique to building a microservice may vary from one to the next, maybe even down to the programming language used. Which strategy, technology, and languages to apply or employ is left to the microservice implementation team.
Independent services: Any other microservices in a microservice architecture are unaffected by the deployment of a new service or the upgrading of an existing one. They are self-contained and may be changed on-the-fly without affecting other services.
Services aren't tightly linked: Microservices, in a perfect world, would only be loosely coupled. When I say coupling, I'm referring to how the services are interconnected, the models they use, and so on.
Business-centric services: Unlike traditional service-oriented methods, microservice architecture services are built and evolved around business capabilities rather than technicalities.
Figure 1 shows a typical meal ordering microservice design. To provide functionality to the user, the client application makes use of microservices. Each microservice has its own database and is self-contained. In this case, a single microservice handles a single aspect of the larger business domain. The order service, for example, is responsible for placing restaurant orders. After the user has completed an order, the delivery service contacts a courier to bring the meal to the user. The delivery service requests the user's address from the account service for this reason. The client application, on the other hand, is completely unaware that after the order is placed, the delivery service searches for a free courier to deliver the meal to the user. Also, the client application isn't interested in how the backend handles it; all it cares about is that the task is done.
Client apps often use HTTP requests to connect with microservices. Relying on this protocol has become commonplace. In other instances, though, gRPC or other protocols may be preferable. All of these solutions ultimately serve the same goal: to abstract the communication between the client and the microservices.
What are the benefits of microservices? Before we embark on our adventure, the following is the primary reason: Because all business logic is handled on the backend, applications become extremely lightweight. In a worst-case scenario, the app is entirely made up of front-end code with no business logic. You may develop a mobile app and a desktop app with the same business logic without incurring any additional costs (in terms of business logic). Microservices also allow you to write to many apps that just use a fraction of the business logic by utilizing only certain services. In other words, you specify the extent of functionality you want to offer with your application by selecting the services on which it depends. Microservices also remove the need to constantly replicate business logic across many applications. Furthermore, when a problem patch is rolled out, the client apps do not require an update since the update occurs only in the appropriate microservices, and all client applications receive the bug repair immediately and without effort.
I hope I've persuaded you to utilize the microservices architectural style as a starting point for your journey.
The Blueprint of a Microservice is the first stop on your journey.
Before you begin coding, you must first decide on the microservice's implementation language and architecture. I'm assuming C# for the former. The latter option is far more nuanced, and making a good judgement here will be crucial later. Before choosing a design for the blueprint, let's sketch down some ideas for how to organize each individual microservice.
The first choice relates to a service-by-service approach (see Figure 2). The dependencies between the projects that make up the service are organized in a strict hierarchical manner in this method. An ASP.NET WebAPI assembly (which contains the service's HTTP endpoints), a business logic assembly, and a storage assembly are often included in the layers.
Separating services into different repositories allows you to release your microservices with a lot of freedom. Assume you disregard this post and consolidate all of your files into a single repository. Let's say you need to repair an issue in the order service while also adding a new functionality to the shipping service. The issue repair in order services is completed ahead of the new feature implementation in shipping services, which appears to be suitable. In such scenario, you finish the process by submitting a pull request from the bug fix branch master. What's the best way to get that fix out now? I mean, the shipping service's feature branch is still operational, which isn't a problem in general. When you now release the order service, make a Docker image for it, and run it in Kubernetes or something similar, you also (I think) archive or tag your existing master for maintenance or something similar. Even if you simply modified one service, you'll probably need to archive or tag the entire master in that scenario. You are in charge of such a procedure for a modest number of services, but you have 30 in the same repository. That is going to be a disaster! The only way out is to dismantle this massive master into small, self-contained parts.
This division of the codebase also corresponds to the dimensions in which programmes often evolve. Remember that most updates to services come from the sub-domains they support. As a result, having one repository per service allows you to design and control the procedures for releasing and developing them at the appropriate level of granularity. Simply create a new branch in the relevant repository when a service requires a modification. Close the pull request toward the master and release it when you're ready to release the service. The rest of the services and repositories are unaffected. The team in charge of making the modifications works totally independently of the other teams, giving them a great deal of flexibility.
Let me summarize this adventure. I've taught you how to create microservices quickly in the.NET environment. There were numerous stops along the way. I started by looking at how a microservice is structured and how a single microservice appears. Selecting a single architectural design for all microservices simplifies your life since you can quickly transfer developers between teams. They are familiar with the appearance of all services, which cuts down on the time they need to learn about overall service architecture. Then I came to a halt at the extent of service. This halt was necessary in order to comprehend what constitutes a single service. You align the scope of services with domain-driven design's bounded-contexts, which divides all of your services into sub-domains of the business they support. Because adjustments are more likely to occur there, you're prepared for them and know exactly where to make them. Services require certain cross-cutting functionality, such as logging, tracing, and so on, in addition to business logic. The inclusion of a service chassis enables you to deal with them consistently across all services while also drastically reducing source code duplication. Finally, by using the organizational strategy from the previous stop, you may accelerate development even more.