gRPC #

Ketika ratusan microservice saling berkomunikasi ribuan kali per detik, overhead kecil di setiap request terakumulasi menjadi masalah besar. JSON parsing yang lambat, header HTTP yang berulang, connection setup yang mahal — semua ini yang mendorong Google membangun gRPC. Tapi gRPC bukan sekadar “REST yang lebih cepat” — ia adalah paradigma komunikasi yang berbeda: contract-first, binary, streaming-native, dan dirancang dari awal untuk service-to-service communication di lingkungan internal. Artikel ini membahas gRPC dari dasar hingga production-ready: cara kerja Protocol Buffers, empat pola streaming, backward compatibility, error handling, interceptor, dan kapan justru kamu sebaiknya tetap pakai REST.

Mengapa gRPC Ada #

Google menjalankan miliaran request per hari di antara ribuan internal service. Sejak awal 2000-an, mereka membangun sistem RPC internal bernama Stubby yang sudah menangani skala tersebut selama lebih dari satu dekade. Ketika container dan microservices mulai populer di industri, Google memutuskan untuk merilis versi open-source dari Stubby — itulah gRPC, dirilis ke publik pada 2015.

Masalah yang gRPC dirancang untuk selesaikan:

Masalah REST untuk komunikasi internal:

1. Overhead JSON parsing
   REST: JSON parsing membutuhkan ~5–10x lebih lama dari Protobuf deserialization
   Untuk service yang memanggil satu sama lain ratusan kali per detik,
   ini menjadi bottleneck yang nyata

2. HTTP/1.1 connection overhead
   REST: setiap request membuka koneksi baru (atau menunggu koneksi available)
   HTTP/2 (gRPC): satu koneksi, banyak request concurrent (multiplexing)

3. Tidak ada contract yang dipaksakan
   REST: documentation bisa tidak sinkron dengan implementasi
   gRPC: .proto file adalah kontrak yang dikompilasi — mismatch langsung error

4. Streaming tidak natural
   REST: polling atau WebSocket yang terasa seperti hack
   gRPC: streaming adalah first-class citizen

Dua Fondasi gRPC: HTTP/2 dan Protocol Buffers #

HTTP/2 — Lebih dari Sekadar Upgrade #

flowchart LR
    subgraph HTTP1["HTTP/1.1"]
        direction TB
        C1["Client"]
        S1["Server"]
        C1 -->|"Request 1"| S1
        S1 -->|"Response 1"| C1
        C1 -->|"Request 2 — tunggu dulu"| S1
        S1 -->|"Response 2"| C1
    end

    subgraph HTTP2["HTTP/2 gRPC"]
        direction TB
        C2["Client"]
        S2["Server"]
        C2 -->|"Stream 1: Request A"| S2
        C2 -->|"Stream 2: Request B"| S2
        C2 -->|"Stream 3: Request C"| S2
        S2 -->|"Response A, B, C concurrent"| C2
    end

HTTP/2 memberikan tiga keunggulan utama untuk gRPC: multiplexing (banyak request dalam satu TCP connection tanpa head-of-line blocking), header compression dengan HPACK (header yang berulang dikompresi), dan fondasi untuk native streaming.

Protocol Buffers — Kontrak yang Dikompilasi #

Protocol Buffers (Protobuf) adalah format serialisasi binary yang menjadi bahasa definisi kontrak gRPC. Tidak seperti JSON yang human-readable tapi verbose, Protobuf dirancang untuk efisiensi mesin.

// user_service.proto
syntax = "proto3";

package user.v1;

// Definisi service — ini yang menghasilkan stub client dan server
service UserService {
  // Unary RPC
  rpc GetUser(GetUserRequest) returns (UserResponse);

  // Server streaming — server kirim banyak response untuk satu request
  rpc ListUsers(ListUsersRequest) returns (stream UserResponse);

  // Client streaming — client kirim banyak request, server balas satu
  rpc CreateUsers(stream CreateUserRequest) returns (CreateUsersResponse);

  // Bidirectional streaming
  rpc SyncUsers(stream SyncRequest) returns (stream SyncResponse);
}

message GetUserRequest {
  string id = 1;  // field number — JANGAN ubah setelah schema dipakai
}

message UserResponse {
  string id = 1;
  string name = 2;
  string email = 3;
  int64 created_at = 4;
  UserStatus status = 5;
}

enum UserStatus {
  USER_STATUS_UNSPECIFIED = 0;  // selalu ada nilai default 0
  USER_STATUS_ACTIVE = 1;
  USER_STATUS_SUSPENDED = 2;
}

Mengapa Protobuf lebih efisien dari JSON:

Perbandingan serialisasi data yang sama:

JSON (89 bytes, text):
{"id":"usr_123","name":"Budi Santoso","email":"[email protected]","created_at":1706352000}

Protobuf (~40 bytes, binary):
[field 1: "usr_123"][field 2: "Budi Santoso"][field 3: "[email protected]"][field 4: 1706352000]

Lebih kecil ~55%, deserialisasi ~5–7x lebih cepat.
Untuk 1 juta request per hari: hemat ~50 MB transfer + CPU overhead.

Empat Pola Komunikasi gRPC #

flowchart TD
    subgraph Unary["Unary RPC"]
        UC["Client"] -->|"1 Request"| US["Server"]
        US -->|"1 Response"| UC
    end

    subgraph SS["Server Streaming"]
        SC["Client"] -->|"1 Request"| SSS["Server"]
        SSS -->|"Response 1... 2... 3..."| SC
    end

    subgraph CS["Client Streaming"]
        CC["Client"] -->|"Request 1... 2... 3..."| CSS["Server"]
        CSS -->|"1 Response saat semua diterima"| CC
    end

    subgraph Bi["Bidirectional Streaming"]
        BC["Client"] <-->|"Requests & Responses\nberjalan bersamaan"| BS["Server"]
    end

Unary adalah yang paling umum — seperti REST, tapi binary. Cocok untuk CRUD biasa, operasi yang hasilnya tunggal, mayoritas use case service-to-service.

Server Streaming cocok untuk export data besar (kirim chunk per chunk, tidak load semua ke memory), live feed atau progress update, pencarian yang hasilnya banyak.

// Contoh server streaming untuk export
rpc ExportTransactions(ExportRequest) returns (stream TransactionRecord);
// Server kirim ribuan transaksi satu per satu
// Client mulai proses sambil menerima — lebih efisien dari menunggu semua dulu

Client Streaming cocok untuk upload file besar dalam chunks, bulk insert atau batch processing, agregasi data yang dikirim secara incremental.

Bidirectional Streaming untuk real-time communication (chat, live collaboration), sinkronisasi state yang terus berjalan antara dua service, sistem yang perlu negosiasi dinamis.


Backward Compatibility Protobuf — Aturan yang Tidak Boleh Dilanggar #

Ini adalah aspek paling kritis dalam mengelola gRPC di production. Field number adalah identitas field di Protobuf — ia yang menentukan bagaimana binary data di-decode, bukan nama field.

// ✓ AMAN: Menambahkan field baru dengan field number baru
message UserResponse {
  string id = 1;
  string name = 2;
  string email = 3;
  string phone_number = 4;  // baru — client lama mengabaikan field ini
  UserStatus status = 5;    // baru — client lama mendapat default value
}

// ✗ BREAKING: Menukar atau mengubah field number
message UserResponse {
  string id = 1;
  string email = 2;   // BAHAYA: field number 2 sebelumnya adalah "name"
  string name = 3;    // BAHAYA: client lama decode field 2 sebagai "name"
}                     //         padahal sekarang isinya "email"

// ✗ BREAKING: Mengubah tipe data yang tidak compatible
message UserResponse {
  int32 id = 1;   // BAHAYA: berubah dari string ke int32
}

// ✓ BENAR: Menghapus field dengan cara yang aman
message UserResponse {
  string id = 1;
  reserved 2;         // field number 2 tidak boleh dipakai lagi
  reserved "name";    // nama "name" tidak boleh dipakai lagi
  string email = 3;
}
Ringkasan aturan:

SELALU AMAN:
  ✓ Menambahkan field baru dengan field number baru yang belum pernah dipakai
  ✓ Menghapus field (dengan menandainya reserved)
  ✓ Mengubah nama field (field number yang penting, bukan nama)

TIDAK PERNAH BOLEH:
  ✗ Mengubah atau reuse field number yang sudah ada
  ✗ Mengubah tipe data field ke tipe yang tidak compatible
  ✗ Mengubah field dari singular ke repeated atau sebaliknya

Error Handling di gRPC #

gRPC tidak menggunakan HTTP status code seperti REST. Ia memiliki sistem status code sendiri yang lebih spesifik untuk RPC.

gRPC Status Codes yang paling umum:

OK (0)               → Sukses
CANCELLED (1)        → Request dibatalkan oleh client (deadline terlampaui)
INVALID_ARGUMENT (3) → Input tidak valid (seperti 400 di REST)
DEADLINE_EXCEEDED (4)→ Deadline habis sebelum operasi selesai
NOT_FOUND (5)        → Resource tidak ditemukan (seperti 404 di REST)
ALREADY_EXISTS (6)   → Resource sudah ada (seperti 409 di REST)
PERMISSION_DENIED (7)→ Tidak punya izin (seperti 403 di REST)
RESOURCE_EXHAUSTED(8)→ Rate limit atau quota habis (seperti 429 di REST)
INTERNAL (13)        → Error internal server (seperti 500 di REST)
UNAVAILABLE (14)     → Service tidak tersedia (seperti 503 di REST)
UNAUTHENTICATED (16) → Belum autentikasi (seperti 401 di REST)
// BENAR: Gunakan gRPC status code yang tepat
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserResponse, error) {
    user, err := s.db.FindUser(req.Id)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return nil, status.Errorf(codes.NotFound,
                "user dengan id %s tidak ditemukan", req.Id)
        }
        return nil, status.Errorf(codes.Internal, "internal error: %v", err)
    }
    return toProto(user), nil
}

// ANTI-PATTERN: Return raw Go error → menghasilkan UNKNOWN status code
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserResponse, error) {
    user, err := s.db.FindUser(req.Id)
    if err != nil {
        return nil, err  // client hanya dapat UNKNOWN — tidak informatif
    }
    return toProto(user), nil
}

Deadline dan Timeout — Wajib di Setiap Call #

Tanpa deadline, satu service yang lambat bisa menyebabkan cascade failure ke seluruh sistem melalui thread pool exhaustion.

sequenceDiagram
    participant A as Service A
    participant B as Service B
    participant C as Service C

    Note over A,C: Tanpa deadline — cascade failure
    A->>B: GetOrder (no deadline)
    B->>C: GetInventory (no deadline)
    Note over C: C stuck atau lambat
    C--xB: Tidak ada response
    Note over B: B menunggu selamanya, thread pool habis
    B--xA: Tidak ada response
    Note over A: A juga tidak responsif

    Note over A,C: Dengan deadline — fail fast
    A->>B: GetOrder (deadline: 500ms)
    B->>C: GetInventory (deadline: 300ms dari sisa waktu)
    Note over C: C timeout setelah 300ms
    B-->>A: DEADLINE_EXCEEDED — cepat, tidak cascade
// BENAR: Selalu set deadline
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
resp, err := client.GetUser(ctx, req)

// BENAR: Propagate context — jangan buat context baru di tengah request chain
func (s *orderServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.OrderResponse, error) {
    // Gunakan ctx yang sama (sudah ada deadline dari upstream)
    inventory, err := s.inventoryClient.GetInventory(ctx, &pb.GetInventoryRequest{
        ProductId: req.ProductId,
    })
    // ...
}
Jangan pernah membuat context.Background() baru di tengah request chain untuk menghindari deadline propagation. Ini mengalahkan tujuan deadline sepenuhnya — downstream service tidak akan tahu bahwa upstream sudah timeout dan tetap memproses request yang hasilnya tidak akan pernah digunakan.

Interceptor — Cross-Cutting Concerns #

Interceptor di gRPC adalah equivalent dari middleware di HTTP framework. Ia memungkinkan kamu menambahkan logic yang berlaku untuk semua RPC call tanpa mengubah setiap handler.

// Unary server interceptor — contoh untuk logging
func loggingInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    start := time.Now()

    resp, err := handler(ctx, req)  // panggil handler yang sebenarnya

    duration := time.Since(start)
    if err != nil {
        st, _ := status.FromError(err)
        log.Printf("gRPC %s | %s | %v", info.FullMethod, st.Code(), duration)
    } else {
        log.Printf("gRPC %s | OK | %v", info.FullMethod, duration)
    }
    return resp, err
}

// Daftarkan multiple interceptor saat membuat server
server := grpc.NewServer(
    grpc.ChainUnaryInterceptor(
        recoveryInterceptor,  // tangani panic
        authInterceptor,      // validasi token
        loggingInterceptor,   // log setiap call
        metricsInterceptor,   // prometheus metrics
        tracingInterceptor,   // OpenTelemetry tracing
    ),
)

Interceptor yang hampir selalu perlu ada di production: recovery (handle panic), auth (validasi token), logging (method + status + duration), metrics (counter + histogram latency), dan tracing (propagate trace context).


Mengekspos gRPC ke Consumer External #

Browser tidak mendukung HTTP/2 raw yang dibutuhkan gRPC. Ada dua pendekatan umum:

flowchart LR
    Browser["Browser"]
    Mobile["Mobile App\n(bisa gRPC native)"]
    GW["API Gateway\n(REST → gRPC transcoding)"]
    GWP["gRPC-Web Proxy\n(Envoy)"]
    Svc["Backend Service\n(gRPC native)"]

    Browser -->|"REST API"| GW
    Browser -->|"gRPC-Web"| GWP
    Mobile -->|"gRPC native"| Svc
    GW -->|"gRPC"| Svc
    GWP -->|"gRPC"| Svc

HTTP Transcoding adalah pendekatan yang paling pragmatis: expose REST endpoint di API gateway yang secara internal berkomunikasi ke backend via gRPC. Consumer menggunakan REST biasa, backend tetap gRPC.

// Annotation untuk auto-generate REST endpoint dari proto
import "google/api/annotations.proto";

service UserService {
  rpc GetUser(GetUserRequest) returns (UserResponse) {
    option (google.api.http) = {
      get: "/v1/users/{id}"  // REST endpoint yang di-generate otomatis
    };
  }
}

Anti-Pattern gRPC yang Harus Dihindari #

// ✗ Anti-pattern 1: Reuse field number yang sudah dihapus
message UserRequest {
  string id = 1;
  // old_name = 2; ← dihapus
  string new_field = 2;  // SALAH — field number 2 sudah dipakai sebelumnya
}

// ✓ Solusi: Reserved field number
message UserRequest {
  string id = 1;
  reserved 2;
  reserved "old_name";
  string new_field = 3;
}
// ✗ Anti-pattern 2: Tidak ada deadline
resp, err := client.GetUser(context.Background(), req)

// ✓ Solusi: Selalu set deadline
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, req)
// ✗ Anti-pattern 3: Ekspos gRPC langsung ke internet
Internet → gRPC Service (langsung)
→ Browser tidak support gRPC native
→ Tidak ada auth layer yang proper
→ Binary protocol sulit di-debug consumer

// ✓ Solusi: API Gateway di depan
Internet → API Gateway (REST/GraphQL) → gRPC Services (internal)
// ✗ Anti-pattern 4: Proto schema yang terlalu generik
message DoAnythingRequest {
  string action = 1;
  string entity_id = 2;
  bool flag1 = 3;
  bool flag2 = 4;
  // ... 20 field lagi
}

// ✓ Solusi: RPC yang spesifik per use case
rpc GetUserProfile(GetUserProfileRequest) returns (UserProfile);
rpc GetUserOrders(GetUserOrdersRequest) returns (UserOrdersResponse);
Untuk debugging gRPC yang binary, gunakan tools seperti grpcurl (seperti curl tapi untuk gRPC), grpcui (GraphiQL-like UI untuk gRPC), atau Postman yang sudah support gRPC. Tanpa tooling ini, debugging request/response gRPC di production jauh lebih sulit dari REST.

Checklist gRPC yang Siap Production #

PROTO DESIGN:
  □ Package name menggunakan versioning (user.v1, order.v1)
  □ Field number tidak pernah diubah atau di-reuse
  □ Field yang dihapus ditandai reserved (number dan nama)
  □ Enum selalu dimulai dengan nilai 0 yang berarti UNSPECIFIED
  □ Input message dan output message dipisahkan (tidak shared)

ERROR HANDLING:
  □ Semua error menggunakan gRPC status code yang tepat
  □ Raw Go/Java/Python error tidak di-return langsung
  □ Error message informatif tapi tidak mengekspos detail internal

DEADLINE DAN TIMEOUT:
  □ Setiap RPC call dari client memiliki deadline
  □ Deadline dipropagasi ke downstream calls — tidak membuat context baru
  □ Timeout dikalibrasi sesuai SLA yang diharapkan

INTERCEPTOR:
  □ Recovery interceptor menangani panic
  □ Auth interceptor memvalidasi semua request yang perlu autentikasi
  □ Logging interceptor mencatat method, status code, dan duration
  □ Metrics interceptor mengirim data ke Prometheus
  □ Tracing interceptor propagate trace context (OpenTelemetry)

DEPLOYMENT:
  □ gRPC tidak diekspos langsung ke internet
  □ API Gateway atau gRPC-Web digunakan untuk consumer external
  □ TLS wajib di production (mTLS untuk service-to-service sensitif)
  □ Health check endpoint diimplementasikan

TOOLING:
  □ grpcurl atau grpcui tersedia untuk debugging
  □ Proto files di-version control bersama code
  □ Code generation otomatis sebagai bagian dari build process

Ringkasan #

  • gRPC bukan REST yang lebih cepat — ia paradigma yang berbeda — contract-first, binary, dan dirancang spesifik untuk service-to-service communication di lingkungan internal.
  • Protocol Buffers adalah kontrak yang dikompilasi — field number tidak boleh pernah diubah atau di-reuse. Field yang dihapus harus ditandai reserved. Ini adalah aturan paling kritis.
  • HTTP/2 multiplexing menghilangkan head-of-line blocking — ratusan RPC concurrent dalam satu TCP connection, dengan header compression yang mengurangi overhead secara signifikan.
  • Empat pola streaming untuk empat use case berbeda — Unary untuk CRUD biasa, Server Streaming untuk data export/live feed, Client Streaming untuk upload/bulk, Bidirectional untuk real-time sync.
  • Deadline wajib di setiap RPC call dan harus dipropagasi — tanpa deadline, satu service yang lambat bisa cascade failure ke seluruh sistem. Jangan buat context.Background() baru di tengah request chain.
  • Interceptor untuk semua cross-cutting concerns — recovery, auth, logging, metrics, dan tracing harus ada sebagai interceptor, bukan diimplementasikan ulang di setiap handler.
  • Error handling menggunakan gRPC status code — return codes.NotFound, codes.InvalidArgument, bukan raw error yang menghasilkan UNKNOWN status code.
  • gRPC tidak cocok untuk public API yang dikonsumsi browser — gunakan API Gateway dengan REST atau GraphQL di depan, gRPC hanya untuk internal communication.
  • Versioning di level package Protobufuser.v1user.v2 untuk breaking changes, backward-compatible changes untuk perubahan additive tanpa version bump.
  • Observability lebih challenging dari REST — binary protocol tidak bisa di-inspect dengan curl. grpcurl, distributed tracing, dan structured logging dengan trace ID adalah kebutuhan minimum.

← Sebelumnya: GraphQL   Berikutnya: JWT →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact