Classic gRPC Interview Questions with Answers – Must‑Save for Your Prep

This article presents eight self‑test gRPC interview questions covering performance, contract, cross‑language support, streaming, HTTP/2 fundamentals, Protobuf field tags, call patterns, retryable status codes, load‑balancing policies, interceptor usage, and Spring Boot 3 integration, each accompanied by concise reference answers.

CodeSmart Hoops
CodeSmart Hoops
CodeSmart Hoops
Classic gRPC Interview Questions with Answers – Must‑Save for Your Prep

Q1: Differences between gRPC and REST+JSON

Performance

REST+JSON: text serialization + HTTP/1.1 single request per connection.

gRPC: binary Protobuf + HTTP/2 multiplexing.

Root cause: Protobuf replaces field names with numeric tags, reducing payload to ~1/3 of JSON; HTTP/2 multiplexes thousands of RPCs on a single TCP connection, making connection cost near‑zero.

Contract

REST+JSON: Swagger docs are added after implementation and can become stale.

gRPC: .proto IDL is the single source of truth; code generators produce client/server stubs for all languages.

Root cause: Field name, type and number are fixed in the proto; mismatches cause compile‑time errors instead of runtime NPEs.

Cross‑language support

REST+JSON: works well but requires hand‑written clients for each language.

gRPC: one proto file feeds language‑specific protoc plugins; native support for C++, Java, Go, Python, JavaScript and many others.

Streaming

REST+JSON: hard, relies on SSE/WebSocket work‑arounds.

gRPC: native four modes (unary, server‑streaming, client‑streaming, bidirectional).

Root cause: gRPC builds directly on HTTP/2 streams, no extra protocol needed.

Why gRPC wins : it redesigns contract (IDL‑first), serialization (binary field tags) and transport (HTTP/2 multiplexing). The three improvements combine to give a clear advantage over REST.

Q2: HTTP/2 multiplexing, streams and HPACK and their significance for gRPC

Multiplexing : a single TCP connection carries countless concurrent requests without head‑of‑line blocking. HTTP/2 splits data into frames, groups them into streams, and reassembles them on the receiver side. For gRPC this means one long‑lived TCP connection per client‑server pair, eliminating per‑request connection overhead.

Stream : each RPC maps to a stream identified by a 32‑bit Stream ID (odd for client‑initiated, even for server‑initiated). An RPC is encoded as HEADERS (e.g., :path, grpc-timeout, metadata) + DATA (length‑prefixed Protobuf message) + trailing HEADERS (e.g., grpc-status).

HPACK header compression : uses a static table of common headers (e.g., :method: POST) and a dynamic table of previously sent headers. gRPC metadata travels in HEADERS frames, so repeated entries like te: trailers and content-type: application/grpc incur virtually zero overhead.

One TCP connection can handle thousands of concurrent RPCs, reducing connection count from “one per instance” to “one per instance”.

gRPC status is carried in grpc-status trailers; HTTP status remains 200 even on errors.

Streaming RPCs map naturally to HTTP/2 streams, avoiding SSE/WebSocket work‑arounds.

Q3: Protobuf field tag, immutability of field numbers and deletion handling

Field tag definition : tag = (field_number << 3) | wire_type Example for user_id = 123 (field 1, varint):

tag = (1 << 3) | 0 = 0x08
value = 0x7B
→ bytes: 08 7B  (2 bytes vs. JSON "userId":123 ≈ 11 bytes)

The field number uniquely identifies the wire format; changing it changes the wire format and breaks backward compatibility. If a service changes field 1 from email to phone, old clients will deserialize the old bytes as phone, causing type mismatches.

Deleting a field should be done by reserving the number and name:

message User {
  string name = 1;
  reserved 2;               // lock number 2
  reserved "email";        // lock field name
  int64 phone = 3;          // new field uses a new number
}

Compatibility summary:

Add field (new number) – compatible.

Delete field (keep reserved) – compatible.

Modify field number – incompatible.

Change field type (most cases) – incompatible.

optional ↔ required – incompatible.

Q4: gRPC call modes and typical business scenarios

Unary – normal request, normal response. Typical: risk‑scoring service receives userId and returns a score. rpc Predict (Req) returns (Resp); Server Streaming – normal request, stream response. Typical: model inference returns tokens as they are generated (ChatGPT‑style) or log push. rpc WatchLog (Req) returns (stream Event); Client Streaming – stream request, normal response. Typical: batch metric reporting, aggregation, file chunk upload. rpc Upload (stream Metric) returns (Summary); Bidirectional Streaming – stream request, stream response. Typical: chat, real‑time collaborative editing, bidirectional inference. rpc Chat (stream Msg) returns (stream Msg); Server‑side implementation returns a StreamObserver<ReqT> for client‑stream methods and uses onNext/onError/onCompleted to process incoming messages, then sends responses via a StreamObserver<RespT>.

Q5: Retryable gRPC status codes and why INVALID_ARGUMENT is excluded from circuit‑breaker metrics

Client‑retryable status codes (typically transient) : UNAVAILABLE (14) – service down / connection failure. Strategy: exponential back‑off. DEADLINE_EXCEEDED (4) – timeout. Strategy: back‑off then retry (avoid avalanche). RESOURCE_EXHAUSTED (8) – rate‑limit / quota exhausted. Strategy: delay then retry. ABORTED (10) – concurrent conflict (e.g., optimistic lock). Strategy: immediate retry. UNAUTHENTICATED (16) – token expired. Strategy: refresh token then retry.

Non‑retryable codes (deterministic errors) : INVALID_ARGUMENT (3) – client‑side parameter error; retry repeats the same failure. NOT_FOUND (5) – resource does not exist. ALREADY_EXISTS (6) – duplicate creation. PERMISSION_DENIED (7) – unauthorized. FAILED_PRECONDITION (9) – pre‑condition not met. OUT_OF_RANGE (11) – index out of bounds. UNIMPLEMENTED (12) – method not implemented. INTERNAL (13) – server bug (retry may work but usually not advised).

Why exclude INVALID_ARGUMENT from circuit‑breaker failure rates : the failure rate should reflect service health. INVALID_ARGUMENT indicates a client‑side mistake; counting it would (1) trigger a breaker for a healthy service, (2) block legitimate traffic after the breaker opens, and (3) pollute the metric with non‑service errors. Circuit‑breaker metrics should count only server‑side error codes such as UNAVAILABLE, DEADLINE_EXCEEDED, INTERNAL, UNKNOWN, etc.

Q6: Load‑balancing policies – pick_first vs round_robin

pick_first (default) :

Attempts addresses returned by NameResolver in order, selects the first reachable.

All RPCs are sent to that single instance; other instances stay idle.

Fails over to the next address only when the first instance disconnects.

Pros: minimal connections (only one), suitable for connection‑sensitive scenarios.

Cons: traffic concentrates on one instance, wasting capacity.

round_robin :

Maintains sub‑channels for all resolved instances; after all are READY, it round‑robin distributes calls.

Each RPC may hit a different instance, achieving balanced load.

Pros: higher resource utilization.

Cons: requires all instances to be READY; connection count equals instance count.

Imbalance example (NameResolver returns [A, B, C]):

pick_first connects to A → all traffic goes to A, B and C idle.
Only when A fails does it switch to B.
Result: single‑point service, wasted horizontal scaling.

Switching to round_robin distributes traffic across A, B, C.

Configuration change (Java) :

ManagedChannel c = ManagedChannelBuilder
    .forTarget("dns:///algo-service:9090")
    .defaultLoadBalancingPolicy("round_robin")
    .build();

Note: round_robin should be combined with health‑checking (gRPC health service) to filter out unhealthy instances; otherwise, any not‑READY instance skews distribution.

Q7: JWT authentication and tracing with gRPC interceptors, execution order

Server‑side JWT interceptor (simplified) :

public class AuthServerInterceptor implements ServerInterceptor {
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
        String token = headers.get(AUTH_TOKEN_KEY);
        if (token == null || !jwtVerifier.verify(token)) {
            call.close(Status.UNAUTHENTICATED.withDescription("token invalid"), new Metadata());
            return new ServerCall.Listener<>() {};
        }
        return next.startCall(call, headers);
    }
}

Client‑side tracing interceptor (simplified) :

public class TraceClientInterceptor implements ClientInterceptor {
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
            MethodDescriptor<ReqT, RespT> method, CallOptions options, Channel next) {
        return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, options)) {
            @Override
            public void start(Listener<RespT> responseListener, Metadata headers) {
                String traceId = MDC.get("traceId");
                if (traceId != null) {
                    headers.put(TRACE_ID_KEY, traceId);
                }
                super.start(responseListener, headers);
            }
        };
    }
}

Registration example :

// Client
ManagedChannel c = ManagedChannelBuilder.forAddress("algo", 9090)
    .intercept(new TraceClientInterceptor())
    .build();
// Server
Server s = ServerBuilder.forPort(9090)
    .addService(new AlgoServiceImpl())
    .intercept(new AuthServerInterceptor())
    .build();

Execution order (registration → execution):

Client interceptors: registration order A → B → C, actual execution C → B → A (last registered runs first, closest to the network).

Server interceptors: registration order A → B → C, actual execution A → B → C (first registered runs first, outermost layer).

Key points :

Metadata keys must be lower‑case because HTTP/2 headers are case‑insensitive and stored lower‑case.

Authentication interceptor should be the first server interceptor to reject invalid requests early.

Client interceptors execute in reverse registration order; place logging/tracing interceptors last (registered first) to ensure they wrap all others.

Q8: Integrating gRPC into a Spring Boot 3 application and why the Channel must be a process‑wide singleton

Key steps (using net.devh:grpc-spring-boot-starter ) :

Add dependencies: grpc-spring-boot-starter, protobuf-java, protobuf-maven-plugin, and the protoc / protoc-gen-grpc-java executables.

Configure protobuf-maven-plugin in pom.xml to generate Java code from .proto files.

<plugin>
    <groupId>org.xolstice.maven.plugins</groupId>
    <artifactId>protobuf-maven-plugin</artifactId>
    <configuration>
        <protocArtifact>com.google.protobuf:protoc:3.25.2:exe:${os.detected.classifier}</protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.63.0:exe:${os.detected.classifier}</pluginArtifact>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>compile-custom</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Place .proto files under src/main/proto/ and define messages and services.

Implement the server by extending AlgoServiceGrpc.AlgoServiceImplBase, annotate with @GrpcService, and set grpc.server.port in application.yml.

Inject the client stub with @GrpcClient("service-name") and configure grpc.client.service-name.address in application.yml.

In production, enable TLS ( grpc.server.security.enabled=true), set load‑balancing policy (e.g., round_robin) and configure call deadlines.

Why the Channel must be a singleton :

A Channel holds one or more long‑lived HTTP/2 TCP connections, an internal Netty EventLoop thread pool, NameResolver/LoadBalancer subsystems, and connection‑state machines.

Creating a new channel for every RPC incurs a TCP three‑way handshake, TLS handshake (hundreds of ms), new thread‑pool startup, and fresh NameResolver/LoadBalancer initialization – a performance disaster.

Correct usage (Spring bean) :

@Bean
public ManagedChannel algoChannel() {
    return ManagedChannelBuilder
        .forTarget("nacos:///algo-service")
        .defaultLoadBalancingPolicy("round_robin")
        .idleTimeout(5, TimeUnit.MINUTES)
        .build();
}

@Bean
public AlgoServiceGrpc.AlgoServiceBlockingStub algoStub(ManagedChannel c) {
    return AlgoServiceGrpc.newBlockingStub(c);
}

Graceful shutdown before process exit:

channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);

References

gRPC full analysis & practice – original article.

gRPC official documentation.

Protocol Buffers Language Guide (proto3).

HTTP/2 RFC 7540.

gRPC over HTTP/2 specification.

grpc-spring-boot-starter project.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaLoad BalancinggRPCProtobufSpring BootHTTP/2Interceptors
CodeSmart Hoops
Written by

CodeSmart Hoops

A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.