In the previous post, being sort of a teaser, I made a brief introduction to DShop project, as well as the idea behind the overall course. Starting from now on, we’ll focus on the fundamental parts of DShop, including the theory behind a particular concept, its possible solutions, and eventually an implementation.
At the time of writing this article, we’re still working on the project, and some of the concepts may change – we have received a lot of great feedback so far (during conferences, events and as comments) and there are always pros & cons of the chosen solution when it comes to distributed systems. We are to share with you our own approach, yet it may evolve (hopefully, to the better) in time, so just keep that friendly warning in mind :).
Speaking of theoretical concepts, when it comes to microservices, that’s a lot of the ground to cover (I strongly recommend reading this book), however, we’ll focus on the core comparison, between monolithic and distributed world.
Single vs Multiple solution(s)
Monolith means a single solution (although it may contain a lot of projects having references to each other), which also means a single repository. On the other hand, in the microservices area, ideally, you want to have a single repository per service. And why is that? Mostly due to the fact, that services should be separated from each other and a single team should be responsible for its implementation, deployment, maintenance and so on. You could keep everything in a single repository, or even use Git submodules, but well – in a real world, you want your projects to be flexible and decoupled, so start with separating them into distinct repositories.
Domain vs Bounded Context knowledge
Given that you have heard a little of DDD, the microservices turn out to be a good example, of so-called bounded contexts. Just take a look at DShop solution – there’s a service dedicated to products, orders, users and so on – although they’re most likely to somehow work and communicate with each other, they represent different use cases and boundaries of your business. Simply saying, if you think of the services separation, you should think of the bounded contexts as well. Start with a monolithic approach by putting them all e.g. in different folders and once you figure out how to split them, these might be good candidates for the unique services. Just beware that it’s usually a difficult task.
Immediate vs Eventual consistency
Sending a request to HTTP API, means receiving a response indicating either success or failure, correct? Well yes, and if you get a 20X status code, that means that data was somehow modified (as long as it’s not a GET request) at this particular moment. Again, that’s totally true, as long as your API handles the request on its own. Otherwise, the request gets pushed further to the message bus and has to be consumed + handled by specialized microservice. Which takes time. How much time? It could be milliseconds, as well as it could be hours, depending on the scenario. Whenever you work with a distributed system, there’s always this “delta of t(s)” when the data is not consistent as it has to be stored asynchronously by a different service.
Internal vs External communication
Handling a command? Getting results from a query? In a monolithic application, all of your components work with each other in the same process – your domain objects, application services or controllers, thus they communicate “internally”. In the distributed world, when service A has to talk to service B, it means sending HTTP request to that service B that may be hosted on the other part of the world, which may take time and be vulnerable to network partitioning.
Single vs Multiple technology/ies
One of the beauties of the microservices is being able to use the best technology available (language, framework, library etc.) in order to solve the particular use case. You can have 10 microservices, written in 10 different programming languages and still create a top-notch system. It’s also one of the reasons, why you should strive for having the different repositories for your projects.
Vertical vs Horizontal scalability
Let’s say you have a monolithic application and it handles some requests that are either CPU intensive or just take a significant amount of time or resources to complete, whatever the reason is. You can add more cores or RAM (vertical scaling) but it might cost a lot + there’s a limit to such upgrades. You could also scale your application (horizontally), by adding more instances and spreading the workload into different servers (+ putting the load balancer on top of it), yet again – some requests may be literally “draining” your application and it becomes unresponsive. If you work with microservices, you can easily split e.g. these demanding parts – why not having a single instance of HTTP API and 5 instances of Worker Service and just ask it to process the heavy requests? Your API shall remain responsive all the time, and you can easily scale out (horizontally) the microservices that have to do a lot of work.
Single vs Multiple unit(s) of deployment
Deploying a monolithic application is quite easy, however, deploying N services? Well, that’s not an easy task. Although, you can use a variety of tools and have a fully automated CI & CD, still, there’s a lot of things that might go wrong.
Single vs Multiple points of failure(s)
This one is a sort of continuation of the previous point. If your deployment fails or your monolithic application crashes, everything is down, nothing works. On the other hand, if one service goes down in a distributed scenario, the rest of the system might (and probably should) still work. Well, maybe you won’t be able to add a comment to your blog post application, but still will be able to read an article. However, it’s not an easy task, to make your services resilient and behave properly when one of its dependencies (other services) become offline.
Easily vs Hardly maintainable
Maintaining an application is rarely an easy task, as it all depends on the size of a project, its architecture, patterns being use and code quality. However, it tends to be much easier to work with a single solution that knows about everything and handles all of the requests, than with a solution who has to rely on other applications, being deployed somewhere, far, far away. Whether it’s deployment, external communication or not breaking the data contracts – these are just a few things that make it usually more difficult to build a distributed application.
Synchronous vs Asynchronous
I’m not telling you, to no longer use Task, async and await features. In that context, asynchronous means that handling HTTP request being sent to API will not yield an immediate result. It will be sent to the message bus, and the processed by specific service. The caller of the API might get 202 (Accepted) status code, which means that a request is being processed, but it might take some time (already mentioned “delta of t(s)”) before it’s completed.
Tight vs Loose coupling
Loosely coupled in that case means that due to the fact of microservices being treated as separate applications (which can be hosted anywhere) we might think of them as components that talk to each other but are totally separate and concentrate on their own part of the overall domain. We could even state that a well-designed microservice should imply a high cohesion.
Generic vs Specific usage
Although, there’s been a lot of hype in the last years, and almost everyone desires to implement the microservices without giving it a second thought, let’s face a rather brutal truth, most of the time it makes no sense. Most of the time, a monolithic application will be the best choice. It will save you a lot of time, resolving the things that happen only in a distributed world. Honestly, I’ve started playing with this architecture almost 2 years ago, and during that period, quite a few times I had to figure out some really weird things on my own. As you will notice during our journey, even deploying a project having a different set of packages is not a trivial task, not to mention the other, more complex scenarios. However, isn’t our life also all about the learning? Getting to know about different programming paradigms or patterns is a way to go (at least for myself). And if implemented correctly, the microservices can give your application quite an advantage. No more bottlenecks, distributed workload, full asynchronicity, and finally, adding “microservices knowledge” to your resume sounds cool.
DShop solution structure
And once again, below I do include the current solution structure.
Let’s briefly discuss one by one, what each repository is about:
- DShop – nothing special about this one, just a set of common scripts (e.g. Docker ones).
- Api – so-called API gateway, an entry point to the whole system – the end-user communicates mostly with this one, except the Identity Service handling the registration and authentication process.
- Common – helper methods and classes, sort of infrastructural project e.g. authentication, database connector and so on, used as a NuGet package.
- Messages – commands & events being used through the whole system.
- Services.Xyz – microservices dedicated to handling Xyz bounded context.
- Services.Operations – tracking the user requests (whether they completed or not).
- Services.Signalr – pushing data via web sockets.
- Services.Storage – storing read models being used by API clients (CQRS approach here).
Some of these projects may be updated, for example when you think of having common messages package vs messages per service package vs no packages at all – all of these approaches have their pros & cons. Going further – you could keep generic utilities (as we do in the Common package) or split them into smaller packages or have no packages at all and simply enforce each service implement e.g. the same code to handle MongoDB connection. We’ll discuss these approaches in the upcoming posts, therefore stay tuned.