But as it often turns out in reality, the efficiency is the result of trade offs. This is true in general, and in particular in the sense that it is not always possible or advisable for various reasons to find a specialist in the relevant technology stack, also that means whether you are a developer or an application architect or an IT consultant , it’s essential that you understand those tradeoffs at hand when considering a particular method for moving information between systems and the process of transforming data from one schema to another, which is actually the essence of IT architecture, also known as data exchange. Data exchange has been a critical aspect of enterprise architecture since the dawn of digital computing. While it’s true that a lot of data exchange goes on within the internals of a company’s mainframe, at some point, that information has to be shared with another computer. And for that purpose, considering reliability, efficiency, security and so on, network communication protocols and data formats should become standardized. And at some point, they were standardized by the industry. Early on, people started with XML, which is still used. However, for the most part, text-based JSON and a few binary formats such as Protocol Buffers and Thrift have become the main formats of data exchange.
This standardization of data formats has led to the proliferation of architectural designs that position APIs as the core of application architecture. Put simply, the trend is to have clients interacting with an API layer representing the application on the server-side.
The advantage of taking an API-based approach to application architecture design is that it allows a wide variety of physical client devices and application types to interact with the given application. The same API can be used not only for PC-based computing but also for mobile phones and IoT devices. Communication is not limited to interactions between humans and applications. With the rise of AI and ML, service-to-service interaction facilitated by APIs will become the Internet’s principal activity.
However, while network communication and data structures have become more conventional over time, there is still variety among API approaches . There is no „one-size-fits-all.“ Instead, there are many API formats, with the most popular being REST, GraphQL, and gRPC. Thus a reasonable question to ask is, as a Developer or an Architect, how do I pick the best API format to meet the need at hand? The answer is that it’s a matter of understanding the benefits and limitations of the given format.
While the purpose of this article is not to highlight the advantages and disadvantages of the most popular API formats: REST, GraphQL, and gRPC, but rather to provide a description and some guidance on how to effectively use one of them, namely gRPC, a brief overview of the others should be provided.
REST
REST is an acronym for Representational State Transfer. REST is an architectural style devised by Roy Fielding in his 2000 Ph.D. thesis. The basic premise is that developers use the standard HTTP methods, GET, POST, PUT and DELETE, to query and mutate resources represented by URIs on the Internet.
You can think of a resource as a pretty big dataset that describes a collection of entities of the type. REST is neutral in terms of the format used to structure the response data from a resource. Using text-based data formats has become the convention. JSON is the most popular data format, although you can use others, such as XML, CSV, and even RSS.
HTTP/1.1 is the protocol used for a REST data exchange. Thus, the stateless request-response mechanism is intrinsic to the style. REST is a self-describing format. This means that you can figure out the fields and values in the response just by looking at the response results.
GraphQL
GraphQL is a technology that was developed by Facebook but is now open-source. GraphQL is an inherently language neutral specification. There are many implementations of the specification in a variety of programming languages. There are implementations in Go, .NET/C#, Node.js, and Python, to name a few.
The underlying mechanism for executing queries and mutations is the HTTP POST verb. This means that a GraphQL client written in Go can communicate with a GraphQL server written in Node.JS. Or, you can execute a query or mutation from the curl command
GraphQL is intended to represent data in a graph. Instead of the columns and rows found in a relational database or the collection of structured documents found in a document-centric database such as MongoDB, a graph database is a collection of nodes and edges.
A graph is defined according to a schema language that is particular to GraphQL. Developers use the schema language to define the types as well as the query and mutation operations that will be published by the GraphQL API. Once types, queries, and mutations are defined in the schema, the developer implements the schema using the language framework of choice.
GraphQL’s very flexible in defining the structure of the data that’s returned when making a query against the API. Unlike REST, in which the caller has no control over the structure of the returned dataset, GraphQL allows you to define the structure of the returned data explicitly in the query itself. Being able to define only the data you want, as you want it, saves the labor, memory, and CPU consumption that goes with parsing and filtering out useless data from enormous result sets.
Query and mutation data exchange under GraphQL is synchronous due to the request-response pattern inherent in the HTTP/1.1 protocol. However, GraphQL allows users to receive messages asynchronously when a specific event is raised on the server-side.
The GraphQL hybrid model provides a great deal of flexibility. Combining synchronous and asynchronous capabilities into a single API adds a new dimension to backend capabilities. GraphQL is powerful, but it’s not perfect. Synchronous and asynchronous activities are distinct. In fact, under the hood, there are actually two servers in play in a typical GraphQL API.
Is this difference too much of a problem? Not really. But there is another technology that combines both synchronicity and asynchronicity seamlessly into a programming framework. That technology is gRPC, which is described in the rest of the article.
gRPC
As I’ve already mentioned before, information has to be shared somehow, and nowadays, communication over networks is a backbone of all of our modern technology and gRPC is one of the high-level frameworks that we can use to achieve efficient reception and transmission of data.
Google Remote Procedure Call (gRPC) is a high-performance, open-source framework for implementing APIs via HTTP/2. It’s designed to make it easier for developers to build distributed applications, especially when code might be running on different machines.
gRPC was initially developed by Google as technology for implementing Remote Procedure Calls (RPCs). Later became open-source , under the Cloud Native Computing Foundation.
Socket and HTTP programming both use a message-passing paradigm. A client sends a message to a server, which usually sends a message back. Both sides are responsible for creating messages in a format understood by both sides, and in reading the data out of those messages. However, most standalone applications do not use message passing techniques much. Generally, the preferred mechanism is that of the function (or method or procedure) call. In this style, a program will call a function with a list of parameters, and on completion of the function call, will have a set of return values. These values may be the function value, or if addresses have been passed as parameters, then the contents of those addresses might have been changed.
The Remote Procedure Call is an attempt to bring this style of programming into the network world. RPCs allow you to write code as though it will run on a local computer, even though you might actually call a service running on a different machine. Thus, a client will make what looks to it like a normal procedure call. The client side will package this into a network message and transfer it to the server. The server will unpack this and turn it back into a procedure call on the server side. The results of this call will be packaged up for return to the client.
What are the benefits of gRPC?
By adopting newer technologies, gRPC updates the older RPC method to make it interoperable and more efficient. Today, this is an appealing choice when developing APIs for microservices architectures.
Some of the advantages of gRPC include:
- Performance – gRPC uses HTTP/2 as its transport protocol and Protocol Buffers by default, which serializes data into a binary format, which in turn reduces data transfer size and processing time—especially in comparison to text-based formats like JSON. So this can increase performance beyond REST and JSON communication.
- Strong typing: Protobuf enforces strong typing, which provides a well-defined and validated structure for data. This reduces the likelihood of errors and mismatches when data is exchanged between services.
- Extensibility: With gRPC, you can add new fields or methods to your services and messages without breaking existing client code. This enables teams to evolve and extend APIs without forcing clients to update immediately, which is especially important in distributed systems.
- Streaming – gRPC supports data streaming for event-driven architectures, such as server-side streaming, client-side streaming, and bidirectional streaming for sending client requests and server responses simultaneously.
- Interoperability – gRPC allows developers to automatically generate client and server code from .proto files, in multiple programming languages, which facilitates interoperability across diverse technology stacks, reduces manual coding effort and helps ensure consistency and accuracy. It supports a wide variety of programming languages, including C++, Java, Python, PHP, Go, Ruby, C#, Node.js, and more.
- Security and resilience – gRPC provides pluggable authentication, tracing, logging, monitoring, load balancing, and health-checks with interceptors and middleware. This helps ensure that gRPC services remain clean and focused on business logic—and that essential functionalities are introduced consistently across the entire service infrastructure, which helps to improve security and resiliency.
- Cloud native – gRPC works well with container-based deployments and is compatible with modern cloud-based technologies like Kubernetes and Docker.
Overall, gRPC offers a high-performance, flexible framework that is ideal for inter-service communications in highly distributed microservices architectures.
What are some challenges of working with gRPC?
While gRPC offers numerous advantages, it also poses some unique challenges. These challenges include:
- The complexity of Protobuf: Protobuf is very efficient and powerful, but defining message structures and service contracts with .proto files can be more challenging than with text-based formats like JSON.
- A steep learning curve: It can take time for developers of all skill levels to understand the intricacies of Protobuf, HTTP/2, strong typing, and code generation.
- Difficult debugging workflows: Binary serialization is not human-readable, which can make debugging and manual inspection more challenging when compared to JSON or XML.
What are the primary use cases of gRPC?
gRPC is a versatile framework that is well-suited for situations in which efficient, cross-platform communication, real-time data exchange, and high-performance networking are essential. Its primary use cases include:
- Microservice architectures: gRPC is one of the best options for communication in a microservices architecture. In microservice-based architectures, individual services may be developed in distinct programming languages to suit their specific needs. Additionally, numerous tasks may need to be carried out simultaneously, and services can face varying workloads. gRPC’s language-agnostic approach, together with its support for concurrent requests, makes it a good fit for these scenarios. One of the most common gRPC-based microservices architectures is to put an API gateway in front of the microservices and then handle all internal communications over gRPC. The API gateway handles incoming requests coming from HTTP/1.1 and proxies them to the microservices as gRPC requests over HTTP/2.
- Streaming applications: gRPC’s support for multiple streaming patterns enables services to share and process data as soon as it becomes available—without the overhead of repeatedly establishing new connections. This makes it a good fit for real-time chat and video applications, online gaming applications, financial trading platforms, and live data feeds.
- IoT systems: Internet-of-Things (IoT) systems connect an enormous number of devices that continuously exchange data. gRPC’s low latency, together with its support for real-time data ingestion, processing, and analysis, make it a good fit for these environments.
But simply put, it can be assumed that the benefits of gRPC largely stem from the use of two technologies:
- Protocol Buffers for structuring messages
- HTTP/2 as the transport layer
Protocol Buffers for Structuring Messages
Protocol Buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.
It’s like JSON, except it’s smaller and faster, and it generates native language bindings. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. Protocol buffers are a combination of the definition language (created in .proto files), the code that the proto compiler generates to interface with data, language-specific runtime libraries, the serialization format for data that is written to a file (or sent across a network connection), and the serialized data.
HTTP/2 as the Transport Layer
Traditionally, REST APIs used HTTP/1.1 as the transport layer. While REST APIs can also be delivered over HTTP/2, gRPC’s exclusive use of HTTP/2 introduces some key advantages. One of these advantages is the ability to send communication using binary. Additionally, HTTP/2 supports the ability to process multiple parallel requests instead of handling one request at a time. Communication is also bidirectional, which means a single connection can send both requests and responses at the same time. The use of this features of HTTP/2 as the underlying transport protocol, made possible following communication patterns: four method types in gRPC—unary, server streaming, client streaming, and bidirectional streaming.
So what is actually gRPC doing and how?
gRPC is described as “Protobuf over HTTP/2.” This means that gRPC will generate all the communication code wrapping the gRPC framework and stand on Protobuf ’s shoulders to serialize and deserialize data. To know which API endpoints are available on the client and server, gRPC will look at the services defined in our .proto files, and from that, it will learn the basic information needed to generate some metadata and the functions needed.
Since I am a Go developer, I’ll talk about Go. I want to show you what kind of code is generated to get a sense of how gRPC works but also to know how to debug and where to look for function signatures.
Setup
First you need to make sure you have protoc installed. For example you can download a zip file from the Releases page of the Protobuf GitHub repository(https://github.com/protocolbuffers/protobuf/releases); uncompress it and follow the readme.txt instructions, or you can use any of other installation options. Then as we’ re using Go, you are going to need two protoc plugins: protoc-gen-go and protoc-gen-go-grpc. The former generates Protobuf code, and the latter generates gRPC code. To add them, you can simply run the following commands:
Also, make sure that your GOPATH environment variable is in your PATH environment variable. Normally, this is already done for you on the installation of Golang, but if you get any error related to not finding protoc-gen-go or protoc-gen-go-grpc, you will need to do it manually. To get the GOPATH environment variable, you run the following command:
After that, depending on your OS, you can go through the steps of adding the output to your PATH environment variable.
What are Our Example Projects
Our example is a set of two repositories host the source code for a client(s) designed to generate a specified number of requests (N), and an API server capable of handling these requests, as specified by the client’s operations, encoded using protocol buffers. I intentionally divided client and service source-code between two different repositories, to give a sense of interaction two real distributed systems. You can find the complete client example here , and the complete server example here , in the respective GitHub repositories.
In this article I won’t talk about setting up a gRPC project structure too much. Instead we’ll concentrate on features that gRPC presents to us.
Creating go.mod file
Create some project directory. In my case logistics_engine_api, for the server project and clients_logistics_engine_api, for the client(s) project. Then start your module using the go mod init command to create a go.mod file, in the project root directory.
Run the go mod init command, giving it the path of the module your code will be in. Here, use github.com/myuser/myrepo for the module path – in production code, this would be the URL from which your module can be downloaded.
The go mod init command creates a go.mod file that identifies your code as a module that might be used from other code. The file you just created includes only the name of your module and the Go version your code supports. But as you add dependencies – meaning packages from other modules – the go.mod file will list the specific module versions to use. This keeps builds reproducible and gives you direct control over which module versions to use.
We will see two common ways of setting up a gRPC project. We will use protoc and Buf. Buf is an abstraction over protoc that lets us run protoc commands more easily. On top of that, it provides features such as linting and detecting breaking changes. You can download Buf from here: https://docs.buf.build/installation.
Defining Our Protocol Format, i.e. Creating a .proto File Definition
To create our client and server applications, we’ll need to start with a .proto file. The definitions in a .proto file are simple: you add a message for each data structure you want to serialize, then specify a name and a type for each field in the message. In our example, the .proto file that defines the messages is https://github.com/innoq/logistics_engine_api/blob/main/api/v1/logistics.proto
The .proto file starts with a package declaration, which helps to prevent naming conflicts between different projects.
The go_package option defines the import path of the package which will contain all the generated code for this file. The Go package name will be the last path component of the import path. For example, our example will use a package name of “logistics_v1”.
Next, you have your message definitions. A message is just an aggregate containing a set of typed fields. Many standard simple data types are available as field types, including bool, int32, float, double, and string. You can also add further structure to your messages by using other message types as field types.
In the above example, the MoveUnitRequest message contains messages of Location type, for example. You can even define message types nested inside other messages. The " = 1„, " = 2“ markers on each element identify the unique “tag” that field uses in the binary encoding. Tag numbers 1–15 require one less byte to encode than higher numbers, so as an optimization you can decide to use those tags for the commonly used or repeated elements, leaving tags 16 and higher for less-commonly used optional elements. Each element in a repeated field requires re-encoding the tag number, so repeated fields are particularly good candidates for this optimization. If a field value isn’t set, a default value is used: zero for numeric types, the empty string for strings, false for bools. For embedded messages, the default value is always the “default instance” or “prototype” of the message, which has none of its fields set. Calling the accessor to get the value of a field which has not been explicitly set always returns that field’s default value. If a field is repeated, the field may be repeated any number of times (including zero). The order of the repeated values will be preserved in the protocol buffer. Think of repeated fields as dynamically sized arrays. You’ll find a complete guide to writing .proto files – including all the possible field types – in the Protocol Buffer Language Guide.
Last but not least, construct that is important to see and that we are going to work with, is the service one. In Protobuf, a service is a collection of RPC endpoints that contains two major Protobuf parts. The first part is the input of the RPC, and the second is the output. In other words you define rpc methods inside your service definition, specifying their request and response types, using protocol buffers.
Here, for example, we use a message MoveUnitRequest representing a request, and another one DefaultResponse, representing the response and we use these as input and output of our MoveUnit RPC call. It is important to understand is that Protobuf defines the services but does not generate the code for them. Only gRPC will. Protobuf ’s services are here to describe a contract, and it is the job of an RPC framework to fulfill that contract on the client and server part. Notice that it’s written an RPC framework and not simply gRPC. Any RPC framework could read the information provided by Protobuf ’s services and generate code out of it. The goal of Protobuf here is to be independent of any language and framework. What the application does with the serialized data is not important to Protobuf. Finally, these services are the pillars of gRPC. As we are going to see later, we will use them to make requests, and we are going to implement them on the server side to return responses. Using the defined services on the client side will let us feel like we are directly calling a function on the server. If we talk about LogisticsEngineAPI, for example, we can make a call to MoveUnit by having the following code:
Here, apiClientGRPC is an instance of a gRPC client, req is an instance of MoveUnitRequest, and res is an instance of DefaultResponse. In this case, it feels a little bit like we are calling MoveUnit, which is implemented on the server side. However, this is the doing of gRPC. It will hide all the complex process of serializing and deserializing objects and sending those to the client and server.
gRPC lets you define four kinds of service method, but we use only one in our LogisticsEngineAPI service:
- A simple RPC where the client sends a request to the server using the stub and waits for a response to come back, just like a normal function call.(we use this one)
- A server-side streaming RPC where the client sends a request to the server and gets a stream to read a sequence of messages back. The client reads from the returned stream until there are no more messages. You specify a server-side streaming method by placing the stream keyword before the response type.
- A client-side streaming RPC where the client writes a sequence of messages and sends them to the server, again using a provided stream. Once the client has finished writing the messages, it waits for the server to read them all and return its response. You specify a client-side streaming method by placing the stream keyword before the request type.
- A bidirectional streaming RPC where both sides send a sequence of messages using a read-write stream. The two streams operate independently, so clients and servers can read and write in whatever order they like: for example, the server could wait to receive all the client messages before writing its responses, or it could alternately read a message then write a message, or some other combination of reads and writes. The order of messages in each stream is preserved. You specify this type of method by placing the stream keyword before both the request and the response.
Compiling Our Protocol Buffers, i.e. Generating client and server code
Next we need to generate the gRPC client and server interfaces from our .proto service definition. For generating the stubs, we have two alternatives: protoc and buf. Protoc is the more classic generation experience that is used widely in the industry, but it has a pretty steep learning curve. buf is a newer tool that is built with user experience and speed in mind. It also offers linting and breaking change detection, something protoc doesn’t offer. We use both here.
Generating stubs using protoc
Here’s an example of what a protoc command might look like to generate Go stubs, assuming that you’re at the root of your repository and you have your proto files in a directory called api/v1/:
We use the go and go-grpc plugins to generate Go types and gRPC service definitions. We’re outputting the generated files relative to the api/v1 folder, and we’re using the paths=source_relative option, which means that the generated files will appear in internal/generated/logistics/api/v1 .
Generating stubs using buf
Buf is a tool that provides various protobuf utilities such as linting, breaking change detection and generation. Please find installation instructions on https://docs.buf.build/installation/. It is configured through a buf.yaml file that should be checked in to the root of your Protobuf file hierarchy. Buf will automatically read this file if present. Configuration can also be provided via the command-line flag –config, which accepts a path to a .json or .yaml file, or direct JSON or YAML data. As opposed to protoc, where all .proto files are manually specified on the command-line, buf operates by recursively discovering all .proto files under configuration and building them. The following is an example of a valid configuration, and you would put it in the root of your Protobuf file hierarchy, e.g. in api/v1/buf.yaml relative to the root of your repository.
To generate type and gRPC stubs for Go, create the file buf.gen.yaml and place it to the root of your repository:
We use the go and go-grpc plugins to generate Go types and gRPC service definitions. We’re outputting the generated files relative to the api/v1 folder, and we’re using the paths=source_relative option, which means that the generated files will appear in the internal/generated/logistics/api/v1 directory. Then run:
This will have generated a *.pb.go and a *_grpc.pb.go file for each protobuf package in our proto file hierarchy.
Project structure
Overall project structure for server will be looking something like this:
The client project structure is roughly the same.
The server
Let us look at what was generated on the server side first. We are going to start from the bottom of the file with the service descriptor. In Protobuf and gRPC context, a descriptor is a meta object that represents Protobuf code. This means that, in our case, we have a Go object representing a service or other concepts.
For our LogisticsEngineAPI service, we have the following descriptor:
This means that we have a service called LogisticsEngineAPI that is linked to a type called LogisticsEngineAPIServer, and this service has a method called MoveUnit that should be handled by a function called _LogisticsEngineAPI_MoveUnit_Handler.
You should find the handler(s) above the service descriptor. This looks like the following:
This handler is responsible for creating a new object of type MoveUnitRequest and populating it before passing it to the MoveUnit function in an object of type LogisticsEngineAPIServer. Note here that we are going to assume that we always have an interceptor equal to nil because this is a more advanced feature, but later, we are going to see an example of how to set one up and use it.
Finally, we see the LogisticsEngineAPIServer type being mentioned. Here is what it looks like:
This is a type that contains the function signatures of our RPC endpoints and as it’s go doc(inline documentation i.e. comments) mentions, that all implementations of it should embed UnimplementedLogisticsEngineAPIServer type, which follows next, for forward compatibility. With that, we can understand that this UnimplementedLogisticsEngineAPIServer type must be embedded somewhere, and this somewhere is in a type that is defined further, in https://github.com/innoq/logistics_engine_api/blob/main/internal/logistics/grpcserver/server.go where we are writing our API endpoints. We have the following code:
This is called type embedding, and this is the way Go goes about adding properties and methods from another type. We add the methods’ definitions from UnimplementedLogisticsEngineAPIServer to server type. This lets us, for example, have the default implementations that return method MoveUnit not implemented generated for us. This means that if a server without a full implementation receives a call on one of its unimplemented API endpoints, it will return an error but not crash because of the non-existent endpoint. But as far as we already defined our own server type that embeds the UnimplementedLogisticsEngineAPIServer type, and we have overridden the MoveUnit function, and the others with the implementation, any call to MoveUnit, and to the others as well, will then be redirected to the implementation and not to the default generated code.
As already stated before, logic for running gRPC application components, The gRPC Server/Handlers implementation, and this everything fired up together is gathered in github.com/innoq/logistics_engine_api/blob/main/internal/grpcapp/app.go ,github.com/innoq/logistics_engine_api/blob/main/internal/logistics/grpcserver/server.go ,github.com/innoq/logistics_engine_api/blob/main/internal/app/app.go respectively. But in general, to listen for the incoming connection, we use the net.Listen provided in Go. The listener have to be closed at the end of the program, for which we use the mechanism of graceful shutdown, provided by in-built function from gRPC. To create gRPC server, we need to define some connection options, with array of grpc.ServerOption objects. Then we register our endpoints with Register(..) function in github.com/innoq/logistics_engine_api/blob/main/internal/grpcapp/app.go , and run it, calling Serve on the grpc.Server that we created.
The client
The generated code for the client is even simpler than the server code. We have an interface called LogisticsEngineAPIClient that contains all the API endpoints:
Notice one important thing in here. We have an endpoint route called /logistics.api.v1.LogisticsEngineAPI/MoveUnit. If you take a look back at the LogisticsEngineAPI_ServiceDesc variable described earlier, you will find out that this route is a concatenation of the ServiceName and MethodName properties. This will let the server know how to route that request to the _LogisticsEngineAPI_MoveUnit_Handler. That is it. We can see that gRPC is handling all the boilerplate code to call an endpoint. We just need to create an object following the LogisticsEngineAPIClient interface by calling NewLogisticsEngineAPIClient, and then with that object, we can call the MoveUnit member.
In https://github.com/innoq/clients_logistics_engine_api/blob/main/internal/grpc_client/grpc_client.go we call the grpc.DialContext function and pass the connection options to it. There we make an insecure connection to the server with the insecure.NewCredentials() function. Later in https://github.com/innoq/clients_logistics_engine_api/blob/main/internal/app/app.go we initialize the connection and so on.
A bit more on out-of-the-Box Features
Handling errors
Errors in gRPC are returned with the help of a wrapper struct called Status. This struct can be built in multiple ways but the most interesting ones are the following:
They both take a message for the error and an error code. The status codes are predefined codes that are consistent across the different implementations of gRPC. It is similar to the HTTP codes such as 404 and 500, but the main difference is that they have more descriptive names and that they are much fewer codes than in HTTP (16 in total).
External logic with interceptors
Interceptors are pieces of code that are called before or after handling a request (server side) or sending a request (client side). The goal is generally to enrich the request with some extra information, but it can also be used to log requests or deny certain requests if they do not meet requirements, for example. It’s like a middleware, but in the context of gRPC. Here we use some, logging and tracing API calls:
So to see everything in action, you can test it by running our server first:
or just
Then in the other terminal, we run our client, in project root:
or just
The server should wait infinitely, emitting logs on calls, and the client should be returning without any error on the terminal. Then you want to hit the localhost:50051 LogisticsEngineAPI/MetricsReport, with any gRPC client to see the calculations result.
Undoubtedly, this framework has many more interesting and useful features. But this is a matter of separate, more in-depth research on various cases. So this time it’s all guys. Enjoy your gRPC journey!