gRPC¶
What is gRPC?¶
High-performance RPC framework developed by Google, using HTTP/2 and Protocol Buffers.
Protocol Buffers¶
Basic Syntax¶
// order.proto
syntax = "proto3";
package ecommerce;
option java_package = "com.example.ecommerce";
option java_multiple_files = true;
import "google/protobuf/timestamp.proto";
// Message definitions
message Order {
string id = 1;
string customer_id = 2;
OrderStatus status = 3;
repeated OrderItem items = 4;
Money total = 5;
Address shipping_address = 6;
google.protobuf.Timestamp created_at = 7;
map<string, string> metadata = 8;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
Money unit_price = 3;
}
message Money {
int64 amount = 1; // Amount in cents
string currency = 2;
}
message Address {
string line1 = 1;
string line2 = 2;
string city = 3;
string state = 4;
string postal_code = 5;
string country = 6;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
ORDER_STATUS_CANCELLED = 5;
}
Field Types¶
// Scalar types
string name = 1;
int32 count = 2; // 32-bit signed
int64 big_count = 3; // 64-bit signed
uint32 positive = 4; // 32-bit unsigned
uint64 big_positive = 5;
float price = 6; // 32-bit float
double precise = 7; // 64-bit float
bool active = 8;
bytes data = 9; // Arbitrary bytes
// Collections
repeated string tags = 10; // List
map<string, int32> scores = 11; // Map
// Optional (proto3 - all fields optional by default)
optional string nickname = 12; // Explicit optional
// Oneof (only one field can be set)
oneof payment_method {
CreditCard credit_card = 13;
BankAccount bank_account = 14;
string paypal_email = 15;
}
// Nested messages
message Customer {
string id = 1;
string name = 2;
message ContactInfo {
string email = 1;
string phone = 2;
}
ContactInfo contact = 3;
}
Field Numbers & Evolution¶
Service Definitions¶
Service Types¶
service OrderService {
// Unary RPC - single request, single response
rpc GetOrder(GetOrderRequest) returns (Order);
rpc CreateOrder(CreateOrderRequest) returns (Order);
// Server streaming - single request, stream of responses
rpc ListOrders(ListOrdersRequest) returns (stream Order);
// Client streaming - stream of requests, single response
rpc BatchCreateOrders(stream CreateOrderRequest) returns (BatchCreateResponse);
// Bidirectional streaming
rpc OrderUpdates(stream OrderUpdateRequest) returns (stream OrderUpdate);
}
message GetOrderRequest {
string order_id = 1;
}
message ListOrdersRequest {
string customer_id = 1;
int32 page_size = 2;
string page_token = 3;
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
Address shipping_address = 3;
}
message BatchCreateResponse {
repeated Order orders = 1;
int32 success_count = 2;
int32 failure_count = 3;
}
message OrderUpdateRequest {
string order_id = 1;
OrderStatus new_status = 2;
}
message OrderUpdate {
string order_id = 1;
OrderStatus status = 2;
google.protobuf.Timestamp updated_at = 3;
}
Communication Patterns¶
Java Implementation¶
Server Implementation¶
// Generated from proto
public class OrderServiceImpl extends OrderServiceGrpc.OrderServiceImplBase {
private final OrderRepository orderRepository;
// Unary RPC
@Override
public void getOrder(GetOrderRequest request,
StreamObserver<Order> responseObserver) {
try {
Order order = orderRepository.findById(request.getOrderId())
.orElseThrow(() -> Status.NOT_FOUND
.withDescription("Order not found: " + request.getOrderId())
.asRuntimeException());
responseObserver.onNext(order);
responseObserver.onCompleted();
} catch (Exception e) {
responseObserver.onError(Status.INTERNAL
.withDescription(e.getMessage())
.asException());
}
}
// Server streaming
@Override
public void listOrders(ListOrdersRequest request,
StreamObserver<Order> responseObserver) {
orderRepository.findByCustomerId(request.getCustomerId())
.forEach(responseObserver::onNext);
responseObserver.onCompleted();
}
// Client streaming
@Override
public StreamObserver<CreateOrderRequest> batchCreateOrders(
StreamObserver<BatchCreateResponse> responseObserver) {
List<Order> createdOrders = new ArrayList<>();
return new StreamObserver<>() {
@Override
public void onNext(CreateOrderRequest request) {
Order order = createOrder(request);
createdOrders.add(order);
}
@Override
public void onError(Throwable t) {
responseObserver.onError(t);
}
@Override
public void onCompleted() {
BatchCreateResponse response = BatchCreateResponse.newBuilder()
.addAllOrders(createdOrders)
.setSuccessCount(createdOrders.size())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
};
}
// Bidirectional streaming
@Override
public StreamObserver<OrderUpdateRequest> orderUpdates(
StreamObserver<OrderUpdate> responseObserver) {
return new StreamObserver<>() {
@Override
public void onNext(OrderUpdateRequest request) {
// Process update and send response
OrderUpdate update = processUpdate(request);
responseObserver.onNext(update);
}
@Override
public void onError(Throwable t) {
log.error("Error in stream", t);
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
}
// Server setup
public class GrpcServer {
public static void main(String[] args) throws Exception {
Server server = ServerBuilder.forPort(9090)
.addService(new OrderServiceImpl(orderRepository))
.addService(new HealthServiceImpl())
.intercept(new AuthInterceptor())
.intercept(new LoggingInterceptor())
.build();
server.start();
server.awaitTermination();
}
}
Client Implementation¶
public class OrderClient {
private final OrderServiceGrpc.OrderServiceBlockingStub blockingStub;
private final OrderServiceGrpc.OrderServiceStub asyncStub;
public OrderClient(ManagedChannel channel) {
this.blockingStub = OrderServiceGrpc.newBlockingStub(channel);
this.asyncStub = OrderServiceGrpc.newStub(channel);
}
// Unary call (blocking)
public Order getOrder(String orderId) {
GetOrderRequest request = GetOrderRequest.newBuilder()
.setOrderId(orderId)
.build();
return blockingStub.getOrder(request);
}
// Unary call (async)
public void getOrderAsync(String orderId, Consumer<Order> callback) {
GetOrderRequest request = GetOrderRequest.newBuilder()
.setOrderId(orderId)
.build();
asyncStub.getOrder(request, new StreamObserver<>() {
@Override
public void onNext(Order order) {
callback.accept(order);
}
@Override
public void onError(Throwable t) {
log.error("Error getting order", t);
}
@Override
public void onCompleted() {
// Done
}
});
}
// Server streaming (blocking iterator)
public List<Order> listOrders(String customerId) {
ListOrdersRequest request = ListOrdersRequest.newBuilder()
.setCustomerId(customerId)
.build();
Iterator<Order> orders = blockingStub.listOrders(request);
List<Order> result = new ArrayList<>();
orders.forEachRemaining(result::add);
return result;
}
// Client streaming
public BatchCreateResponse batchCreate(List<CreateOrderRequest> requests) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<BatchCreateResponse> responseRef = new AtomicReference<>();
StreamObserver<CreateOrderRequest> requestObserver =
asyncStub.batchCreateOrders(new StreamObserver<>() {
@Override
public void onNext(BatchCreateResponse response) {
responseRef.set(response);
}
@Override
public void onError(Throwable t) {
latch.countDown();
}
@Override
public void onCompleted() {
latch.countDown();
}
});
// Send all requests
requests.forEach(requestObserver::onNext);
requestObserver.onCompleted();
latch.await();
return responseRef.get();
}
}
// Client setup with configuration
public class GrpcClientConfig {
public ManagedChannel createChannel() {
return ManagedChannelBuilder
.forAddress("localhost", 9090)
.usePlaintext() // For development
// .useTransportSecurity() // For production
.keepAliveTime(30, TimeUnit.SECONDS)
.keepAliveTimeout(10, TimeUnit.SECONDS)
.idleTimeout(5, TimeUnit.MINUTES)
.intercept(new AuthInterceptor())
.build();
}
}
Error Handling¶
gRPC Status Codes¶
| Code | Description |
|---|---|
| OK (0) | Success |
| CANCELLED (1) | Operation cancelled by caller |
| UNKNOWN (2) | Unknown error |
| INVALID_ARGUMENT | Client specified invalid argument |
| DEADLINE_EXCEEDED | Deadline expired |
| NOT_FOUND (5) | Resource not found |
| ALREADY_EXISTS | Resource already exists |
| PERMISSION_DENIED | No permission for operation |
| RESOURCE_EXHAUSTED | Rate limit, quota exceeded |
| FAILED_PRECONDITION | Operation rejected (state issue) |
| ABORTED (10) | Concurrency conflict |
| OUT_OF_RANGE | Value out of valid range |
| UNIMPLEMENTED | Operation not implemented |
| INTERNAL (13) | Internal server error |
| UNAVAILABLE (14) | Service unavailable (retry) |
| DATA_LOSS (15) | Unrecoverable data loss |
| UNAUTHENTICATED | Authentication required |
Error Handling in Server¶
@Override
public void getOrder(GetOrderRequest request,
StreamObserver<Order> responseObserver) {
try {
if (request.getOrderId().isEmpty()) {
throw Status.INVALID_ARGUMENT
.withDescription("Order ID is required")
.asRuntimeException();
}
Order order = orderRepository.findById(request.getOrderId())
.orElseThrow(() -> Status.NOT_FOUND
.withDescription("Order not found: " + request.getOrderId())
.asRuntimeException());
responseObserver.onNext(order);
responseObserver.onCompleted();
} catch (StatusRuntimeException e) {
responseObserver.onError(e);
} catch (Exception e) {
responseObserver.onError(Status.INTERNAL
.withDescription("Internal error")
.withCause(e)
.asException());
}
}
// Rich error details
import com.google.rpc.BadRequest;
import com.google.rpc.ErrorInfo;
import io.grpc.protobuf.StatusProto;
public void createOrder(CreateOrderRequest request,
StreamObserver<Order> responseObserver) {
List<BadRequest.FieldViolation> violations = validate(request);
if (!violations.isEmpty()) {
BadRequest badRequest = BadRequest.newBuilder()
.addAllFieldViolations(violations)
.build();
com.google.rpc.Status status = com.google.rpc.Status.newBuilder()
.setCode(Code.INVALID_ARGUMENT.getNumber())
.setMessage("Validation failed")
.addDetails(Any.pack(badRequest))
.build();
responseObserver.onError(StatusProto.toStatusRuntimeException(status));
}
}
Error Handling in Client¶
public Order getOrder(String orderId) {
try {
return blockingStub
.withDeadlineAfter(5, TimeUnit.SECONDS)
.getOrder(GetOrderRequest.newBuilder()
.setOrderId(orderId)
.build());
} catch (StatusRuntimeException e) {
Status status = e.getStatus();
switch (status.getCode()) {
case NOT_FOUND:
throw new OrderNotFoundException(orderId);
case INVALID_ARGUMENT:
throw new InvalidRequestException(status.getDescription());
case DEADLINE_EXCEEDED:
throw new TimeoutException("Request timed out");
case UNAVAILABLE:
// Could retry
throw new ServiceUnavailableException();
default:
throw new RuntimeException("gRPC error: " + status);
}
}
}
Interceptors¶
Server Interceptor¶
public class LoggingInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String method = call.getMethodDescriptor().getFullMethodName();
long startTime = System.currentTimeMillis();
log.info("gRPC call started: {}", method);
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<>(
next.startCall(new ForwardingServerCall.SimpleForwardingServerCall<>(call) {
@Override
public void close(Status status, Metadata trailers) {
long duration = System.currentTimeMillis() - startTime;
log.info("gRPC call completed: {} - {} - {}ms",
method, status.getCode(), duration);
super.close(status, trailers);
}
}, headers)) {
};
}
}
public class AuthInterceptor implements ServerInterceptor {
private static final Metadata.Key<String> AUTH_KEY =
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String token = headers.get(AUTH_KEY);
if (token == null || !validateToken(token)) {
call.close(Status.UNAUTHENTICATED
.withDescription("Invalid or missing token"),
new Metadata());
return new ServerCall.Listener<>() {};
}
// Add user context
Context context = Context.current()
.withValue(USER_CONTEXT_KEY, extractUser(token));
return Contexts.interceptCall(context, call, headers, next);
}
}
Client Interceptor¶
public class AuthClientInterceptor implements ClientInterceptor {
private final String token;
@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) {
headers.put(
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER),
"Bearer " + token
);
super.start(responseListener, headers);
}
};
}
}
gRPC vs REST¶
| Aspect | gRPC | REST |
|---|---|---|
| Protocol | HTTP/2 | HTTP/1.1 or HTTP/2 |
| Format | Protocol Buffers | JSON/XML |
| Contract | Strict (.proto) | Loose (OpenAPI opt.) |
| Streaming | Bidirectional | Limited |
| Browser Support | Needs proxy | Native |
| Performance | Faster | Slower |
| Human Readable | No | Yes |
| Code Generation | Required | Optional |
| Tooling | Growing | Mature |
When to use gRPC: - Internal microservice communication - Low latency requirements - Streaming needed - Polyglot environments
When to use REST: - Public APIs - Browser clients - Simple CRUD operations - Human debugging needed
Common Interview Questions¶
- What is Protocol Buffers?
- Binary serialization format, schema-defined
- Smaller, faster than JSON
-
Backward/forward compatible
-
gRPC streaming types?
-
Unary, server streaming, client streaming, bidirectional
-
How does gRPC handle errors?
- Status codes (NOT_FOUND, INVALID_ARGUMENT, etc.)
-
Can include rich error details
-
gRPC vs REST performance?
- gRPC: Binary, smaller payload, HTTP/2 multiplexing
-
5-10x faster for many use cases
-
Proto schema evolution?
- Add fields with new numbers
- Reserve deleted field numbers
- Don't change existing field types