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

IllegalArgumentException / bean-validation failure

INVALID_ARGUMENT

Request failed input validation.

OrderNotFoundException / EntityNotFoundException

NOT_FOUND

Referenced resource does not exist.

OptimisticLockingFailureException

ABORTED

Concurrent modification conflict; safe to retry.

AccessDeniedException

PERMISSION_DENIED

Caller authenticated but not authorized.

AuthenticationException

UNAUTHENTICATED

Caller did not present valid credentials.

Any other unchecked exception

INTERNAL

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, .proto versioning, and choosing between REST and gRPC for a given API.