Back to home

gRPC and RPC: A Practical Guide for Backend Engineers

Zhenyu Wen
#gRPC#RPC#Protobuf#Microservices#Java#Python#Backend

gRPC and RPC: A Practical Guide for Backend Engineers

Understanding remote procedure calls, protocol buffers, and polyglot service communication


What is RPC?

RPC (Remote Procedure Call) is a communication model where you call a remote service as if it were a local function. Instead of thinking in terms of HTTP verbs and resource URLs, you think in terms of actions — GetUser(userId), TransferFunds(from, to, amount), ProcessOrder(orderId).

The caller feels like it's invoking a local method even though the call goes over the network. All the serialization, connection management, and network complexity is hidden behind what looks like a normal function call.


REST vs RPC: Different Mental Models

Both REST and RPC solve the same problem — communication between services — but with fundamentally different approaches.

REST is resource-oriented. You model your API around nouns and use HTTP verbs to express operations:

GET    /users/123
DELETE /orders/456
POST   /transfers

RPC is action-oriented. You model your API around verbs:

GetUser(userId)
DeleteOrder(orderId)
TransferFunds(from, to, amount)

REST can feel awkward when your operation doesn't map cleanly to CRUD. "What's the REST endpoint for transferring money between accounts?" — you end up with hacks like POST /transfers. RPC handles this naturally since you're just defining a function.

Dimension REST gRPC
Protocol HTTP/1.1 or HTTP/2 HTTP/2
Payload JSON (text) Protobuf (binary)
Performance Good Excellent
Type safety Optional (OpenAPI) Enforced (proto schema)
Browser support Native Requires grpc-web proxy
Best for Public APIs Internal microservices

A common pattern in practice: REST for public-facing APIs where accessibility matters, gRPC for internal service-to-service communication where performance and type safety matter.


gRPC: The Modern RPC Framework

gRPC is the dominant RPC framework today, originally built at Google. It uses HTTP/2 for transport and Protocol Buffers (protobuf) for serialization — binary encoding that is significantly more compact and faster to parse than JSON. It also supports streaming (server-side, client-side, bidirectional) which REST does not do natively.

The Proto File: Your API Contract

The .proto file is the heart of gRPC. It defines:

  • What operations exist (service methods)
  • What the inputs and outputs look like (message types)

It is the equivalent of an OpenAPI spec in the REST world, except it is executable — you feed it to the compiler and get working code in any language. The contract and implementation cannot drift apart.

syntax = "proto3";

service UserService {
  rpc GetUser (GetUserRequest) returns (UserResponse);
}

message GetUserRequest {
  int32 user_id = 1;
}

message UserResponse {
  int32 user_id = 1;
  string name    = 2;
  string email   = 3;
}

Code Generation

Running the protoc compiler against your proto generates two files (using Python as an example):

  • user_pb2.py — message classes, handles serialization/deserialization of your data types
  • user_pb2_grpc.py — service wiring: the client stub, the server base class, and the registration function
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. user.proto

These generated files are build artifacts — never edit them manually. When the proto changes, regenerate them.


Building a gRPC Service in Java

Java is one of the most common languages for gRPC due to its mature ecosystem and strong typing.

Maven Dependencies pom.xml

<dependencies>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-netty</artifactId>
        <version>1.59.0</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-protobuf</artifactId>
        <version>1.59.0</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-stub</artifactId>
        <version>1.59.0</version>
    </dependency>
</dependencies>

Maven handles running protoc automatically via the protobuf-maven-plugin — just run mvn generate-sources and the Java classes are generated under target/generated-sources.

Server Implementation

// Extend the generated base class — same concept as inheriting
// UserServiceServicer in Python
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {

    @Override
    public void getUser(GetUserRequest request,
                        StreamObserver<UserResponse> responseObserver) {
        // your business logic here
        UserResponse response = UserResponse.newBuilder()
                .setUserId(request.getUserId())
                .setName("Alice")
                .setEmail("alice@example.com")
                .build();

        responseObserver.onNext(response);   // send the response
        responseObserver.onCompleted();      // signal done
    }
}
// Register your implementation and start the server
Server server = ServerBuilder
        .forPort(50051)
        .addService(new UserServiceImpl())
        .build()
        .start();

server.awaitTermination();

Client

ManagedChannel channel = ManagedChannelBuilder
        .forAddress("localhost", 50051)
        .usePlaintext()
        .build();

// Create the stub — your client-side handle to the remote service
UserServiceGrpc.UserServiceBlockingStub stub =
        UserServiceGrpc.newBlockingStub(channel);

UserResponse response = stub.getUser(
    GetUserRequest.newBuilder().setUserId(123).build()
);

System.out.println("User: " + response.getName());
channel.shutdown();

What is a Stub?

A stub is the client-side proxy object that makes remote calls look like local method calls. When you call stub.getUser(request), the stub is silently doing all of this:

  1. Serializing your request object into binary protobuf
  2. Opening an HTTP/2 connection to the server
  3. Sending the bytes over the network
  4. Waiting for the binary response
  5. Deserializing it back into a UserResponse object
  6. Returning it to you

All the network complexity is hidden. From your code it looks like a plain function call.

Java gRPC generates three stub flavors:

  • BlockingStub — synchronous, the call blocks until the response arrives. Simple to use.
  • AsyncStub — non-blocking, you pass a callback. Better for high throughput.
  • FutureStub — returns a Future you can check later.

The same concept exists in all gRPC languages — Python calls it UserServiceStub, Go calls it NewUserServiceClient. Different names, same purpose: hide the network, make remote calls feel local.


Polyglot gRPC: One Proto, Many Languages

This is one of gRPC's most important properties — the server and client do not need to use the same language. The proto file is language-agnostic. You generate idiomatic client code for each language from the same source contract.

user.proto  →  protoc --java_out    →  Java client (Maven)
            →  protoc --go_out      →  Go client (Go modules)
            →  protoc --python_out  →  Python client (PyPI)

A Java server can serve a Go client and a Python client simultaneously. The wire protocol — binary protobuf over HTTP/2 — is the same regardless of language on either end.

Client Package Distribution Pattern

The server team owns the proto file and is responsible for publishing client packages for all supported languages:

  1. Server team defines and maintains user.proto
  2. CI/CD pipeline automatically generates client packages on every proto change
  3. Generated packages are published to the appropriate registry
  4. Client teams add a single dependency and call methods — no protoc required, no knowledge of gRPC internals needed
# Java client team
maven dependency: com.example:user-service-client:1.0

# Go client team
go get github.com/example/user-service-client

# Python client team
pip install user-service-client

This is the same model used at Amazon with Coral: the service team publishes a model package (the proto equivalent) and a generated client package. Consuming teams add a dependency and call functions — completely abstracted from the transport layer.


Testing gRPC Services

Unlike REST where you can use curl from the terminal, gRPC requires tools that understand the binary protocol:

  • grpcurl — the curl equivalent for gRPC
grpcurl -plaintext -d '{"user_id": 123}' localhost:50051 UserService/GetUser
  • grpcui — browser-based UI, similar to Postman but for gRPC
  • Postman — supports gRPC natively, import the proto file and call methods through the UI

Key Takeaways

  • RPC models communication as function calls rather than HTTP verbs and resource URLs — better for action-oriented operations that don't fit CRUD
  • The proto file is the API contract — language-agnostic, executable, and the single source of truth for both server and client
  • The stub is the client-side proxy that hides all network complexity behind what looks like a local function call
  • gRPC is polyglot by design — server and client can be different languages, both generated from the same proto file
  • The server team owns the proto and publishes generated client packages for each language — consumers just add a dependency
  • Use REST for public APIs where browser and third-party access matter; use gRPC for internal microservices where performance, type safety, and streaming matter

gRPC is the foundation of modern internal service communication at companies like Google, Netflix, and Amazon (via Coral). Understanding the proto-first workflow and the polyglot client distribution pattern is essential for platform and infrastructure engineers building at scale.