1. Communication in a Microservices World
In a modern microservices architecture, we almost never deal with a single standalone application. Instead, the system is composed of many small services, each responsible for a specific piece of functionality, and these services continuously talk to each other in order to handle a single user request. One service might handle authentication, another might handle user profiles, and another might handle orders, but internally they must constantly exchange data by sending requests and receiving responses.
This inter-service communication is not an occasional activity. In a real production system, it happens all the time, often at very high scale. Therefore, the way in which data is represented, sent over the network, and reconstructed on the other side becomes a core performance and reliability concern, not just an implementation detail.
2. How JSON Is Typically Used Today
In most systems today, JSON is the default format for inter-service communication, and there are very good historical reasons for this choice.
- JSON is text-based and human-readable, which means developers can easily inspect requests and responses in logs, debugging tools, or network traces. This greatly simplifies development and troubleshooting, especially in the early stages of a project.
- JSON has excellent tooling and ecosystem support, especially in web environments, and it is supported natively by browsers and almost every programming language.
Let us imagine a simple example where Service A wants to send a Person object, containing a name and an age, to Service B. In Java, Service A will create a Person object and then use a library such as Jackson to serialize that object into JSON text. This JSON text is then sent over the network. Service B receives the JSON text and uses the same or a similar library to deserialize it back into a Person object. After processing, the response goes through the same cycle again: it is serialized to JSON, sent back, and deserialized on the original side.
If we observe this flow carefully, we realize that for every single request and every single response, we pay the cost of serialization and deserialization, even if the business logic itself is very simple.
3. Why JSON Becomes a Problem at Scale
When a system handles only a small number of requests, the cost of serialization and deserialization is usually not noticeable. However, in a high-throughput system that processes hundreds of thousands or even millions of requests per minute, this cost becomes very significant.
- Serialization and deserialization are CPU-intensive operations, and they are executed for every request and every response. When multiplied by a huge number of calls, this becomes a major consumer of CPU time.
- JSON is a verbose text format, which means the serialized data is relatively large. Field names are repeated again and again, and numbers are represented as text, which increases the payload size.
- Larger payloads mean more network bandwidth usage, and they also mean more time spent parsing and generating these payloads on both sides of the communication.
So although JSON is excellent for humans, it is not optimized for machines that need to exchange data as efficiently as possible. In other words, JSON is human-friendly, but not machine-friendly.
4. What Is Protocol Buffers and Why It Exists
Protocol Buffers, often called Protobuf, is a technology developed by Google to solve exactly this class of problems. Google has been using it internally for many years in some of the largest distributed systems in the world.
Conceptually, Protocol Buffers is:
- A language-neutral and platform-neutral way of defining structured data, which means the same data definition can be used in Java, C++, Python, Go, and many other languages.
- A binary serialization format, which means the data is represented in a compact, machine-friendly form rather than as human-readable text.
Because Protobuf uses a binary format:
- The serialized size is much smaller than JSON, which directly reduces network bandwidth usage.
- Serialization and deserialization are much faster, because the format is designed to be easy for machines to process.
- Overall latency and CPU usage are significantly reduced, which makes a big difference in high-throughput systems.
So while Protobuf is not meant to be read by humans, it is extremely efficient for machine-to-machine communication, which is exactly what happens inside a microservices architecture.
5. Type Safety: A Hidden but Crucial Advantage
Another very important advantage of Protocol Buffers is that it enforces a strict schema and therefore provides strong type safety.
- With JSON, the format itself does not enforce any schema. The sender can send almost anything, and the receiver will only find out at runtime whether the data matches what it expected.
- With Protobuf, both the client and the server generate code from the same schema definition. This means many kinds of errors are caught early, often at compile time, long before the system is deployed to production.
This shared schema becomes a formal contract between services, not just documentation.
6. Defining Data Using a .proto File
Instead of starting with Java classes, in Protobuf we start by defining our data structures in a .proto file. For example, if we want to represent a person with a name and an age, we might write:
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
}
- The
messagekeyword is conceptually similar to a class in Java. It defines a structured type. - The types
stringandint32are Protobuf types, not Java types. Each target language will map them to its own native types. - The numbers
1and2are called field numbers, and they are part of the binary encoding.
7. Why Field Numbers Are So Important
The field numbers in Protobuf are not optional and not cosmetic.
- Protobuf does not send field names over the wire. It only sends field numbers and values.
- This is one of the main reasons why Protobuf messages are so compact and efficient.
- Because these numbers are part of the wire format, they must never be changed once a message is in use. You can add new fields with new numbers, but you should never reuse or modify existing numbers.
This rule is what allows Protobuf to support backward and forward compatibility in evolving systems.
8. Code Generation: How Protobuf Is Actually Used
Once the .proto file is written, we do not manually write serialization or deserialization code.
- Protobuf provides code generators for many languages such as Java, C++, Python, Go, and others.
- These generators read the
.protofile and produce classes that represent the messages. - These generated classes already contain all the logic required to serialize objects into binary form and deserialize them back.
So instead of writing DTOs and mapping code by hand, we define the schema once and let the tooling generate everything for us.
9. Where This Fits in Real Systems
At this point, the main idea should be clear:
- JSON is excellent for human-facing APIs and debugging.
- Protobuf is excellent for high-performance, internal, service-to-service communication.
- In real systems, it is very common to use both, each where it makes the most sense.
