Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog
in java

grpc

Tung Bui Thanh (TungDaDev)32 mins read

Grpc.webp

Nếu REST là quốc lộ quen thuộc thì gRPC là đường cao tốc: nhanh hơn, hiệu quả hơn, nhưng phải biết cách lái.

Một Google Engineer nào đó, trong buổi tech talk lúc 2 giờ sáng.


# gRPC là gì? tại sao google tạo ra nó?

# câu chuyện bắt đầu từ bên trong google

Trước khi có gRPC, Google đã dùng một hệ thống RPC nội bộ tên Stubby hơn một thập kỷ để nối vô số microservice trong và giữa các data center của họ. Stubby nhanh, hiệu quả, nhưng không dựa trên chuẩn mở nào và gắn quá chặt với hạ tầng nội bộ của Google nên không đem ra open-source được.

Tháng 3/2015, Google quyết định làm phiên bản kế tiếp của Stubby dựa trên các chuẩn mở như HTTP/2 và open-source nó, kết quả là gRPC (bản 1.0 ra tháng 8/2016). Tên thường được đọc là "gRPC Remote Procedure Calls", kiểu viết tắt đệ quy. Vui hơn nữa: repo gRPC có hẳn một file ghi chữ "g" ở mỗi bản phát hành nghĩa là gì, mỗi bản một nghĩa (good, green, gentle...), rất Google style 😄.

# định nghĩa chính thức

gRPC là một framework RPC (Remote Procedure Call) hiệu năng cao, open-source, giúp các service giao tiếp với nhau hiệu quả, bất kể viết bằng ngôn ngữ gì hay chạy ở đâu.

Nói đơn giản: gRPC cho phép bạn gọi một function trên server khác như thể nó nằm ngay trong code của bạn.

grpc-call

Bạn gọi orderService.getOrder(orderId) ở client Java và nó thực sự chạy logic trên server Python. Client không cần biết server viết bằng gì, gRPC lo hết.

# các đặc điểm chính

Đặc điểmMô tả
HTTP/2Multiplexing, header compression, bidirectional streaming
Protocol BuffersBinary serialization, thường nhỏ hơn JSON vài lần
Contract-firstĐịnh nghĩa API bằng .proto file trước, code generate sau
PolyglotChính thức hỗ trợ ~11 ngôn ngữ: Java, Go, Python, C++, C#…
StreamingHỗ trợ 4 kiểu communication pattern
Deadline/TimeoutBuilt-in mechanism cho timeout propagation
InterceptorsMiddleware pattern cho cross-cutting concerns

# kiến trúc & cách hoạt động bên trong

# luồng hoạt động từ A đến Z

Thử trace một gRPC call từ đầu đến cuối:

grpc-flow

# bước 1: định nghĩa contract (.proto)

Mọi thứ bắt đầu từ file .proto, "hợp đồng" giữa client và server:

syntax = "proto3";
 
package com.example.order;
 
option java_multiple_files = true;
option java_package = "com.example.order.grpc";
 
service OrderService {
  rpc GetOrder (GetOrderRequest) returns (OrderResponse);
  rpc CreateOrder (CreateOrderRequest) returns (OrderResponse);
  rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse);
}
 
message GetOrderRequest {
  string order_id = 1;
}
 
message CreateOrderRequest {
  string customer_id = 1;
  repeated OrderItem items = 2;
  string shipping_address = 3;
}
 
message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double price = 3;
}
 
message OrderResponse {
  string order_id = 1;
  string customer_id = 2;
  repeated OrderItem items = 3;
  OrderStatus status = 4;
  double total_amount = 5;
}
 
enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  PENDING = 1;
  CONFIRMED = 2;
  SHIPPED = 3;
  DELIVERED = 4;
  CANCELLED = 5;
}

Mỗi field có một số thứ tự (field number). Protobuf dùng số này để nhận diện field trong binary format, không dùng tên, nên đổi tên field không làm hỏng dữ liệu binary giữa bản cũ và bản mới. Có điều tên field vẫn xuất hiện ở chỗ khác: JSON mapping (vd khi đi qua gRPC-Web gateway hoặc log dạng JSON), text format, và code được generate. Đổi tên thì code bên dùng phải sửa theo, và client nào đang gửi JSON với tên cũ sẽ hỏng.

# bước 2: code generation

Khi chạy protoc compiler (kèm plugin protoc-gen-grpc-java cho phần service), nó generate ra:

  • Message classes: Java POJOs với builder pattern cho mỗi message
  • Client Stub: Class để gọi remote methods
  • Server Base class: Abstract class mà bạn implement logic

# bước 3: serialization với protocol buffers

Khi client gọi getOrder():

  1. Request object được serialize thành binary bằng protobuf
  2. Binary data được đóng gói trong HTTP/2 frame
  3. Gửi qua network
  4. Server deserialize binary thành object
  5. Xử lý logic, tạo response
  6. Response serialize → binary → HTTP/2 frame → client
  7. Client deserialize → response object

# tại sao http/2 quan trọng?

HTTP/1.1 có vấn đề head-of-line blocking: trên một connection, response phải trả về đúng thứ tự request, request sau phải chờ request trước xong. Client muốn song song thì phải mở nhiều connection. HTTP/2 giải quyết chuyện này ở tầng HTTP (ở tầng TCP thì vẫn còn: một packet bị mất làm khựng mọi stream trên connection đó, HTTP/3 chạy trên QUIC mới gỡ được):

grpc-http2
  • Multiplexing: Nhiều request/response song song trên 1 connection
  • Header compression (HPACK): Giảm overhead cho repeated headers
  • Server push: Server có thể gửi data mà client chưa request (gRPC không dùng tính năng này; streaming của gRPC là các DATA frame trên chính stream của lời gọi)
  • Binary framing: Hiệu quả hơn text-based HTTP/1.1

# protocol buffers: ngôn ngữ chung của gRPC

# binary vs text: cuộc chiến hiệu năng

So sánh cùng một data:

JSON (REST), ~150 bytes khi bỏ khoảng trắng (~210 bytes nếu format như dưới):

{
  "orderId": "ORD-12345",
  "customerId": "CUST-789",
  "items": [
    {
      "productId": "PROD-001",
      "quantity": 2,
      "price": 29.99
    }
  ],
  "status": "CONFIRMED",
  "totalAmount": 59.98
}

Protocol Buffers (gRPC), 55 bytes:

Binary: 0A 09 4F 52 44 2D 31 32 33 34 35 12 08 ...

→ Nhỏ hơn gần 3 lần. Đọc byte đầu: 0A là field số 1 kiểu length-delimited (1 << 3 | 2), 09 là độ dài 9 byte của chuỗi ORD-12345; tên field không hề có trong payload. Tỷ lệ này thay đổi theo dữ liệu: payload toàn số và tên field dài thì protobuf thắng đậm, payload chủ yếu là chuỗi dài thì chênh lệch nhỏ đi (và JSON nén gzip cũng thu hẹp khoảng cách). Tốc độ serialize/deserialize thường nhanh hơn JSON đáng kể, nhưng con số cụ thể phụ thuộc thư viện và dữ liệu, nên tự benchmark với payload thật của bạn.

# proto3 syntax cheat sheet

syntax = "proto3";
 
// Scalar types
message Example {
  double latitude = 1;       // 64-bit float
  float score = 2;           // 32-bit float
  int32 age = 3;             // Variable-length encoding (số âm tốn 10 byte, dùng sint32 nếu hay âm)
  int64 timestamp = 4;       // Variable-length encoding
  bool is_active = 5;        // Boolean
  string name = 6;           // UTF-8 string
  bytes avatar = 7;          // Arbitrary byte data
}
 
// Collections
message Cart {
  repeated CartItem items = 1;          // List<CartItem>
  map<string, int32> quantities = 2;    // Map<String, Integer>
}
 
// Nested messages
message Address {
  message GeoPoint {
    double lat = 1;
    double lng = 2;
  }
  string street = 1;
  string city = 2;
  GeoPoint location = 3;
}
 
// Oneof — chỉ 1 field được set tại một thời điểm
message Payment {
  oneof method {
    CreditCard credit_card = 1;
    BankTransfer bank_transfer = 2;
    Wallet wallet = 3;
  }
}
 
// Optional — phân biệt "not set" vs "default value"
message Filter {
  optional int32 min_price = 1;  // có thể check hasMinPrice()
  optional int32 max_price = 2;
}
 
// Well-known types
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/wrappers.proto";
 
message Event {
  google.protobuf.Timestamp created_at = 1;
  google.protobuf.Duration ttl = 2;
  google.protobuf.StringValue nullable_name = 3;
}

# mấy quy tắc khi thiết kế proto

  1. Không bao giờ thay đổi field number của field đã tồn tại
  2. Không bao giờ reuse field number đã bị xóa, dùng reserved:
message Order {
  reserved 4, 8;
  reserved "old_status", "legacy";
}
  1. Enum phải có value 0 (proto3 bắt buộc giá trị đầu tiên là 0), quy ước đặt là XXX_UNSPECIFIED để phân biệt "chưa set". Style guide còn khuyên đặt prefix theo tên enum cho mọi value (ORDER_STATUS_PENDING), vì value của enum nằm chung scope với package và dễ đụng tên giữa các enum.
  2. Dùng optional (từ protobuf 3.15) hoặc wrapper types khi cần phân biệt "không set" với giá trị mặc định (0, "", false)
  3. Đặt tên theo snake_case cho field, PascalCase cho message và service

# bốn communication patterns

gRPC hỗ trợ 4 kiểu giao tiếp, trong đó 3 kiểu streaming là thứ REST thuần không có sẵn:

# pattern 1: unary RPC (request-response)

Giống REST truyền thống: 1 request, 1 response.

rpc GetOrder (GetOrderRequest) returns (OrderResponse);

Use case: CRUD operations, authentication, simple queries.

# pattern 2: server streaming rpc

Client gửi 1 request, server trả về một stream of responses.

rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse);

Use case: Real-time feeds, downloading large datasets, live notifications.

# pattern 3: client streaming rpc

Client gửi một stream of requests, server trả về 1 response khi stream kết thúc.

rpc UploadOrderItems (stream OrderItem) returns (UploadSummary);
protobuf-rpc

Use case: File upload, batch processing, IoT sensor data collection.

# pattern 4: bidirectional streaming rpc

Cả client và server đều gửi stream, độc lập với nhau.

rpc OrderChat (stream ChatMessage) returns (stream ChatMessage);
protobuf-rpc-msg

Use case: Chat, real-time collaboration, gaming, live dashboards.

# so sánh gRPC vs REST vs GraphQL

Tiêu chígRPCRESTGraphQL
ProtocolHTTP/2HTTP/1.1 hoặc HTTP/2Thường là HTTP
Data formatProtobuf (binary)JSON (text)JSON (text)
Contract.proto fileOpenAPI/SwaggerSchema
Code generationBuilt-in, đa ngôn ngữThird-party toolsThird-party tools
Streaming4 patterns nativeKhông (cần SSE/WS)Subscriptions
Browser supportCần gRPC-Web proxyNativeNative
PerformanceNhanhChậm hơnChậm hơn
Payload sizeNhỏ nhấtLớn nhấtTrung bình
Learning curveCaoThấpTrung bình
DebuggingKhó (binary)Dễ (text/curl)Trung bình
EcosystemLớn, trưởng thànhRất lớnLớn

# khi nào chọn gì?

  • gRPC: Service-to-service communication, microservices nội bộ, real-time streaming, low-latency requirements
  • REST: Public APIs, browser clients, simple CRUD, team mới bắt đầu
  • GraphQL: Frontend-driven APIs, mobile apps cần flexible queries, BFF pattern

# hands-on: xây dựng gRPC service với Java & Spring Boot

Đủ lý thuyết rồi, giờ code.

Ta sẽ xây một Order Management Service với:

  • Unary RPC: Tạo và lấy order
  • Server Streaming: Stream danh sách orders
  • Error handling với gRPC Status codes
  • Interceptors cho logging

# project structure

order-grpc-service/
├── src/main/
│   ├── proto/
│   │   └── order_service.proto
│   ├── java/com/example/orderservice/
│   │   ├── OrderGrpcServiceApplication.java
│   │   ├── service/
│   │   │   └── OrderGrpcService.java
│   │   ├── interceptor/
│   │   │   ├── LoggingInterceptor.java
│   │   │   └── AuthInterceptor.java
│   │   └── client/
│   │       └── OrderGrpcClient.java
│   └── resources/
│       └── application.yml
└── pom.xml

# bước 1: setup dependencies (maven)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
    </parent>
 
    <groupId>com.example</groupId>
    <artifactId>order-grpc-service</artifactId>
    <version>1.0.0</version>
 
    <properties>
        <java.version>17</java.version>
        <grpc.version>1.60.0</grpc.version>
        <protobuf.version>3.25.1</protobuf.version>
        <grpc-spring-boot.version>3.0.0.RELEASE</grpc-spring-boot.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>net.devh</groupId>
            <artifactId>grpc-server-spring-boot-starter</artifactId>
            <version>${grpc-spring-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>net.devh</groupId>
            <artifactId>grpc-client-spring-boot-starter</artifactId>
            <version>${grpc-spring-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
 
    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.7.1</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protocArtifact>
                        com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
                    </protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>
                        io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
                    </pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

💡 Tip: Ví dụ trong bài dùng net.devh:grpc-spring-boot-starter (nay ở grpc-ecosystem/grpc-spring), thư viện cộng đồng từng là lựa chọn phổ biến nhất cho gRPC + Spring Boot. Nó auto-configure gRPC server, tích hợp với Spring DI, và hỗ trợ cả server lẫn client. Từ cuối 2025, Spring có dự án chính thức Spring gRPC (1.0 cho Spring Boot 4.0), và tới Spring Boot 4.1 (6/2026) thì auto-configuration gRPC đã nằm hẳn trong Spring Boot. Project mới trên Boot 4.x nên cân nhắc Spring gRPC: annotation và property khác (vd @GrpcService của org.springframework.grpc, cấu hình dưới spring.grpc.*), nhưng phần proto và code StreamObserver bên dưới vẫn y nguyên.

# bước 2: định nghĩa proto file

Tạo file src/main/proto/order_service.proto:

syntax = "proto3";
 
package com.example.order;
 
option java_multiple_files = true;
option java_package = "com.example.orderservice.grpc";
option java_outer_classname = "OrderServiceProto";
 
// ─── Service Definition ───────────────────────────────────
service OrderService {
  // Unary: tạo order mới
  rpc CreateOrder (CreateOrderRequest) returns (OrderResponse);
 
  // Unary: lấy order theo ID
  rpc GetOrder (GetOrderRequest) returns (OrderResponse);
 
  // Server Streaming: lấy danh sách orders của customer
  rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse);
 
  // Client Streaming: upload nhiều items cùng lúc
  rpc BatchCreateItems (stream OrderItem) returns (BatchCreateResponse);
 
  // Bidirectional Streaming: theo dõi order status real-time
  rpc TrackOrders (stream TrackOrderRequest) returns (stream OrderStatusUpdate);
}
 
// ─── Request Messages ─────────────────────────────────────
message CreateOrderRequest {
  string customer_id = 1;
  repeated OrderItem items = 2;
  string shipping_address = 3;
  optional string coupon_code = 4;
}
 
message GetOrderRequest {
  string order_id = 1;
}
 
message ListOrdersRequest {
  string customer_id = 1;
  int32 page_size = 2;
  string page_token = 3;
  OrderStatus status_filter = 4;
}
 
message TrackOrderRequest {
  string order_id = 1;
}
 
// ─── Response Messages ────────────────────────────────────
message OrderResponse {
  string order_id = 1;
  string customer_id = 2;
  repeated OrderItem items = 3;
  OrderStatus status = 4;
  double total_amount = 5;
  string shipping_address = 6;
  string created_at = 7;
}
 
message BatchCreateResponse {
  int32 total_items = 1;
  int32 success_count = 2;
  int32 failed_count = 3;
}
 
message OrderStatusUpdate {
  string order_id = 1;
  OrderStatus previous_status = 2;
  OrderStatus current_status = 3;
  string updated_at = 4;
  string message = 5;
}
 
// ─── Shared Messages ──────────────────────────────────────
message OrderItem {
  string product_id = 1;
  string product_name = 2;
  int32 quantity = 3;
  double unit_price = 4;
}
 
// ─── Enums ────────────────────────────────────────────────
enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  PENDING = 1;
  CONFIRMED = 2;
  PROCESSING = 3;
  SHIPPED = 4;
  DELIVERED = 5;
  CANCELLED = 6;
}

Chạy mvn compile để generate Java code từ proto file. Maven plugin sẽ tự động gọi protoc và tạo ra các class trong target/generated-sources/protobuf/.

# bước 3: application configuration

src/main/resources/application.yml:

spring:
  application:
    name: order-grpc-service
 
grpc:
  server:
    port: 9090 # gRPC server port (tách biệt với HTTP)
    enable-keep-alive: true
    keep-alive-time: 30s
    keep-alive-timeout: 5s
    permit-keep-alive-without-calls: true
    max-inbound-message-size: 4MB
 
  client:
    # Config cho khi service này gọi service khác
    inventory-service:
      address: static://localhost:9091
      negotiation-type: plaintext
      enable-keep-alive: true
      keep-alive-without-calls: true
    order-service: # tên dùng trong @GrpcClient("order-service") ở bước 5
      address: static://localhost:9090
      negotiation-type: plaintext
 
logging:
  level:
    com.example: DEBUG
    net.devh.boot.grpc: DEBUG

# bước 4: implement gRPC server

Đây là phần quan trọng nhất, implement logic cho service:

package com.example.orderservice.service;
 
import com.example.orderservice.grpc.*;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;
 
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.UUID;
 
/**
 * gRPC Service implementation cho Order Management.
 *
 * Annotation @GrpcService tương đương @Service của Spring,
 * nhưng đăng ký class này như một gRPC service handler.
 * Spring Boot auto-configure sẽ tự động start gRPC server
 * và register service này.
 */
@GrpcService
public class OrderGrpcService extends OrderServiceGrpc.OrderServiceImplBase {
 
    // In-memory store (thay bằng database trong production)
    private final Map<String, OrderResponse> orderStore = new ConcurrentHashMap<>();
 
    // ═══════════════════════════════════════════════════════════
    // UNARY RPC: CreateOrder
    // Client gửi 1 request, server trả 1 response
    // ═══════════════════════════════════════════════════════════
    @Override
    public void createOrder(CreateOrderRequest request,
                            StreamObserver<OrderResponse> responseObserver) {
 
        // Validate input
        if (request.getCustomerId().isBlank()) {
            responseObserver.onError(
                Status.INVALID_ARGUMENT
                    .withDescription("customer_id is required")
                    .asRuntimeException()
            );
            return;
        }
 
        if (request.getItemsList().isEmpty()) {
            responseObserver.onError(
                Status.INVALID_ARGUMENT
                    .withDescription("Order must have at least one item")
                    .asRuntimeException()
            );
            return;
        }
 
        // Calculate total
        double totalAmount = request.getItemsList().stream()
            .mapToDouble(item -> item.getUnitPrice() * item.getQuantity())
            .sum();
 
        // Build response
        String orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8);
        OrderResponse order = OrderResponse.newBuilder()
            .setOrderId(orderId)
            .setCustomerId(request.getCustomerId())
            .addAllItems(request.getItemsList())
            .setStatus(OrderStatus.PENDING)
            .setTotalAmount(totalAmount)
            .setShippingAddress(request.getShippingAddress())
            .setCreatedAt(Instant.now().toString())
            .build();
 
        // Persist
        orderStore.put(orderId, order);
 
        // Trả response — flow giống REST controller
        // onNext() = gửi response message
        // onCompleted() = kết thúc call (đóng stream của call này, connection HTTP/2 vẫn giữ)
        responseObserver.onNext(order);
        responseObserver.onCompleted();
    }
 
    // ═══════════════════════════════════════════════════════════
    // UNARY RPC: GetOrder
    // ═══════════════════════════════════════════════════════════
    @Override
    public void getOrder(GetOrderRequest request,
                         StreamObserver<OrderResponse> responseObserver) {
 
        String orderId = request.getOrderId();
        OrderResponse order = orderStore.get(orderId);
 
        if (order == null) {
            // gRPC dùng Status codes thay vì HTTP status codes
            // NOT_FOUND tương đương HTTP 404
            responseObserver.onError(
                Status.NOT_FOUND
                    .withDescription("Order not found: " + orderId)
                    .asRuntimeException()
            );
            return;
        }
 
        responseObserver.onNext(order);
        responseObserver.onCompleted();
    }
 
    // ═══════════════════════════════════════════════════════════
    // SERVER STREAMING: ListOrders
    // Client gửi 1 request, server trả về NHIỀU responses
    // ═══════════════════════════════════════════════════════════
    @Override
    public void listOrders(ListOrdersRequest request,
                           StreamObserver<OrderResponse> responseObserver) {
 
        String customerId = request.getCustomerId();
        OrderStatus statusFilter = request.getStatusFilter();
 
        orderStore.values().stream()
            .filter(order -> order.getCustomerId().equals(customerId))
            .filter(order -> statusFilter == OrderStatus.ORDER_STATUS_UNSPECIFIED
                             || order.getStatus() == statusFilter)
            .forEach(order -> {
                // Mỗi lần gọi onNext() = gửi 1 message trong stream
                // Client nhận được từng message một, không cần đợi tất cả
                responseObserver.onNext(order);
 
                // Simulate delay (trong thực tế, data có thể đến từ DB cursor)
                try { Thread.sleep(100); } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
 
        // Signal: stream đã kết thúc
        responseObserver.onCompleted();
    }
 
    // ═══════════════════════════════════════════════════════════
    // CLIENT STREAMING: BatchCreateItems
    // Client gửi NHIỀU requests, server trả 1 response cuối cùng
    // ═══════════════════════════════════════════════════════════
    @Override
    public StreamObserver<OrderItem> batchCreateItems(
            StreamObserver<BatchCreateResponse> responseObserver) {
 
        // Trả về một StreamObserver để NHẬN data từ client
        return new StreamObserver<>() {
            private final List<OrderItem> items = new ArrayList<>();
            private int failedCount = 0;
 
            @Override
            public void onNext(OrderItem item) {
                // Được gọi mỗi khi client gửi 1 item
                if (item.getQuantity() <= 0) {
                    failedCount++;
                } else {
                    items.add(item);
                }
            }
 
            @Override
            public void onError(Throwable t) {
                // Client gặp lỗi hoặc disconnect
                System.err.println("Client error: " + t.getMessage());
            }
 
            @Override
            public void onCompleted() {
                // Client đã gửi xong tất cả items
                // Bây giờ mới trả response
                BatchCreateResponse response = BatchCreateResponse.newBuilder()
                    .setTotalItems(items.size() + failedCount)
                    .setSuccessCount(items.size())
                    .setFailedCount(failedCount)
                    .build();
 
                responseObserver.onNext(response);
                responseObserver.onCompleted();
            }
        };
    }
 
    // ═══════════════════════════════════════════════════════════
    // BIDIRECTIONAL STREAMING: TrackOrders
    // Cả client và server đều gửi stream, độc lập nhau
    // ═══════════════════════════════════════════════════════════
    @Override
    public StreamObserver<TrackOrderRequest> trackOrders(
            StreamObserver<OrderStatusUpdate> responseObserver) {
 
        return new StreamObserver<>() {
            @Override
            public void onNext(TrackOrderRequest request) {
                // Client subscribe theo dõi 1 order
                String orderId = request.getOrderId();
                OrderResponse order = orderStore.get(orderId);
 
                if (order != null) {
                    // Gửi status update ngay lập tức
                    OrderStatusUpdate update = OrderStatusUpdate.newBuilder()
                        .setOrderId(orderId)
                        .setPreviousStatus(OrderStatus.ORDER_STATUS_UNSPECIFIED)
                        .setCurrentStatus(order.getStatus())
                        .setUpdatedAt(Instant.now().toString())
                        .setMessage("Current status for " + orderId)
                        .build();
 
                    responseObserver.onNext(update);
                }
            }
 
            @Override
            public void onError(Throwable t) {
                System.err.println("Client disconnected: " + t.getMessage());
            }
 
            @Override
            public void onCompleted() {
                responseObserver.onCompleted();
            }
        };
    }
}

Ví dụ dùng double cho tiền cho gọn. Code thật thì đừng: dùng số nguyên theo đơn vị nhỏ nhất (vd int64 amount_minor) hoặc kiểu google.type.Money, tránh sai số dấu phẩy động.

Để ý pattern chung:

  • Unary: void method(Request, StreamObserver<Response>), đơn giản nhất
  • Server Streaming: Giống Unary, nhưng gọi onNext() nhiều lần
  • Client Streaming: Return StreamObserver<Request> để nhận data từ client
  • Bidi Streaming: Kết hợp cả hai, return StreamObserver và dùng responseObserver

# bước 5: implement gRPC client

package com.example.orderservice.client;
 
import com.example.orderservice.grpc.*;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;
 
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
 
@Service
public class OrderGrpcClient {
 
    // @GrpcClient inject một stub tự động từ Spring context
    // "order-service" map với config trong application.yml
    @GrpcClient("order-service")
    private OrderServiceGrpc.OrderServiceBlockingStub blockingStub;
 
    @GrpcClient("order-service")
    private OrderServiceGrpc.OrderServiceStub asyncStub;
 
    // ─── Unary Call (Blocking) ────────────────────────────────
    public OrderResponse createOrder(String customerId,
                                     List<OrderItem> items,
                                     String address) {
        CreateOrderRequest request = CreateOrderRequest.newBuilder()
            .setCustomerId(customerId)
            .addAllItems(items)
            .setShippingAddress(address)
            .build();
 
        try {
            // Blocking call — thread chờ cho đến khi có response (tối đa 3 giây)
            return blockingStub
                .withDeadlineAfter(3, TimeUnit.SECONDS)
                .createOrder(request);
        } catch (StatusRuntimeException e) {
            System.err.println("gRPC error: " + e.getStatus());
            throw e;
        }
    }
 
    // ─── Unary Call (Blocking) ────────────────────────────────
    public OrderResponse getOrder(String orderId) {
        GetOrderRequest request = GetOrderRequest.newBuilder()
            .setOrderId(orderId)
            .build();
 
        try {
            return blockingStub
                .withDeadlineAfter(3, TimeUnit.SECONDS)
                .getOrder(request);
        } catch (StatusRuntimeException e) {
            if (e.getStatus().getCode() == io.grpc.Status.Code.NOT_FOUND) {
                System.err.println("Order not found: " + orderId);
                return null;
            }
            throw e;
        }
    }
 
    // ─── Server Streaming (Blocking Iterator) ─────────────────
    public List<OrderResponse> listOrders(String customerId) {
        ListOrdersRequest request = ListOrdersRequest.newBuilder()
            .setCustomerId(customerId)
            .build();
 
        List<OrderResponse> orders = new ArrayList<>();
 
        // blockingStub trả về Iterator cho server streaming
        // Mỗi lần gọi next() sẽ block cho đến khi server gửi message tiếp
        Iterator<OrderResponse> iterator = blockingStub.listOrders(request);
        while (iterator.hasNext()) {
            OrderResponse order = iterator.next();
            orders.add(order);
            System.out.println("Received order: " + order.getOrderId());
        }
 
        return orders;
    }
 
    // ─── Server Streaming (Async) ─────────────────────────────
    public void listOrdersAsync(String customerId) throws InterruptedException {
        ListOrdersRequest request = ListOrdersRequest.newBuilder()
            .setCustomerId(customerId)
            .build();
 
        CountDownLatch latch = new CountDownLatch(1);
 
        // Async stub — non-blocking, callback-based
        asyncStub.listOrders(request, new StreamObserver<>() {
            @Override
            public void onNext(OrderResponse order) {
                System.out.println("Async received: " + order.getOrderId());
            }
 
            @Override
            public void onError(Throwable t) {
                System.err.println("Stream error: " + t.getMessage());
                latch.countDown();
            }
 
            @Override
            public void onCompleted() {
                System.out.println("Stream completed");
                latch.countDown();
            }
        });
 
        // Đợi stream hoàn thành (max 30 giây)
        latch.await(30, TimeUnit.SECONDS);
    }
 
    // ─── Client Streaming (Async) ─────────────────────────────
    public void batchCreateItems(List<OrderItem> items) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
 
        // asyncStub trả về StreamObserver để GỬI data lên server
        StreamObserver<OrderItem> requestObserver = asyncStub.batchCreateItems(
            new StreamObserver<>() {
                @Override
                public void onNext(BatchCreateResponse response) {
                    System.out.printf("Batch result: %d/%d succeeded%n",
                        response.getSuccessCount(),
                        response.getTotalItems());
                }
 
                @Override
                public void onError(Throwable t) {
                    latch.countDown();
                }
 
                @Override
                public void onCompleted() {
                    latch.countDown();
                }
            }
        );
 
        // Gửi từng item lên server
        for (OrderItem item : items) {
            requestObserver.onNext(item);
            Thread.sleep(50); // Simulate delay
        }
 
        // Signal: đã gửi xong
        requestObserver.onCompleted();
        latch.await(10, TimeUnit.SECONDS);
    }
}

🔑 Các loại Stub cần biết:

  • BlockingStub: Synchronous, thread chờ response. Dùng cho Unary và Server Streaming. (Bản grpc-java mới có thêm BlockingV2Stub dùng được cho cả 4 kiểu, vẫn đang ở dạng experimental.)
  • Stub (async): Asynchronous, callback-based. Dùng cho tất cả 4 patterns.
  • Còn FutureStub: Trả về ListenableFuture, chỉ dùng cho Unary.

# bước 6: spring boot application

package com.example.orderservice;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class OrderGrpcServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderGrpcServiceApplication.class, args);
        // gRPC server tự động start trên port 9090
        // Spring Boot web server (nếu có) chạy trên port 8080
    }
}

Chạy application:

mvn spring-boot:run

Output:

gRPC Server started, listening on address: *, port: 9090

# error handling & deadline

# gRPC status codes

gRPC không dùng HTTP status codes. Thay vào đó, nó có hệ thống status codes riêng:

gRPC StatusHTTP tương đươngKhi nào dùng
OK200Thành công
INVALID_ARGUMENT400Input không hợp lệ
NOT_FOUND404Resource không tồn tại
ALREADY_EXISTS409Resource đã tồn tại
PERMISSION_DENIED403Không có quyền
UNAUTHENTICATED401Chưa xác thực
RESOURCE_EXHAUSTED429Rate limit / quota exceeded
INTERNAL500Lỗi server
UNAVAILABLE503Service tạm thời không khả dụng
DEADLINE_EXCEEDED504Timeout
UNIMPLEMENTED501Method chưa được implement
CANCELLED499Client hủy request

# rich error handling

import com.google.rpc.ErrorInfo;
import com.google.rpc.BadRequest;
import io.grpc.protobuf.StatusProto;
 
// ─── Trả về error với metadata chi tiết ───────────────────
private void handleValidationError(
        StreamObserver<?> responseObserver,
        String field,
        String description) {
 
    // Tạo rich error với field violations
    BadRequest badRequest = BadRequest.newBuilder()
        .addFieldViolations(
            BadRequest.FieldViolation.newBuilder()
                .setField(field)
                .setDescription(description)
                .build()
        )
        .build();
 
    com.google.rpc.Status status = com.google.rpc.Status.newBuilder()
        .setCode(com.google.rpc.Code.INVALID_ARGUMENT_VALUE)
        .setMessage("Validation failed")
        .addDetails(com.google.protobuf.Any.pack(badRequest))
        .build();
 
    responseObserver.onError(StatusProto.toStatusRuntimeException(status));
}
 
// ─── Client-side: parse rich error ────────────────────────
try {
    OrderResponse order = blockingStub.createOrder(request);
} catch (StatusRuntimeException e) {
    com.google.rpc.Status status = StatusProto.fromThrowable(e);
    if (status != null) {
        for (com.google.protobuf.Any detail : status.getDetailsList()) {
            if (detail.is(BadRequest.class)) {
                // unpack() ném InvalidProtocolBufferException (checked): method bao ngoài phải khai báo/bắt
                BadRequest badRequest = detail.unpack(BadRequest.class);
                badRequest.getFieldViolationsList().forEach(violation ->
                    System.err.printf("Field '%s': %s%n",
                        violation.getField(),
                        violation.getDescription())
                );
            }
        }
    }
}

# deadline (timeout propagation)

Deadline là một trong những feature hay nhất của gRPC: nó propagate timeout xuyên suốt chuỗi service calls. Client gửi kèm header grpc-timeout (thời gian còn lại, không phải thời điểm tuyệt đối, nên không phụ thuộc đồng hồ các máy khớp nhau). Ở grpc-java, server đặt deadline đó vào Context, và lời gọi gRPC đi ra từ trong Context đó tự lấy deadline còn lại:

Client (deadline: 5s)
  -> Service A (con 4.8s)
    -> Service B (con 4.2s)
      -> Service C (con 3.5s)
        -> Database (con 2.8s, phai tu dat query timeout)

Hết 5s, client nhận DEADLINE_EXCEEDED, còn Context ở các service trong chain bị cancel, nên lời gọi đi tiếp bị huỷ và server nào chịu kiểm tra Context thì dừng việc sớm. Hai chỗ không tự động: deadline không tự đi xuống database hay HTTP client (phải tự đặt timeout từ thời gian còn lại), và nếu bạn gọi service khác từ một thread pool riêng thì Context không theo sang, phải bọc bằng Context.current().wrap(...) hoặc Context.currentContextExecutor(...).

// ─── Client: set deadline ─────────────────────────────────
OrderResponse order = blockingStub
    .withDeadlineAfter(5, TimeUnit.SECONDS)  // Timeout 5 giây
    .getOrder(request);
 
// ─── Server: check deadline ───────────────────────────────
@Override
public void getOrder(GetOrderRequest request,
                     StreamObserver<OrderResponse> responseObserver) {
 
    // Check xem deadline đã hết chưa trước khi làm việc nặng
    if (Context.current().isCancelled()) {
        responseObserver.onError(
            Status.CANCELLED
                .withDescription("Client cancelled or deadline exceeded")
                .asRuntimeException()
        );
        return;
    }
 
    // Nếu service này gọi service khác (trên cùng thread/Context), deadline
    // tự động propagate qua gRPC context — không cần truyền manual
    OrderResponse order = orderStore.get(request.getOrderId());
    if (order == null) {
        responseObserver.onError(Status.NOT_FOUND.asRuntimeException());
        return;
    }
    responseObserver.onNext(order);
    responseObserver.onCompleted();
}

⚠️ Production tip: LUÔN set deadline cho mọi gRPC call. Không có deadline = request có thể treo vĩnh viễn, leak resources, và gây cascading failures.

# interceptors & middleware

Interceptors trong gRPC tương đương với Filters/Middleware trong REST. Chúng cho phép bạn thêm cross-cutting concerns mà không sửa business logic.

# server interceptor: logging

package com.example.orderservice.interceptor;
 
import io.grpc.*;
import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor;
 
@GrpcGlobalServerInterceptor  // Áp dụng cho TẤT CẢ gRPC services
public class LoggingInterceptor implements ServerInterceptor {
 
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
 
        String methodName = call.getMethodDescriptor().getFullMethodName();
        long startTime = System.currentTimeMillis();
 
        System.out.printf("[gRPC] → %s started%n", methodName);
 
        // Wrap ServerCall để intercept response
        ServerCall<ReqT, RespT> wrappedCall = new ForwardingServerCall
                .SimpleForwardingServerCall<>(call) {
 
            @Override
            public void close(Status status, Metadata trailers) {
                long duration = System.currentTimeMillis() - startTime;
                System.out.printf("[gRPC] ← %s completed | status=%s | %dms%n",
                    methodName, status.getCode(), duration);
                super.close(status, trailers);
            }
        };
 
        return next.startCall(wrappedCall, headers);
    }
}

# server interceptor: authentication

package com.example.orderservice.interceptor;
 
import io.grpc.*;
import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor;
 
@GrpcGlobalServerInterceptor
public class AuthInterceptor implements ServerInterceptor {
 
    // Metadata key cho auth token (tương đương HTTP header)
    private static final Metadata.Key<String> AUTH_KEY =
        Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
 
    // Context key để truyền user info xuống service layer
    public static final Context.Key<String> USER_ID_KEY =
        Context.key("userId");
 
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
 
        String methodName = call.getMethodDescriptor().getFullMethodName();
 
        // Skip auth cho health check (so khớp đúng service chuẩn, đừng dùng contains("Health"):
        // method nào tình cờ có chữ Health trong tên cũng được bỏ qua auth)
        if (methodName.startsWith("grpc.health.v1.Health/")) {
            return next.startCall(call, headers);
        }
 
        // Extract token từ metadata
        String token = headers.get(AUTH_KEY);
        if (token == null || !token.startsWith("Bearer ")) {
            call.close(
                Status.UNAUTHENTICATED.withDescription("Missing or invalid token"),
                new Metadata()
            );
            return new ServerCall.Listener<>() {};
        }
 
        // Validate token (simplified)
        String userId = validateToken(token.substring(7));
        if (userId == null) {
            call.close(
                Status.UNAUTHENTICATED.withDescription("Invalid token"),
                new Metadata()
            );
            return new ServerCall.Listener<>() {};
        }
 
        // Attach userId vào gRPC Context — service có thể đọc bằng
        // AuthInterceptor.USER_ID_KEY.get()
        Context context = Context.current().withValue(USER_ID_KEY, userId);
        return Contexts.interceptCall(context, call, headers, next);
    }
 
    private String validateToken(String token) {
        // Implement JWT validation logic here
        // Return userId if valid, null if invalid
        return "user-123"; // Simplified
    }
}

# client interceptor: attach auth token

package com.example.orderservice.interceptor;
 
import io.grpc.*;
import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor;
 
@GrpcGlobalClientInterceptor
public class ClientAuthInterceptor implements ClientInterceptor {
 
    private static final Metadata.Key<String> AUTH_KEY =
        Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
 
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
            MethodDescriptor<ReqT, RespT> method,
            CallOptions callOptions,
            Channel next) {
 
        return new ForwardingClientCall.SimpleForwardingClientCall<>(
                next.newCall(method, callOptions)) {
 
            @Override
            public void start(Listener<RespT> responseListener, Metadata headers) {
                // Attach token vào mọi outgoing request
                headers.put(AUTH_KEY, "Bearer " + getToken());
                super.start(responseListener, headers);
            }
        };
    }
 
    private String getToken() {
        // Get token from security context, cache, etc.
        return "your-jwt-token-here";
    }
}

# đọc user info trong service

@GrpcService
public class OrderGrpcService extends OrderServiceGrpc.OrderServiceImplBase {
 
    @Override
    public void createOrder(CreateOrderRequest request,
                            StreamObserver<OrderResponse> responseObserver) {
 
        // Đọc userId từ Context (được set bởi AuthInterceptor)
        String userId = AuthInterceptor.USER_ID_KEY.get();
        System.out.println("Request from user: " + userId);
 
        // ... business logic
    }
}

# streaming trong thực tế

# real-world example: live order dashboard

Giả sử bạn đang làm dashboard cho operations team, hiển thị real-time order updates. Đây là chỗ bidirectional streaming phát huy (code dưới là bản rút gọn: filter nhận về chưa được dùng để lọc event):

service DashboardService {
  // Operator subscribe theo dõi orders, server push updates real-time
  rpc WatchOrders (stream WatchFilter) returns (stream DashboardEvent);
}
 
message WatchFilter {
  string region = 1;           // "us-east", "eu-west"
  OrderStatus status = 2;      // Chỉ theo dõi status cụ thể
  bool include_metrics = 3;    // Có gửi kèm metrics không
}
 
message DashboardEvent {
  oneof event {
    OrderCreated order_created = 1;
    OrderUpdated order_updated = 2;
    MetricsSnapshot metrics = 3;
  }
  string timestamp = 10;
}
@GrpcService
public class DashboardGrpcService
        extends DashboardServiceGrpc.DashboardServiceImplBase {
 
    // Quản lý active subscribers
    private final Set<StreamObserver<DashboardEvent>> subscribers =
        ConcurrentHashMap.newKeySet();
 
    @Override
    public StreamObserver<WatchFilter> watchOrders(
            StreamObserver<DashboardEvent> responseObserver) {
 
        // Thêm subscriber
        subscribers.add(responseObserver);
 
        return new StreamObserver<>() {
            private String currentRegion = "";
 
            @Override
            public void onNext(WatchFilter filter) {
                // Client thay đổi filter — ví dụ chuyển từ "us-east"
                // sang "eu-west" mà KHÔNG cần reconnect
                currentRegion = filter.getRegion();
                System.out.println("Filter updated: region=" + currentRegion);
            }
 
            @Override
            public void onError(Throwable t) {
                subscribers.remove(responseObserver);
            }
 
            @Override
            public void onCompleted() {
                subscribers.remove(responseObserver);
                responseObserver.onCompleted();
            }
        };
    }
 
    // Được gọi khi có order mới (từ event bus, Kafka, etc.)
    public void broadcastOrderCreated(OrderCreated event) {
        DashboardEvent dashboardEvent = DashboardEvent.newBuilder()
            .setOrderCreated(event)
            .setTimestamp(Instant.now().toString())
            .build();
 
        // Fan-out tới tất cả subscribers.
        // StreamObserver KHÔNG thread-safe: nếu broadcast được gọi từ nhiều thread
        // (vd nhiều Kafka consumer) thì phải đồng bộ theo từng subscriber.
        subscribers.forEach(subscriber -> {
            try {
                synchronized (subscriber) {
                    subscriber.onNext(dashboardEvent);
                }
            } catch (Exception e) {
                subscribers.remove(subscriber);
            }
        });
    }
}

# flow control & backpressure

Code fan-out ở trên còn một vấn đề: onNext() không chặn khi client đọc chậm, message cứ dồn vào buffer trong bộ nhớ server. Một client mạng yếu có thể làm phình heap. Khi server gửi data nhanh hơn client xử lý, gRPC có flow control của HTTP/2 ở bên dưới, và grpc-java cho bạn biết lúc nào nên dừng gửi qua isReady():

// Server-side: respect backpressure
@Override
public void listOrders(ListOrdersRequest request,
                       StreamObserver<OrderResponse> responseObserver) {
 
    // Cast để access flow control methods
    ServerCallStreamObserver<OrderResponse> serverObserver =
        (ServerCallStreamObserver<OrderResponse>) responseObserver;
 
    // Handler có thể được gọi lại sau khi đã gửi xong, nên cần cờ để
    // không gọi onCompleted() hai lần (lần hai ném IllegalStateException)
    AtomicBoolean done = new AtomicBoolean(false);
 
    // Callback khi client sẵn sàng nhận thêm data
    serverObserver.setOnReadyHandler(() -> {
        while (!done.get() && serverObserver.isReady()) {
            OrderResponse order = getNextOrder(); // từ DB cursor
            if (order == null) {
                done.set(true);
                serverObserver.onCompleted();
                return;
            }
            serverObserver.onNext(order);
        }
        // Nếu client chưa ready, dừng gửi — tự động resume khi ready
    });
}

Làm như vậy thì server chỉ đọc từ DB khi client theo kịp, không bị OOM (Out of Memory) khi stream tập dữ liệu lớn. Lưu ý flow control không tự có: nếu bạn bỏ qua isReady() và cứ onNext() liên tục như ví dụ listOrders ở trên, grpc-java vẫn nhận và buffer hết trong bộ nhớ.

# gRPC trong microservices: bài học từ production

# service mesh integration

Trong production, gRPC thường chạy cùng service mesh (Istio, Linkerd):

grpc-microservice

# health checking

gRPC có chuẩn health checking riêng:

// grpc-spring-boot-starter tự động register health service chuẩn
// Client hoặc load balancer có thể check:
// grpc.health.v1.Health/Check
// Lưu ý: HealthIndicator của Actuator dưới đây chỉ hiện ở /actuator/health;
// muốn nó quyết định luôn trạng thái gRPC health thì phải cấu hình thêm
// (vd grpc.server.health-service.type=ACTUATOR ở các bản starter mới)
 
// Custom health indicator
@Component
public class OrderServiceHealthIndicator
        extends AbstractHealthIndicator {
 
    @Override
    protected void doHealthCheck(Health.Builder builder) {
        // Check database connection, dependencies, etc.
        if (isDatabaseHealthy()) {
            builder.up().withDetail("database", "connected");
        } else {
            builder.down().withDetail("database", "disconnected");
        }
    }
}

# load balancing

gRPC hỗ trợ 2 kiểu load balancing:

1. Proxy-based (L7): Envoy, Nginx, HAProxy đứng giữa client và server

Client → [Load Balancer] → Server 1
                          → Server 2
                          → Server 3

2. Client-side: Client tự quyết định gửi request đến server nào

// Client-side load balancing với round-robin
ManagedChannel channel = ManagedChannelBuilder
    .forTarget("dns:///order-service:9090")
    .defaultLoadBalancingPolicy("round_robin")
    .usePlaintext()
    .build();

💡 Lưu ý quan trọng: HTTP/2 multiplexing có thể gây vấn đề với L4 load balancer (TCP level) vì tất cả requests đi qua 1 connection, nên dồn hết vào một server. Bạn cần L7 load balancer hoặc client-side load balancing. Trên Kubernetes, ClusterIP Service là L4, nên client-side LB với dns:/// phải trỏ tới headless Service (clusterIP: None) để DNS trả về IP của từng pod; client chỉ resolve lại DNS khi connection lỗi, nên pod mới scale lên có thể lâu mới nhận traffic (đặt max-connection-age ở server để client kết nối lại định kỳ).

# retry policy

// Cấu hình retry trong application.yml
// hoặc programmatically:
Map<String, Object> retryPolicy = Map.of(
    "maxAttempts", 3.0,
    "initialBackoff", "0.5s",
    "maxBackoff", "30s",
    "backoffMultiplier", 2.0,
    // Chỉ retry lỗi chắc chắn là tạm thời. DEADLINE_EXCEEDED thường không nên:
    // deadline áp cho cả lời gọi (mọi lần thử), và request có thể đã chạy xong ở server
    "retryableStatusCodes", List.of("UNAVAILABLE")
);
 
Map<String, Object> serviceConfig = Map.of(
    "methodConfig", List.of(Map.of(
        "name", List.of(Map.of(
            "service", "com.example.order.OrderService"
        )),
        "retryPolicy", retryPolicy
    ))
);
 
ManagedChannel channel = ManagedChannelBuilder
    .forTarget("order-service:9090")
    .defaultServiceConfig(serviceConfig)
    .enableRetry()
    .build();

# observability: metrics & tracing

// Tích hợp với Micrometer (Spring Boot Actuator)
// và OpenTelemetry cho distributed tracing
 
@Configuration
public class GrpcObservabilityConfig {
 
    @Bean
    @GrpcGlobalServerInterceptor // là annotation, không phải kiểu trả về
    public ServerInterceptor metricsInterceptor(
            MeterRegistry meterRegistry) {
        return new ServerInterceptor() {
            @Override
            public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
                    ServerCall<ReqT, RespT> call,
                    Metadata headers,
                    ServerCallHandler<ReqT, RespT> next) {
 
                String method = call.getMethodDescriptor().getBareMethodName();
                Timer.Sample sample = Timer.start(meterRegistry);
 
                return next.startCall(
                    new ForwardingServerCall.SimpleForwardingServerCall<>(call) {
                        @Override
                        public void close(Status status, Metadata trailers) {
                            sample.stop(Timer.builder("grpc.server.calls")
                                .tag("method", method)
                                .tag("status", status.getCode().name())
                                .register(meterRegistry));
                            super.close(status, trailers);
                        }
                    }, headers);
            }
        };
    }
}

Viết tay như trên để hiểu cơ chế thôi. Có Actuator và Micrometer trong classpath thì starter đã tự gắn interceptor đo metrics cho cả server lẫn client (vd grpc.server.call.duration), và tracing thì nên dùng instrumentation có sẵn của Micrometer Tracing hoặc OpenTelemetry thay vì tự viết.


# khi nào KHÔNG nên dùng gRPC

gRPC không phải silver bullet. Những trường hợp nên cân nhắc:

# public-facing APIs cho browser

JavaScript trong browser không điều khiển được HTTP/2 ở mức gRPC cần (frame, trailers), nên không gọi gRPC trực tiếp được. Bạn cần gRPC-Web cộng proxy (Envoy) làm trung gian, hoặc dùng giao thức tương thích như Connect, thêm một lớp phức tạp.

→ Dùng REST hoặc GraphQL cho browser clients.

# simple CRUD applications

Nếu app chỉ là CRUD đơn giản với vài endpoints, overhead của protobuf compilation, code generation, và learning curve không đáng.

→ Dùng REST: đơn giản, ecosystem lớn, ai cũng biết.

# khi cần human-readable payloads

Debug gRPC khó hơn REST vì payload là binary. Bạn không thể curl một gRPC endpoint (cần grpcurl hoặc Postman gRPC). Logs cũng khó đọc hơn.

→ Nếu team cần debug nhanh bằng curl/Postman, REST dễ hơn nhiều.

# khi team chưa sẵn sàng

gRPC có learning curve. Quản lý proto file, code generation pipeline, streaming patterns, cái nào cũng cần thời gian học. Nếu team nhỏ và deadline gấp, đừng ép.

# ✅ khi NÊN dùng gRPC

  • Service-to-service communication trong microservices
  • Low-latency requirements (trading, gaming, real-time)
  • Polyglot environments (Java service gọi Go service gọi Python service)
  • Streaming data (live feeds, IoT, monitoring)
  • Mobile backends (bandwidth-sensitive, protobuf nhỏ hơn JSON)
  • High-throughput systems (protobuf serialize nhanh và gọn hơn JSON)

# kết luận

gRPC không thay thế REST, nó chỉ là một công cụ khác trong toolbox. Thường gRPC dùng cho phần lớn internal service communication, còn external consumers vẫn gọi REST APIs.

Nếu bạn đang làm microservices và cần:

  • Performance tốt hơn JSON/REST
  • Type-safe contracts giữa các services
  • Streaming capabilities
  • Polyglot support

...thì gRPC là lựa chọn đáng cân nhắc.

Quick Start Checklist

□ Cài đặt protoc compiler
□ Setup Maven/Gradle plugin cho code generation
□ Viết .proto file đầu tiên
□ Implement server với @GrpcService
□ Test bằng grpcurl, Postman hoặc Kreya
□ Thêm interceptors cho logging & auth
□ Set deadline cho mọi client call
□ Cấu hình health checking
□ Monitor với metrics & tracing

Tài liệu tham khảo

Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.