Microservices are the 'in' way of developing software at the moment, and have been for quite a while now. They certainly seem to be past the 'oooo new shiny thing' stage that is prevalent in software (I'm looking at you javascript frameworks).
I've spent a lot of my working life recently either migrating monoliths to microservices or building new microservices from scratch. One of the biggest challenges is always communication.
In a traditional monolith, different components of your app communicate with relative ease and simplicity. A method call here, a using statement there. Everything is nice and simple.
Take the below rather contrived example:
using MySampleApp.ProductModule;
namespace MySampleApp.OrderModule
{
public class OrderService
{
private readonly IProductService _productService;
public OrderService (IProductService productService)
{
this._productService = productService;
}
public void PlaceOrder(Order newOrder)
{
this._productService.ValidateProducts(newOrder.Products);
}
}
}
A new order is placed, and the products in that order need to be validated. The app has one big shared codebase. The product service can simply be injected into the order service and used. Simple.
The complication of microservices
Imagine the same scenario, but in a world where the Order and Product services are completely separate applications. In their truest sense, microservices are completely self-contained processes that have no direct relation to each other.
Does the monolith still sound so bad?
So how can these two disparate processes, which if true to their design should have no direct link to each other, communicate. Luckily, there are quite a few options.
Good ol' REST
REST has been around for a very long time. It's a simple protocol for communication via HTTP requests.
There are some standards for the requests themselves (GET, PUT, POST etc..) but the content of a request is decided by the service itself.
The Good
REST is a well documented, well standardized and extremely common way of communication. I'm confident almost everybody reading this post will have interacted with a RESTful API at some stage in their development career.
Almost every language has built-in support, and there are 100s of 3rd party libraries that add more features to the standard HTTP calls.
It's also pretty decoupled. A URL for the location of the API is all that is required to get off the ground. Although in my personal opinion it's almost too decoupled for its own good some times. Which leads quite nicely on to
The Bad
The contents of a REST response is completely open to change by the API itself.
Let's say the order team expects a property named 'productCode' in the API response. The product team has a big refactoring/cleanup session and the property is renamed to just be 'code'. This is perfectly valid, but suddenly the order service will stop functioning.
From experience, these kinds of issues are always a pain to find and can go unnoticed for quite a long time depending on the use case.
There is also the matter of the endpoints themselves. In a large application, there could be 100's of these microservices. If all of them communicate via HTTP that is a lot of settings/endpoints to keep track of.
An API gateway could be used to give a standard base endpoint that all requests are routed through. That does then add an extra layer of complexity and a single point of failure, but offsets some of the complications of having potentially hundreds of different endpoints.
Event Bus
Event buses (or message brokers) are a pattern for mediating the communication between applications. They minimize the awareness different services have each other and are by far the most decoupled way of managing inter-service communication.
In our example scenario, the order service would raise a 'ValidateProducts' event to the event bus. This event can either be synchronous (request/response) or asynchronous (publish/subscribe).
The product service, would connect to the EventBus and listen for all events matching a set of criteria. In this instance, any ValidateProducts events. When an event is received, it is handled and marked as complete.
If a response is required, the event handler returns the content of the response to the event bus which then manages the routing back to the original requestor.
The world of event buses is magically decoupled!
There are many different event bus providers, of which I'm not going to get hung up on in this post. But there are a number of considerations
The Good
In my opinion, an event bus gives the best level of decoupling. All of your 10/100/1000 services communicate through a common medium, whilst not having any knowledge at all of each other.
It also gives an intuitive way of structuring your code. The order service creates an order and then publishes a 'NewOrderAdded' event. It doesn't need to care who/what is interested, it is solely concerned with creating the order and letting the world know about it.
The Bad
The downsides are very similar to using REST. Whilst more decoupled, the problem of a structural change still stands. The events at either side are expected to have the same object model. If one is to change, the same problem arises.
As far as common .NET implementations go, the event is serialized as JSON and passed around. If JSON property names change, then the property in question will not be parsed.
Having all events processing through an event bus, also gives a single point of failure. Most providers support clustering and failovers, but it is still a single place that can go wrong.
gRPC
gRPC is a relatively new kid on the block, and of the 3 options I'm covering here it is the one I'm least familiar with.
It is an open-source remote procedure call system originally built by Google. HTTP/2 is used for the transport and it supports protocol interfacing as standard. For any fellow .NET devs, think language-agnostic WCF.
A gRPC service generates a .proto file. These files can be consumed by services and act as an interface description between the caller and the remote server. The proto file covers method signatures, variables and response types. All in one single file.
Almost all popular programming languages now have support for gRPC implementations.
The Good
My favorite thing about gRPC is strict interfaces. To call a method on a gRPC service, the corresponding .proto file must be consumed and therefore the way in which the method is called is explicit. There is no room for changed property names or problematic serializations.
It's also lightning quick. Services are communicating directly with each other, the data is tightly packed and sent using HTTP/2 which has a number of compression based benefits.
The Bad
Time. This is probably a personal thing more so than a reflection on the protocol itself. But if I take two services written in .NET Core, one using REST and the other with gRPC, the gRPC service is considerably more cumbersome to put together.
As a .NET developer, having the forced constraint of a gRPC service only ever running in HTTPS can make deployments difficult. Especially for a service running without any external access, within clients' existing infrastructure.
Similar to REST, it also has the complexion of the explicit endpoint for each service being known. So for the order service to make a request to the product service, the order service must know the exact location of the product service. Manageable, but scale can be tricky.
In Conclusion
So there are three different methods I've used on my journey through the world of microservices.
All have pros and cons, and between the three there is enough difference to negate almost all of the cons. And that, I believe, is the solution to the inter-service communication debate. A combination of the three based on the scenario.
For something asynchronous that has already happened (NewOrderCreated) use an event bus, for speed use gRPC and for simplicity use REST.
All the opinions in this article come from my personal experience building software, but I'm always looking to learn more. If there's anything I've missed or points you'd like to add, please drop a comment below. I love a good discussion!
Top comments (17)
Thanks for the article James. Good points noted here.
I do have one concern, which I think the example you have provided may just be slightly leading down the wrong path. Ideally, a microservice from my understanding should be able to work independently. By having direct service calls within the methods, the initial goal of decoupling is not quite achieved. We are still coupling the service and a break down of product service will still cause order service to fail.
What would be your suggested approach and communication methods to tackle that problem?
My though process would be that we could continue to use the methods you have provided here, but at an async level (perhaps worker svc) which will capture the relavent data and store in the order microservice. There can then be a standard in process call to the data store in the order service. Any thoughts on that or suggestions?
Thanks.
Srini, unfortunately, a completely isolated microservice that works independently is a myth. Services have to communicate with each other using any available mechanism.
Think about this: get the code of your monolith and refactor in such a way where its components don't interact with each other. It is simply not possible and does not make sense. Then instead of having in-memory method calls, adding a network layer and process isolation comprise microservices.
Hey Srini, thanks for the comment!
I completely agree on the coupling point, and that they should be able to work independently. That's one of the main reasons I always gravitate towards an event-based communication model.
All of the best message brokers support some level of state, so if the order service publishes an event and the product service is offline the event will just wait. Once the product service is back online then the event will be processed.
I think the point gRPC is trying to make with HTTPS only, is that there's very few scenarios where using HTTP make sense for any web-based API. This includes APIs that aren't publicly accessible.
letsencrypt.org greases the wheels for deployments, with very little overhead.
Hey Matthew, thanks for the reply.
I have always known about let's encrypt, but hadn't put two and two together in my head. You may have just changed my preferred interservice communication option :-)
Sweet! Ping me if you run into any issues. I've used it for Windows Servers a handful of times.
Amazing, will do. Thanks Matthew :)
James, you made me happy by sharing experience and mentioning inter-service communication :D I'm just glad to see other people having similar problems that I've had in the past.
It is funny that you gave a code example in the very beginning which does not work in a microservice world. Let me prove you wrong! (in a good sense) I've been building new microservice programming concepts where you can choose a communication mechanism without writing any code!
I wonder if this microservice project can also help with your needs.
Hey Serge, thanks for the comment. It's interesting your view that the example (albeit a very trivial example) doesn't work in a microservice world. Could you elaborate why?
DaSync looks an interesting project. I started working with Dapr last night which seems a very similar piece of work.
Of course! Let's say
IProductService
is HTTP or gRPC client. The code would work. However, now you need to handle errors if aValidateProducts
call fails on the network level. Then at some point, you need to replace simple request-response with a message queue to create a more robust workflow. In that case, you have to refactor the code of your services to accommodate message handlers - it is not a simple method call anymore.D-ASYNC gives you a language-integrated way to describe inter-service communication regardless is they use a synchronous or asynchronous channels. So in that particular example, you can use a message queue when calling
IProductService.ValidateProducts
without re-writing the method. That will ensure the statefulness of the method execution (a state of a workflow).And yes, dapr! In the middle of the spectrum you have dapr - a K8s sidecar that facilitates communication and state management, but does not reduce the churn of non-functional code you have to write in your app. You can put D-ASYNC and dapr together - they are not mutually exclusive ;) And I think you are right, it's hard to figure if a tech can solve your problems without trying them.
D-ASYNC certainly seems an interesting tool! I've got a new GitHub repo I'm using to have a play around with some of the Dapr functionality (I'll probably implement the Product/Order validation from this post as a really rudimentary example.
gRPC is certainly a something interesting. As you said, while it gives you pretty damn good reasons to use it, it certainly has it's downsides. The last project I worked on, I had to refactor existing REST APIs to gRPC. While it was fun to watch everything works like a charm, it definitely takes a toll on time. But when you get used to it, and reusability kicks in... It feels, okay. I had a rough time here trying the tech for the first time not knowing anything, but you kinda forget it 😂. And I just used the request/response model it offers. There's lot more to it. Just sharing my experience with the tech. Btw, try rsocket if you get some time. It uses websockets instead of http. I think you can add that to the article as well. 👍
Yep, think it will be the same as anything. When I first started working with Event Buses it used to take me forever to set everything up (events, handlers, subscribers etc). Now I think I could make a pretty good job of doing it with my eye closed.
gRPC just needs some practice :)
Why not use GraphQL? From graphql.com :-
"A query language for your API
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
Ask for what you need,
get exactly that
Send a GraphQL query to your API and get exactly what you need, nothing more and nothing less. GraphQL queries always return predictable results. Apps using GraphQL are fast and stable because they control the data they get, not the server."
Thanks for the reply Carl. I've heard a lot of good things about Graph QL, but didn't feel I had enough experience with the tech to include a worthwhile argument. I imagine the downsides are very similar to REST and gRPC though (explicit endpoints...)
good article. hope you can write about event bus more :)
I have another post lined up talking about exactly that :-) keep your eyes peeled