gRPC APIs with Spring Boot
|
This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page covers the official Spring gRPC project and how its Spring Boot 4.1 starters wire gRPC servers and clients into a Spring context: service beans, typed client injection, interceptors, and mapping gRPC status codes to and from Java exceptions.
|
Spring gRPC 1.0 reached general availability alongside Spring Boot 4.1 — after the publication date of every local reference book listed in this section’s bibliography. None of those books mentions Spring gRPC at all, so this page is written and verified exclusively against the official reference documentation at docs.spring.io/spring-grpc/reference — it is the sole source for everything below. |
Relationship to contract-first API design
The .proto file remains the single source of truth for a gRPC service’s contract: message shapes, RPC
signatures, and package/service naming. This page only recaps enough protobuf to make the Java examples
readable — the full discussion of contract-first design, versioning a .proto contract, and how it compares to
an OpenAPI-first REST contract lives on the sibling page
API-First: REST and gRPC. Read that page first if you are
choosing between REST and gRPC or designing a new contract from scratch.
A minimal contract, kept intentionally small here:
syntax = "proto3";
package orders.v1;
option java_package = "com.example.orders.grpc";
option java_multiple_files = true;
service OrderService {
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse);
}
message GetOrderRequest {
string order_id = 1;
}
message ListOrdersRequest {
string customer_id = 1;
int32 page_size = 2;
}
message OrderResponse {
string order_id = 1;
string status = 2;
int64 total_cents = 3;
}
The Protobuf Gradle/Maven plugin generates OrderServiceGrpc, message builders, and a reactive/blocking stub
base class from this file at build time; Spring gRPC does not replace that generation step, it wires the
generated types into Spring.
The Spring gRPC project and starters
Spring gRPC is an independent Spring project (not part of
spring-boot-starter-* core) that brings auto-configuration, dependency injection, and observability to gRPC
servers and clients built on grpc-java. Spring Boot 4.1 applications add it as a regular starter dependency.
Server side:
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-server-spring-boot-starter</artifactId>
</dependency>
Client side:
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-client-spring-boot-starter</artifactId>
</dependency>
Both starters honor standard Spring Boot configuration-property binding, so the embedded gRPC server’s port and a named client’s target address are set the same way any other Spring Boot property is set:
spring:
grpc:
server:
port: 9090
client:
channels:
orders:
address: static://localhost:9090
See the Spring gRPC "Getting Started" guide for the full dependency and property reference.
Implementing a server with @GrpcService
A gRPC service implementation is a regular Spring bean that extends the generated
<Service>Grpc.<Service>ImplBase class and is annotated @GrpcService. Spring gRPC discovers every
@GrpcService bean in the context and registers it on the embedded gRPC server automatically — there is no
manual ServerBuilder wiring.
import io.grpc.stub.StreamObserver;
import org.springframework.grpc.server.service.GrpcService;
@GrpcService
public class OrderGrpcService extends OrderServiceGrpc.OrderServiceImplBase {
private final OrderRepository orders;
public OrderGrpcService(OrderRepository orders) {
this.orders = orders;
}
@Override
public void getOrder(GetOrderRequest request, StreamObserver<OrderResponse> responseObserver) {
Order order = orders.findById(request.getOrderId());
OrderResponse response = OrderResponse.newBuilder()
.setOrderId(order.id())
.setStatus(order.status())
.setTotalCents(order.totalCents())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
@Override
public void listOrders(ListOrdersRequest request, StreamObserver<OrderResponse> responseObserver) {
orders.findByCustomer(request.getCustomerId(), request.getPageSize())
.forEach(order -> responseObserver.onNext(toResponse(order)));
responseObserver.onCompleted();
}
private OrderResponse toResponse(Order order) {
return OrderResponse.newBuilder()
.setOrderId(order.id())
.setStatus(order.status())
.setTotalCents(order.totalCents())
.build();
}
}
Because OrderGrpcService is a normal Spring bean, constructor injection, @Transactional, and every other
Spring Framework facility apply exactly as they would to a @RestController. See
Spring gRPC — Server services.
Typed client injection with @ImportGrpcClients
On the calling side, @ImportGrpcClients generates and registers stub beans for one or more generated gRPC
service classes, bound to a named channel from spring.grpc.client.channels:
import org.springframework.grpc.client.ImportGrpcClients;
import org.springframework.context.annotation.Configuration;
@Configuration
@ImportGrpcClients(value = "orders", types = OrderServiceGrpc.class)
public class GrpcClientConfig {
}
The generated stub bean is then injected like any other bean — typically the blocking stub for simple request/response calls:
import org.springframework.stereotype.Service;
@Service
public class OrderClient {
private final OrderServiceGrpc.OrderServiceBlockingStub orders;
public OrderClient(OrderServiceGrpc.OrderServiceBlockingStub orders) {
this.orders = orders;
}
public OrderResponse getOrder(String orderId) {
GetOrderRequest request = GetOrderRequest.newBuilder().setOrderId(orderId).build();
return orders.getOrder(request);
}
}
The channel named orders in @ImportGrpcClients matches the spring.grpc.client.channels.orders property
shown earlier, so switching target addresses per environment (or pointing at a load balancer / service-mesh
address) requires only a configuration change, not a code change. See
Spring gRPC — Client services.
Interceptors
gRPC interceptors are the equivalent of a servlet filter or an OpenFeign interceptor: they run around every
call to add cross-cutting behavior such as logging, authentication header propagation, or metrics. Spring gRPC
picks up any ServerInterceptor or ClientInterceptor bean annotated with the corresponding Spring gRPC
stereotype and applies it globally.
import io.grpc.*;
import org.springframework.grpc.server.service.GrpcGlobalServerInterceptor;
@GrpcGlobalServerInterceptor
public class LoggingServerInterceptor 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 start = System.nanoTime();
ServerCall<ReqT, RespT> wrapped = new ForwardingServerCall.SimpleForwardingServerCall<>(call) {
@Override
public void close(Status status, Metadata trailers) {
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
System.out.printf("%s -> %s (%dms)%n", method, status.getCode(), elapsedMs);
super.close(status, trailers);
}
};
return next.startCall(wrapped, headers);
}
}
A client-side interceptor is registered the same way, attached to a named channel through
spring.grpc.client.channels.<name>.interceptors or by declaring a @GrpcGlobalClientInterceptor bean, and is
typically used to attach an Authorization metadata header to every outgoing call. See
Spring gRPC — Server interceptors and
Spring gRPC — Client interceptors.
Error mapping: gRPC status codes vs. Java exceptions
gRPC has no exception-mapping mechanism analogous to @ExceptionHandler on a REST controller: an uncaught
exception thrown from a service method becomes a generic UNKNOWN status on the wire, which loses all
diagnostic information for the caller. Server implementations must translate domain exceptions into a
io.grpc.Status explicitly, typically from a shared interceptor or a helper invoked from each method:
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
@Override
public void getOrder(GetOrderRequest request, StreamObserver<OrderResponse> responseObserver) {
try {
Order order = orders.findById(request.getOrderId());
responseObserver.onNext(toResponse(order));
responseObserver.onCompleted();
} catch (OrderNotFoundException ex) {
responseObserver.onError(Status.NOT_FOUND
.withDescription("Order " + request.getOrderId() + " does not exist")
.withCause(ex)
.asRuntimeException());
} catch (IllegalArgumentException ex) {
responseObserver.onError(Status.INVALID_ARGUMENT
.withDescription(ex.getMessage())
.asRuntimeException());
} catch (RuntimeException ex) {
responseObserver.onError(Status.INTERNAL
.withDescription("Unexpected error processing order lookup")
.withCause(ex)
.asRuntimeException());
}
}
A rough mapping between common Java exceptions and gRPC status codes used above and elsewhere in a typical service:
| Java exception | gRPC status | Typical meaning |
|---|---|---|
|
|
Request failed input validation. |
|
|
Referenced resource does not exist. |
|
|
Concurrent modification conflict; safe to retry. |
|
|
Caller authenticated but not authorized. |
|
|
Caller did not present valid credentials. |
Any other unchecked exception |
|
Unexpected server-side failure; do not leak internal details in the description. |
On the client side, every stub call throws StatusRuntimeException (blocking stub) or delivers the status
through StreamObserver.onError (async/reactive stub); callers inspect
StatusRuntimeException.getStatus().getCode() to decide whether to retry, surface a user-facing error, or
translate it back into an HTTP status when a gRPC call sits behind a REST facade:
try {
return orders.getOrder(request);
} catch (StatusRuntimeException ex) {
if (ex.getStatus().getCode() == Status.Code.NOT_FOUND) {
throw new OrderNotFoundException(request.getOrderId(), ex);
}
throw ex;
}
Centralizing this translation in a single server-side interceptor (rather than repeating try/catch blocks in every method) keeps individual service methods focused on business logic. See Spring gRPC — Exception handling.
Further reading
-
Spring gRPC reference documentation — the sole source for this page, covering everything from starter configuration to TLS, health checks, and reflection service setup.
-
API-First: REST and gRPC — contract-first design,
.protoversioning, and choosing between REST and gRPC for a given API.