Topic Overview

gRPC

Learn gRPC: high-performance RPC framework using HTTP/2 and Protocol Buffers. Understand service definition, streaming, and comparison with REST.

gRPC

gRPC is a high-performance, open-source RPC (Remote Procedure Call) framework developed by Google. It uses HTTP/2 for transport and Protocol Buffers for serialization, making it efficient for microservices communication.


What is gRPC?

gRPC provides:

  • RPC framework: Call remote functions as if they were local
  • HTTP/2: Multiplexing, header compression, server push
  • Protocol Buffers: Efficient binary serialization
  • Language agnostic: Works across multiple languages
  • Streaming: Supports unary and streaming RPCs

Benefits:

  • Performance: Faster than REST (binary protocol, HTTP/2)
  • Type safety: Strong typing with Protocol Buffers
  • Code generation: Auto-generate client/server code
  • Streaming: Bidirectional streaming support

gRPC vs REST

REST (HTTP/JSON)

Client → Server: POST /api/users
  Content-Type: application/json
  Body: {"name": "John", "email": "john@example.com"}
  
Server → Client: 200 OK
  Content-Type: application/json
  Body: {"id": 1, "name": "John", "email": "john@example.com"}

Characteristics:

  • Text-based (JSON)
  • Human-readable
  • Stateless
  • Resource-oriented

gRPC (HTTP/2/Protobuf)

Client → Server: RPC call (binary)
  Method: CreateUser
  Request: (Protocol Buffer binary)
  
Server → Client: RPC response (binary)
  Response: (Protocol Buffer binary)

Characteristics:

  • Binary protocol (Protocol Buffers)
  • More efficient
  • Strong typing
  • Streaming support

Protocol Buffers

Protocol Buffers (protobuf) is a language-neutral serialization format.

Example .proto File

syntax = "proto3";

package user;

// Service definition
service UserService {
  // Unary RPC
  rpc GetUser (GetUserRequest) returns (User);
  
  // Server streaming
  rpc ListUsers (ListUsersRequest) returns (stream User);
  
  // Client streaming
  rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
  
  // Bidirectional streaming
  rpc Chat (stream Message) returns (stream Message);
}

// Message definitions
message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

message GetUserRequest {
  int32 id = 1;
}

message ListUsersRequest {
  int32 page = 1;
  int32 page_size = 2;
}

Key points:

  • Service: Defines RPC methods
  • Message: Data structures
  • Field numbers: Unique identifiers (1, 2, 3, ...)
  • Streaming: stream keyword for streaming RPCs

gRPC RPC Types

1. Unary RPC

One request, one response:

rpc GetUser (GetUserRequest) returns (User);
Client → Server: Request
Server → Client: Response

2. Server Streaming

One request, multiple responses:

rpc ListUsers (ListUsersRequest) returns (stream User);
Client → Server: Request
Server → Client: Response 1
Server → Client: Response 2
Server → Client: Response 3
...

3. Client Streaming

Multiple requests, one response:

rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
Client → Server: Request 1
Client → Server: Request 2
Client → Server: Request 3
Server → Client: Response

4. Bidirectional Streaming

Multiple requests, multiple responses:

rpc Chat (stream Message) returns (stream Message);
Client → Server: Request 1
Server → Client: Response 1
Client → Server: Request 2
Server → Client: Response 2
...

Examples

gRPC Server (Node.js)

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

// Load proto file
const packageDefinition = protoLoader.loadSync('user.proto', {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true
});

const userProto = grpc.loadPackageDefinition(packageDefinition).user;

// Implement service
const server = new grpc.Server();

server.addService(userProto.UserService.service, {
  // Unary RPC
  GetUser: (call, callback) => {
    const userId = call.request.id;
    const user = {
      id: userId,
      name: 'John Doe',
      email: 'john@example.com'
    };
    callback(null, user);
  },
  
  // Server streaming
  ListUsers: (call) => {
    const users = [
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' },
      { id: 3, name: 'Charlie', email: 'charlie@example.com' }
    ];
    
    users.forEach(user => {
      call.write(user);
    });
    call.end();
  },
  
  // Client streaming
  CreateUsers: (call, callback) => {
    const users = [];
    
    call.on('data', (request) => {
      users.push({
        id: Math.random(),
        name: request.name,
        email: request.email
      });
    });
    
    call.on('end', () => {
      callback(null, { count: users.length, users });
    });
  },
  
  // Bidirectional streaming
  Chat: (call) => {
    call.on('data', (message) => {
      // Echo message
      call.write({
        id: message.id,
        text: `Echo: ${message.text}`,
        timestamp: Date.now()
      });
    });
    
    call.on('end', () => {
      call.end();
    });
  }
});

server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
  server.start();
  console.log('gRPC server running on port 50051');
});

gRPC Client (Node.js)

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

// Load proto file
const packageDefinition = protoLoader.loadSync('user.proto', {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true
});

const userProto = grpc.loadPackageDefinition(packageDefinition).user;

// Create client
const client = new userProto.UserService(
  'localhost:50051',
  grpc.credentials.createInsecure()
);

// Unary RPC
client.GetUser({ id: 1 }, (error, user) => {
  if (error) {
    console.error('Error:', error);
    return;
  }
  console.log('User:', user);
});

// Server streaming
const call = client.ListUsers({ page: 1, page_size: 10 });
call.on('data', (user) => {
  console.log('User:', user);
});
call.on('end', () => {
  console.log('Stream ended');
});

// Client streaming
const createCall = client.CreateUsers((error, response) => {
  if (error) {
    console.error('Error:', error);
    return;
  }
  console.log('Created users:', response);
});

createCall.write({ name: 'Alice', email: 'alice@example.com' });
createCall.write({ name: 'Bob', email: 'bob@example.com' });
createCall.end();

// Bidirectional streaming
const chatCall = client.Chat();
chatCall.on('data', (message) => {
  console.log('Received:', message);
});
chatCall.write({ id: 1, text: 'Hello' });
chatCall.write({ id: 2, text: 'World' });
chatCall.end();

gRPC with Authentication

// Server with authentication
const server = new grpc.Server();

server.addService(userProto.UserService.service, {
  GetUser: (call, callback) => {
    // Verify authentication
    const metadata = call.metadata;
    const token = metadata.get('authorization')[0];
    
    if (!this.verifyToken(token)) {
      callback({
        code: grpc.status.UNAUTHENTICATED,
        message: 'Invalid token'
      });
      return;
    }
    
    // Process request
    const user = this.getUser(call.request.id);
    callback(null, user);
  }
});

// Client with authentication
const metadata = new grpc.Metadata();
metadata.add('authorization', 'Bearer token123');

const client = new userProto.UserService(
  'localhost:50051',
  grpc.credentials.createInsecure()
);

client.GetUser({ id: 1 }, metadata, (error, user) => {
  // Handle response
});

Common Pitfalls

  • Not handling errors: gRPC uses status codes, not HTTP status codes. Fix: Handle gRPC status codes (OK, NOT_FOUND, UNAUTHENTICATED, etc.)
  • Large message sizes: Default limit is 4MB. Fix: Increase message size limit or use streaming
  • Not using streaming: Using unary for large datasets. Fix: Use server streaming for large responses
  • Version compatibility: Proto changes break clients. Fix: Use versioning, backward compatibility
  • No timeout: Requests can hang indefinitely. Fix: Set deadlines/timeouts
  • Not handling connection errors: Network issues not handled. Fix: Implement retry logic, connection pooling

Interview Questions

Beginner

Q: What is gRPC and how does it differ from REST?

A:

gRPC is a high-performance RPC framework using HTTP/2 and Protocol Buffers.

Differences from REST:

AspectRESTgRPC
ProtocolHTTP/1.1HTTP/2
FormatJSON (text)Protocol Buffers (binary)
PerformanceSlowerFaster
Type safetyWeakStrong
StreamingLimitedFull support
Code generationManualAuto-generated

gRPC Benefits:

  • Performance: Binary protocol, HTTP/2 multiplexing
  • Type safety: Strong typing with Protocol Buffers
  • Code generation: Auto-generate client/server code
  • Streaming: Bidirectional streaming support

Example:

REST:
  POST /api/users
  Content-Type: application/json
  {"name": "John"}

gRPC:
  RPC: CreateUser
  Request: (Protocol Buffer binary)

Intermediate

Q: Explain the different types of gRPC RPCs. When would you use each?

A:

1. Unary RPC:

rpc GetUser (GetUserRequest) returns (User);
  • One request, one response
  • Use when: Simple request/response (like REST)
  • Example: Get user by ID, create user

2. Server Streaming:

rpc ListUsers (ListUsersRequest) returns (stream User);
  • One request, multiple responses
  • Use when: Large dataset, real-time updates
  • Example: List users, live notifications, log streaming

3. Client Streaming:

rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
  • Multiple requests, one response
  • Use when: Batch operations, uploading data
  • Example: Batch create users, file upload

4. Bidirectional Streaming:

rpc Chat (stream Message) returns (stream Message);
  • Multiple requests, multiple responses
  • Use when: Real-time bidirectional communication
  • Example: Chat, gaming, collaborative editing

When to use:

  • Unary: Simple operations (most common)
  • Server streaming: Large datasets, real-time data
  • Client streaming: Batch operations
  • Bidirectional: Real-time communication

Senior

Q: Design a microservices architecture using gRPC. How do you handle service discovery, load balancing, retries, and observability?

A:

class gRPCMicroservicesArchitecture {
  private serviceRegistry: ServiceRegistry;
  private loadBalancer: LoadBalancer;
  private circuitBreaker: CircuitBreaker;
  private observability: Observability;
  
  constructor() {
    this.serviceRegistry = new ServiceRegistry();
    this.loadBalancer = new LoadBalancer();
    this.circuitBreaker = new CircuitBreaker();
    this.observability = new Observability();
  }
  
  // 1. Service Discovery
  class ServiceRegistry {
    private services: Map<string, ServiceInstance[]>;
    
    async register(serviceName: string, instance: ServiceInstance): Promise<void> {
      if (!this.services.has(serviceName)) {
        this.services.set(serviceName, []);
      }
      this.services.get(serviceName).push(instance);
      
      // Health check
      this.startHealthCheck(serviceName, instance);
    }
    
    async discover(serviceName: string): Promise<ServiceInstance[]> {
      const instances = this.services.get(serviceName) || [];
      return instances.filter(instance => instance.healthy);
    }
  }
  
  // 2. Load Balancing
  class LoadBalancer {
    async selectInstance(instances: ServiceInstance[]): Promise<ServiceInstance> {
      // Round robin
      const index = this.getNextIndex(instances.length);
      return instances[index];
      
      // Or: Least connections
      // return instances.reduce((min, i) => 
      //   i.connectionCount < min.connectionCount ? i : min
      // );
    }
  }
  
  // 3. gRPC Client with Retry
  class gRPCClient {
    async call(serviceName: string, method: string, request: any): Promise<any> {
      const instances = await this.serviceRegistry.discover(serviceName);
      const instance = await this.loadBalancer.selectInstance(instances);
      
      // Retry logic
      return await this.retry(async () => {
        return await this.makeRequest(instance, method, request);
      }, {
        maxRetries: 3,
        backoff: 'exponential'
      });
    }
    
    async makeRequest(instance: ServiceInstance, method: string, request: any): Promise<any> {
      const client = await this.getClient(instance);
      
      return new Promise((resolve, reject) => {
        const deadline = new Date();
        deadline.setSeconds(deadline.getSeconds() + 5); // 5 second timeout
        
        client[method](request, { deadline }, (error, response) => {
          if (error) {
            reject(error);
          } else {
            resolve(response);
          }
        });
      });
    }
  }
  
  // 4. Circuit Breaker
  class CircuitBreaker {
    private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
    private failures: number = 0;
    private lastFailureTime: number = 0;
    
    async execute<T>(fn: () => Promise<T>): Promise<T> {
      if (this.state === 'OPEN') {
        if (Date.now() - this.lastFailureTime > 60000) {
          this.state = 'HALF_OPEN';
        } else {
          throw new Error('Circuit breaker is OPEN');
        }
      }
      
      try {
        const result = await fn();
        if (this.state === 'HALF_OPEN') {
          this.state = 'CLOSED';
          this.failures = 0;
        }
        return result;
      } catch (error) {
        this.failures++;
        this.lastFailureTime = Date.now();
        
        if (this.failures >= 5) {
          this.state = 'OPEN';
        }
        
        throw error;
      }
    }
  }
  
  // 5. Observability (Metrics, Tracing, Logging)
  class Observability {
    async recordMetric(service: string, method: string, duration: number, status: string): Promise<void> {
      // Record to metrics system (Prometheus, etc.)
      await this.metrics.record({
        service,
        method,
        duration,
        status
      });
    }
    
    async trace(service: string, method: string, request: any): Promise<string> {
      // Create trace span
      const traceId = this.generateTraceId();
      await this.tracer.startSpan(traceId, {
        service,
        method,
        request
      });
      return traceId;
    }
  }
  
  // 6. Interceptors (Middleware)
  class gRPCInterceptor {
    intercept(call: any, options: any, next: Function): any {
      const traceId = this.observability.trace(call.service, call.method, call.request);
      const startTime = Date.now();
      
      return next(call, options).then(
        (response) => {
          const duration = Date.now() - startTime;
          this.observability.recordMetric(call.service, call.method, duration, 'success');
          return response;
        },
        (error) => {
          const duration = Date.now() - startTime;
          this.observability.recordMetric(call.service, call.method, duration, 'error');
          throw error;
        }
      );
    }
  }
}

Features:

  1. Service discovery: Register and discover services
  2. Load balancing: Distribute requests across instances
  3. Retry logic: Automatic retries with exponential backoff
  4. Circuit breaker: Prevent cascading failures
  5. Observability: Metrics, tracing, logging
  6. Interceptors: Middleware for cross-cutting concerns

Key Takeaways

  • gRPC: High-performance RPC framework using HTTP/2 and Protocol Buffers
  • Protocol Buffers: Binary serialization format, more efficient than JSON
  • RPC types: Unary, server streaming, client streaming, bidirectional streaming
  • Benefits: Performance, type safety, code generation, streaming support
  • vs REST: Binary vs text, HTTP/2 vs HTTP/1.1, stronger typing
  • Service definition: Define services and messages in .proto files
  • Use cases: Microservices communication, high-performance APIs, real-time streaming
  • Best practices: Handle errors, set timeouts, use streaming for large data, implement retries and circuit breakers

About the author

InterviewCrafted helps you master system design with patience. We believe in curiosity-led engineering, reflective writing, and designing systems that make future changes feel calm.