Adding gRPC to Spring Boot traditionally meant choosing a third-party starter such as LogNet or yidongnan, managing gRPC versions, and dealing with server configuration yourself. Spring gRPC 1.0 made it official but kept the auto-configuration outside Boot. Spring Boot 4.1 finished the job: the auto-configuration moved into Boot itself, the starters are on start.spring.io, and the BOM manages the versions of the gRPC dependencies it provides. If you’re comfortable building REST endpoints with Spring, Spring Boot 4.1 makes the gRPC programming model feel surprisingly familiar.

The starters

Two dependencies, no third-party anything, no extra BOM:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-grpc-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-grpc-client</artifactId>
</dependency>

On start.spring.io they’re just “gRPC Server” and “gRPC Client”. The Spring Boot BOM manages the compatible Spring gRPC and gRPC Java versions, so you don’t need to declare them yourself.

Contract-first: the .proto

Proto files go in src/main/proto. Put your .proto files there, then use the protobuf Maven plugin to generate the Java classes:

// src/main/proto/greeter.proto
syntax = "proto3";

option java_package = "com.example.demo.proto";
option java_multiple_files = true;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
  rpc StreamHellos (HelloRequest) returns (stream HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

The Spring Boot BOM manages the runtime gRPC dependencies. The protobuf Maven plugin still needs an explicit protoc and protoc-gen-grpc-java version for build-time code generation:

<!-- the standard protobuf-maven-plugin setup -->
<build>
  <extensions>
    <extension>
      <groupId>kr.motd.maven</groupId>
      <artifactId>os-maven-plugin</artifactId>
      <version>1.7.1</version>
    </extension>
  </extensions>
  <plugins>
    <plugin>
      <groupId>org.xolstice.maven.plugins</groupId>
      <artifactId>protobuf-maven-plugin</artifactId>
      <version>0.6.1</version>
      <configuration>
        <protocArtifact>com.google.protobuf:protoc:${os.detected.classifier}:exe:${protobuf.version}</protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.80.0:exe:${os.detected.classifier}</pluginArtifact>
      </configuration>
      <executions>
        <execution><goals><goal>compile</goal><goal>compile-custom</goal></goals></execution>
      </executions>
    </plugin>
  </plugins>
</build>

The server: one annotation

Any Spring bean implementing io.grpc.BindableService can be exposed as a gRPC service. In practice, extend the generated ImplBase and annotate it with @GrpcService. No server builder, no lifecycle code.

@GrpcService
public class GreeterService extends GreeterGrpc.GreeterImplBase {

    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        HelloReply reply = HelloReply.newBuilder()
                .setMessage("Hello, " + request.getName())
                .build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }

    @Override
    public void streamHellos(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        // standard gRPC Java server-streaming: send multiple responses through the StreamObserver
        for (int i = 1; i <= 3; i++) {
            responseObserver.onNext(HelloReply.newBuilder()
                    .setMessage("Hello #%d, %s".formatted(i, request.getName()))
                    .build());
        }
        responseObserver.onCompleted();
    }
}
# application.properties
spring.grpc.server.port=9090

Boot starts a Netty gRPC server on 9090. Everything is configurable under spring.grpc.server.*: TLS via SSL bundles, keep-alive, message size limits, and graceful shutdown. When grpc-services is on the classpath, Spring Boot can automatically configure gRPC reflection and health support — so grpcurl can discover the service without additional server configuration:

grpcurl -plaintext localhost:9090 list
# com.example.demo.proto.Greeter
# grpc.health.v1.Health
# grpc.reflection.v1.ServerReflection

-plaintext is right for this sample because the server doesn’t use TLS — don’t copy that flag into production. Each of these lines is a service you can then call with grpcurl: grpc.health.v1.Health for health checks, grpc.reflection.v1.ServerReflection for discovery.

Exception handling and interceptors

The programming model will feel familiar: @GrpcAdvice is the gRPC equivalent of the familiar @RestControllerAdvice pattern, mapping exceptions to gRPC statuses, and @GlobalServerInterceptor beans apply to every service.

@GrpcAdvice
public class GreeterExceptionHandler {

    @GrpcExceptionHandler(IllegalArgumentException.class)
    public StatusRuntimeException handleIllegalArgument(IllegalArgumentException ex) {
        return Status.INVALID_ARGUMENT.withDescription(ex.getMessage()).asRuntimeException();
    }
}

@Component
@GlobalServerInterceptor
public class LoggingInterceptor implements ServerInterceptor {
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
        log.info("gRPC call: {}", call.getMethodDescriptor().getFullMethodName());
        return next.startCall(call, headers);
    }
}

Multiple global interceptors can be ordered with Spring’s usual @Order mechanism.

The client: inject a stub like a RestClient

Declare the channel target in properties, import the clients, inject the stub. No ManagedChannelBuilder anywhere in your code.

spring.grpc.client.channel.greeter.target=localhost:9090
@SpringBootApplication
@ImportGrpcClients(target = "greeter", types = GreeterGrpc.GreeterBlockingStub.class)
public class DemoApplication { ... }

@Service
public class GreetingClient {
    private final GreeterGrpc.GreeterBlockingStub greeter;

    public GreetingClient(GreeterGrpc.GreeterBlockingStub greeter) {
        this.greeter = greeter;
    }

    public String greet(String name) {
        HelloReply reply = greeter.sayHello(HelloRequest.newBuilder().setName(name).build());
        return reply.getMessage();
    }
}

The channel is named greeter, and its settings live under spring.grpc.client.channel.greeter.*:

@ImportGrpcClients(target = "greeter")
                    ↓
spring.grpc.client.channel.greeter.target
                    ↓
localhost:9090

TLS, keep-alive, message-size limits, and other channel-specific settings can be configured under spring.grpc.client.channel.greeter.* too — configured like a data source. Retry-related channel configuration can also be supplied there where supported.

Testing: in-process, no ports

This is the nicest surprise. @AutoConfigureTestGrpcTransport configures an in-process gRPC test transport, so the test doesn’t need to open a TCP port. The test still exercises the Spring gRPC application layer, including service invocation and exception handling, without requiring a network listener. It doesn’t replace tests that need to exercise the real network transport, TLS, HTTP/2 behavior, or deployment configuration. The annotation ships in the gRPC test starters — add spring-boot-starter-grpc-server-test (or its client sibling) to your test scope and it’s on the classpath.

@SpringBootTest
@AutoConfigureTestGrpcTransport
class GreeterServiceTest {

    @Autowired
    GreeterGrpc.GreeterBlockingStub greeter;

    @Test
    void returnsGreeting() {
        HelloReply reply = greeter.sayHello(HelloRequest.newBuilder().setName("Ramesh").build());
        assertThat(reply.getMessage()).isEqualTo("Hello, Ramesh");
    }

    @Test
    void mapsBadInputToInvalidArgument() {
        // exception-handler behavior, verified without a socket
        assertThatThrownBy(() -> greeter.sayHello(HelloRequest.newBuilder().setName("").build()))
                .isInstanceOf(StatusRuntimeException.class)
                .satisfies(e -> assertThat(((StatusRuntimeException) e).getStatus().getCode())
                        .isEqualTo(Status.Code.INVALID_ARGUMENT));
    }
}

Fast, deterministic, and no @DynamicPropertySource port juggling. For me, this is one of the nicest improvements in the new gRPC support.

One port for both worlds

Running gRPC next to an existing REST API? Boot 4.1 supports a Servlet-embedded transport mode so gRPC and Spring MVC share one port over HTTP/2, instead of running the gRPC server on a separate Netty port. Useful for the migration period where half your clients still speak REST. The default standalone setup still uses a native gRPC server such as Netty — the Servlet transport is the opt-in for sharing the application’s HTTP/2 servlet server.

When gRPC, when REST

Honest framing, since the hype oversells it:

gRPCREST
ContractExplicit schema (.proto, codegen)HTTP resources + optional OpenAPI contract
PayloadBinary Protobuf — typically compact and efficientText-based JSON — easy to inspect manually
StreamingBuilt into the RPC modelUsually handled with SSE, WebSockets, or other HTTP mechanisms
Browser clientsUsually requires gRPC-Web or a gatewayNative HTTP/JSON APIs
Toolinggrpcurl, reflectioncurl, every tool ever

gRPC is often a strong fit for service-to-service communication, strongly typed contracts, and streaming RPCs. REST remains a natural fit for public HTTP APIs, browser-facing applications, and APIs where human-readable requests and responses are important. Boot 4.1 doesn’t force either model — it just makes the gRPC side as easy as the REST side always was.

The interview one-liner

“Spring Boot 4.1 moved gRPC auto-configuration into Boot itself: @GrpcService exposes any BindableService, @ImportGrpcClients registers generated type-safe stubs as Spring beans configured under spring.grpc.client.channel.*, and @AutoConfigureTestGrpcTransport gives you in-process tests. No third-party gRPC starter, no manual channel code.”