gRPC

Understanding The Problem
Before gRPC, let's understand why it exists.
Imagine Your Current System:
Auth Service
Logger Service
Notification ServiceSuppose:
User Login
↓
Auth Service
↓
Logger Service
↓
Notification ServiceNow Auth Service needs to ask Notification Service:
"Send Login Alert"How?
Option 1: Direct Function Call
Inside same application:
sendNotification(user);Easy.
Because:
Same Process
Same Memory
Same RuntimeOption 2: Different Servers
Now Notification Service lives elsewhere:
Auth Service -> 10.0.0.1
Notification Service -> 10.0.0.2Function call no longer works.
You need:
Network CommunicationTraditional Approach (REST)
Auth service sends:
POST /send-notification
{
"userId": 100,
"message": "Login detected"
}Notification Service responds:
{
"success": true
}What Happens Internally?
Step 1: Create JSON
{
"userId": 100
}Step 2: Convert JSON to string
"{\"userId\":100}"Step 3: Send over network
Step 4: Receiver parses JSON
{
"userId": 100
}Step 5: Create response JSON
Again serialize.
Again deserialize.
Problem 1: Large Payloads
Look:
{
"userId": 100,
"name": "TheJat",
"email": "TheJat@thejat.in"
}Every field name travels over network.
userId
name
emailAgain and again
For millions of requests:
Huge bandwidth wasteProblem 2: Parsing Cost
JSON is text.
Server must:
String
↓
Parse
↓
ObjectFor every request.
CPU cost increases.
Problem 3: Weak Contracts
REST API docs may say:
{
"userId": 100
}But client sends:
{
"userid": 100
}Notice:
userId
useridBug.
Detected only at runtime.
Problem 4: SDK Generation
For every service:
Frontend SDK
Java SDK
Go SDK
Node SDK
Python SDKDevelopers often write these manually.
Maintenance nightmare.
Google's Challenge
Google had:
Thousands of services
Millions of requests/secREST became expensive internally.
They needed:
Fast
Small Payloads
Strong Contracts
Auto-generated SDKs
StreamingResult:
gRPCCore Idea of gRPC
Instead of sending:
{
"userId":100,
"name":"Manish"
}Send:
000101010101001010Smaller.
Faster.
Machine-friendly.
But How Does Client Know Structure?
Answer:
.proto fileThink of it as:
API Blueprint
+
Contract
+
Documentation
+
Code GeneratorExample:
message User {
int32 id = 1;
string name = 2;
}This says:
Field 1 = id
Field 2 = nameInstead of sending:
id
nameevery request, protobuf sends:
1
2Much smaller.
First Mental Model
Think of gRPC as:
REST = Human readable
gRPC = Machine optimizedHow gRPC Actually Works Internally
Most developers use gRPC like this:
const user = await userClient.getUser({
id: 101
});Looks like a normal function call.
But behind the scenes, a lot happens.
The Magic Trick
When you write:
const user = await userClient.getUser({
id: 101
});It feels like:
Your Code
↓
getUser()
↓
Returns UserReality:
Client Service
↓
Serialize Request
↓
HTTP/2
↓
Network
↓
Server
↓
Execute Method
↓
Serialize Response
↓
Network
↓
ClientgRPC hides all of this.
RPC Architecture:
A gRPC system has four main components:
Application Code
↓
Client Stub
↓
gRPC Runtime
↓
Network
↓
gRPC Runtime
↓
Server Stub
↓
Application CodeComponent 1: Application Code
Suppose you are building your Payment Service.
You write:
const user = await userClient.getUser({
id: 101
});You don't care about:
- TCP
- HTTP/2
- Serialization
- Retries
- Packet framing
You just call a method.
Component 2: Client Stub
The Client Stub is generated automatically from the .proto file.
Example:
service UserService {
rpc GetUser(GetUserRequest)
returns (UserResponse);
}gRPC generates something similar to:
class UserServiceClient {
async getUser(req) {}
}This generated code is called a:
StubThink of a stub as:
Local ProxyIt looks like the real service.
But it isn't.
Visual
Your Code
↓
Client Stub
↓
Actual Network CallComponent 3: Channel
Every gRPC client maintains a Channel.
Client
↓
Channel
↓
ServerA channel is:
Persistent ConnectionUsually one HTTP/2 connection.
Why Important?
REST often creates many connections.
Request 1
Connection A
Request 2
Connection B
Request 3
Connection CgRPC:
Single HTTP/2 ConnectionConnection
├─ Stream 1
├─ Stream 2
├─ Stream 3
└─ Stream 4This dramatically improves performance.
Component 4: Server Stub
Server side also has generated code.
Example:
service UserService {
rpc GetUser(GetUserRequest)
returns (UserResponse);
}Generated interface:
class UserService {
getUser() {}
}You implement it:
class UserService {
async getUser(request) {
return findUser(request.id);
}
}This is the actual business logic.
Complete Request Journey
Let's trace one request.
Step 1: Application Code
await client.getUser({
id: 101
}0;Step 2: Client Stub Receives Request
GetUserRequest{
"id":101
}Step 3: Serialization
Convert into protobuf binary.
Not:
{
"id":101
}Instead:
000010101011010Much smaller.
Step 4: gRPC Frame Creation
gRPC adds metadata.
Message Length
Compression Info
Flags
PayloadResult:
gRPC FrameStep 5: HTTP/2 Stream
Place into a stream.
Connection
├─ Stream 15
├─ Stream 16
└─ Stream 17You request may be Stream 16.
Step 6: Network Transfer
Request Travels:
Payment Service
↓
Switch
↓
Router
↓
Kubernetes Network
↓
User ServiceStep 7: Server Receives Request
Server gRPC runtime reads:
Stream 16Extracts:
GetUserRequestStep 8: Deserialilzation
Binary:
000010101011010becomes:
{
"id":101
}(or language-specific object)
Step 9: Method Dispatch
Server checks:
Requested Method?Header say:
/UserService/GetUserSo gRPC calls:
getUser()Step 10: Business Logic Executes
async getUser(req) {
return db.user.findById(req.id);
}Database queried.
User found.
Step 11: Response Created
{
"id":101,
"name":"TheJat"
}Step 12: Serialize Again
Response converted to protobuf binary.
10101010110101001010Step 13: Send Throudh Same HTTP/2 Stream
Steam 16returns response.
Step 14: Client Receives
gRPC runtime decodes.
Stub converts it back.
Step 15: Your Code Gets Result
const user = await client.getUser({
id:101
});
console.log(user.name);Output:
TheJatLooks like a local function.
Actually crossed the network.
How Does Server Know Which Method to Call?
Every gRPC request contains:
/service/methodExample:
/UserService/GetUserSimilar to REST:
GET /users/101What Happens If Server Is Down?
Client sends:
GetUser()Server unreachable.
gRPC returns:
UNAVAILABLEError code.
Common gRPC Status Codes
| Code | Meaning |
|---|---|
| OK | Success |
| CANCELLED | Client cancelled |
| INVALID_ARGUMENT | Bad request |
| NOT_FOUND | Resource missing |
| UNAUTHENTICATED | No auth |
| PERMISSION_DENIED | Access denied |
| DEADLINE_EXCEEDED | Timeout |
| UNAVAILABLE | Service down |
| INTERNAL | Server error |
These are standardized across languages.
Protocol Buffers
If gRPC is the car,
gRPC = Carthen Protocol Buffers (Protobuf) is the engine.
Protocol Buffers
=
Data Definition Language
+
Serialization Format
+
Code GeneratorWithout Protobuf, gRPC would not exist.
What Problem Does Protobuf Solve?
Suppose your Payment Service sends:
{
"paymentId": 1001,
"amount": 500,
"currency": "INR"
}This is human-readable.
But machine don't care about readability.
They care about:
Size
Speed
CompatibilityGoogle wanted:
Smaller payloads
Faster serialization
Cross-language support
VersioningResult:
Protocol BuffersWhat Is a .proto File?
Think of .proto as:
Database Schema
+
API Contract
+
Documentation
+
SDK GeneratorExample:
syntax = "proto3";
message User {
int32 id = 1;
string name = 2;
}This defines:
A User ObjectFirst Rule
Every proto file starts with:
syntax = "proto3";Example:
syntax = "proto3";
message User {
int32 id = 1;
}Why Proto3?
Google had:
proto1
proto2
proto3Today:
proto3 = standardAlmost all modern systems use it.
Understanding Messages
A message is similar to:
interface User {}or
class User {}Example:
message User {
int32 id = 1;
string name = 2;
}Equivalent TypeScript:
interface User {
id: number;
name: string;
}The Most Important Thing: Field Numbers
Look carefully:
message User {
int32 id = 1;
string name = 2;
}People often think:
1 and 2 are indexesBut they are not.
They are:
Field tagsThese tags are what travel on the wire.
Why Not Send Field Names?
JSON sends:
{
"id": 10,
"name": "TheJat"
}Network carries:
id
nameevery time.
Protobuf sends:
1-> 10
2-> TheJatMuch smaller.
Golden Rule
Never change field numbers.
Bad:
message User {
int32 id = 1;
string name = 2;
}Later:
message User {
int32 id = 2;
string name = 1;
}Disaster.
Because old clients still think:
1 = id
2 = nameData corruption.
Real Production Rule
Once released:
id = 1must remain:
id = 1forever.
Google follows this rule religiously.
Primitive Types
Common types:
double
float
int32
int64
uint32
uint64
bool
string
bytesint32 vs int64
Use:
int32for:
1
100
100000Use:
int64for:
Database IDs
Timestamps
Large countersExample:
int64 user_id = 1;Most production systems use int64 for IDs.
Strings
message User {
string email = 1;
}Example:
thejat@thejat.inBoolean
bool is_active = 1;Possibly values:
true
falseBytes
Raw binary.
bytes image = 1;Useful for:
Files
Images
Certificates
Encryption KeysRepeated Fields (Arrays)
JSON:
{
"roles": [
"admin",
"manager"
]
}Proto:
message User {
repeated string roles = 1;
}Equivalent:
roles: string[]Nested Messages
Example:
message Address {
string city = 1;
string state = 2;
}
message User {
string name = 1;
Address address = 2;
}Result:
{
"name":"TheJat",
"address":{
"city":"KKR",
"state":"Haryana"
}
}Enums
Suppose payment status.
Bad:
string status = 1;People may send:
success
SUCCESS
done
completedInconsistent.
Use enum.
enum PaymentStatus {
PENDING = 0;
SUCCESS = 1;
FAILED = 2;
}Then:
message Payment {
Payment Status = 1;
}Why Enum Starts At 0?
Proto3 requires:
enum PaymentStatus {
PENDING = 0;
}First value must be zero.
It is the best practice.
Maps
JSON:
{
"metadata": {
"ip":"1.2.3.4",
"browser":"chrome"
}
}Proto:
map<string,string> metadata = 1;Equivalent:
Record<string,string>Optional Fields
Proto3 originally removed optional fields.
Now they are back.
Example:
message User {
optional string middle_name = 1;
}Useful when:
Value absentmust be different from
Empty stringOneof
Suppose login can happen using:
Email
OR
PhoneNever both.
Use:
message LoginRequest {
oneof identifier {
string email = 1;
string phone = 2;
}
}Valid:
{
"email": "thejat@thejat.in
}Valid:
{
"phone": "123456790"
}Invalid:
{
"email": "thejat@thejat.in"
"phone": "123456790"
}Only one field can exist.
Services
Now let's connect messages with gRPC.
service UserService {
rpc GetUser(GetUserRequest)
returns (UserResponse);
}This means:
Method: GetUser()
Input: GetUserRequest
Output: UserResponseExample Complete Proto
syntax = "proto3";
package user;
message GetUserRequest {
int64 user_id = 1;
}
message UserResponse {
int64 id = 1;
string name = 2;
string email = 3;
}
service UserService {
rpc GetUser(GetUserRequest)
returns (UserResponse);
}This single file generates:
NodeJS SDK
Java SDK
Go SDK
Pythong SDK
C# SDK
Rust SDKAutomatically.
