DEV Community

jamilxt
jamilxt

Posted on

Spring Boot 4.1 Made gRPC a First-Class Citizen: When to Use It and When REST Still Wins

Two years ago I worked on an order-processing system where five internal services talked to each other over REST. It worked, mostly. But every integration bug I remember from that project traces back to the same three things: a payload that grew until serialization became the bottleneck, a client that drifted from the API contract because OpenAPI docs were optional, and a hand-rolled SSE setup that existed only because we needed server-side streaming.

All three of those problems have a name, and the name is gRPC. The reason most Java teams never adopted it was friction. You wired servers, channels, and lifecycle management by hand, and you lost the Spring Boot magic that made everything else easy.

Spring Boot 4.1, released June 10, 2026, removed that excuse. gRPC support moved into Spring Boot itself, with starters, auto-configuration, client injection, and a test transport that needs no ports. This article is the walkthrough I wish I had, plus the decision matrix for when the migration is actually worth it.

Full disclosure: I have not yet shipped Spring Boot 4.1 gRPC to production. I built a working server and client this week to evaluate it for an internal service, and everything below comes from that hands-on run plus the official documentation. Where I am extrapolating from REST experience instead of gRPC scars, I will say so.

What Actually Changed in Spring Boot 4.1

Before 4.1, the story was the separate Spring gRPC project. Version 1.0 arrived around Spring Boot 4.0, but the auto-configuration lived outside Boot. The 1.1.0 release notes put the change in one line: "The main change in 1.1.0 is the migration of autoconfiguration to Spring Boot 4.1.0."

What that means in practice:

  • Real starters on start.spring.io. Pick "gRPC Server" or "gRPC Client" as a dependency. Under the hood you get spring-boot-starter-grpc-server and spring-boot-starter-grpc-client, with spring-grpc-core:1.1.0 and grpc-java 1.80.0 managed by the Boot BOM. You never declare spring-grpc yourself.
  • Annotation-driven servers. Annotate a bean with @GrpcService and Boot registers and exposes it. No server builder, no lifecycle glue.
  • Injected, configured clients. Declare a channel target in application.properties and inject a type-safe stub like it is a RestClient.
  • An in-process test transport. @AutoConfigureTestGrpcTransport swaps the Netty server for gRPC's in-process transport. Your test still flows through interceptors, exception handlers, and marshalling, just without TCP. Fast and no port conflicts.

If you can build a REST endpoint in Spring, you already know how to build a gRPC service. That was not true before.

Building the Server in Ten Minutes

Start at start.spring.io, select the gRPC Server dependency, and generate. The only file you write by hand before any Java is the contract.

The proto file is the whole contract

Create src/main/proto/hello.proto:

syntax = "proto3";

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

service HelloWorld {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}
Enter fullscreen mode Exit fullscreen mode

This is the part REST never gave us. The contract is not documentation beside the code. It is the code. From this one file you can generate type-safe clients for Java, Go, Python, or anything else with a protobuf compiler, and the compiler breaks your build when you make an incompatible change. That is a build-time guarantee, not a wiki page that rots.

Spring Boot manages the io.github.ascopes:protobuf-maven-plugin for you. If you use spring-boot-starter-parent, the protoc version and the generate goal are already configured. Run ./mvnw compile and the generated classes appear under target/generated-sources/protobuf. In IntelliJ, right-click that folder and mark it as Generated Sources Root or the IDE will not see the classes.

The service implementation

import io.grpc.stub.StreamObserver;
import org.springframework.grpc.server.service.GrpcService;

@GrpcService
public class HelloService extends HelloWorldGrpc.HelloWorldImplBase {

    @Override
    public void sayHello(HelloRequest request,
                         StreamObserver<HelloReply> responseObserver) {
        String message = "Hello '%s'".formatted(request.getName());
        HelloReply reply = HelloReply.newBuilder()
                .setMessage(message)
                .build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }
}
Enter fullscreen mode Exit fullscreen mode

That is the entire server. The pattern is always the same: do your work, call onNext() with the response, call onCompleted() when done. For streaming RPCs you call onNext() multiple times before completing.

Start the app and Netty listens on port 9090 by default. Note that this is a separate server from Tomcat. gRPC here is not living inside your servlet container unless you explicitly switch it there.

Verify without writing a client

Install grpcurl, which is curl for gRPC:

grpcurl -plaintext -d '{"name": "Spring"}' localhost:9090 HelloWorld.SayHello
Enter fullscreen mode Exit fullscreen mode
{
  "message": "Hello 'Spring'"
}
Enter fullscreen mode Exit fullscreen mode

One tip worth the price of this article: add the server reflection dependency to your pom and grpcurl -plaintext localhost:9090 list will enumerate everything your server exposes, including health endpoints. During evaluation, that single command saved me from re-reading generated code to remember method names.

Building the Client

Generate a second project with the gRPC Client starter and copy the same proto file into src/main/proto. In production you would publish the generated contract classes as a shared artifact so both sides compile against one source of truth.

Point the channel at the server:

spring.grpc.client.channels.helloworld.target=localhost:9090
Enter fullscreen mode Exit fullscreen mode

Import the stub and inject it:

@ImportGrpcClients(@GrpcClient(HelloWorldGrpc.HelloWorldBlockingStub.class))
@SpringBootApplication
public class ClientApplication {
    // ...
}
Enter fullscreen mode Exit fullscreen mode
@Bean
CommandLineRunner runner(HelloWorldGrpc.HelloWorldBlockingStub stub) {
    return args -> {
        HelloRequest request = HelloRequest.newBuilder()
                .setName("Jamil")
                .build();
        System.out.println(stub.sayHello(request).getMessage());
    };
}
Enter fullscreen mode Exit fullscreen mode

The stub is a Spring bean, configured through properties exactly like a datasource, and it is fully type-safe. Rename a field in the proto and the client stops compiling. With REST, the equivalent failure mode was a 400 at runtime in staging, discovered by whoever was on call.

Testing with zero ports

This is my favorite piece of the whole release:

@SpringBootTest
@AutoConfigureTestGrpcTransport
@ImportGrpcClients(types = HelloWorldGrpc.HelloWorldBlockingStub.class)
class HelloServiceTest {

    @Autowired
    HelloWorldGrpc.HelloWorldBlockingStub stub;

    @Test
    void saysHello() {
        var reply = stub.sayHello(
                HelloRequest.newBuilder().setName("Spring").build());
        assertThat(reply.getMessage()).isEqualTo("Hello 'Spring'");
    }
}
Enter fullscreen mode Exit fullscreen mode

Requests travel through the full server pipeline, interceptors and exception handlers included, over an in-process transport. Compare that to the usual REST integration test ritual of grabbing random ports and waiting for Tomcat to boot.

REST vs gRPC: The Decision Matrix

Here is the part to save. gRPC is now easy in Spring Boot, and easy is not the same as correct. I would not reach for it everywhere, and neither should you.

Choose gRPC when:

  • Service-to-service traffic is high volume. Protobuf is binary and compact; JSON parsing and payload size compound at scale. For internal calls between services in the same cluster, this is the classic win.
  • You need streaming as a core requirement. Server streaming, client streaming, and bidirectional streaming are built into the RPC model, not bolted on with SSE or WebSockets.
  • Multiple teams or languages consume the contract. One proto file generates verified clients for every stack. The compiler enforces compatibility at build time.
  • The service is internal. Nobody needs to curl your endpoints from a browser or read the payloads by hand.

Choose REST when:

  • The API is public or browser-facing. gRPC in a browser requires gRPC-Web, which adds proxying complexity. JSON over HTTP is universal.
  • Debuggability beats throughput. Human-readable payloads matter more than wire efficiency for your traffic level. Be honest about your actual request volume.
  • The ecosystem expects it. Webhooks, third-party integrations, and most SaaS consumers assume REST. Do not make your customers learn protobuf to integrate with you.
  • Your team has no gRPC experience. Deadlines, deadlines, error codes, deadlines. The learning curve is real even when the framework is friendly.

My rule of thumb after this evaluation: keep REST at the edge, use gRPC between internal services where the traffic is heavy and the contract is yours to control. That is roughly where the industry landed too, and Spring Boot 4.1 finally makes the internal half painless for Java teams.

Gotchas I Hit or Read the Fine Print For

  • Netty or Tomcat, pick deliberately. The default is Netty on port 9090. If you want gRPC served through your servlet container, you swap in io.grpc:grpc-servlet-jakarta, set server.http2.enabled=true, and then spring.grpc.server.port is ignored in favor of server.port. Know which mode you are in before debugging connection issues.
  • Netty version conflicts. If the bundled Netty clashes with another library, exclude io.grpc:grpc-netty and add io.grpc:grpc-netty-shaded. The shaded jar relocates Netty packages so nothing collides.
  • Property renames if you used the old starters. The deprecated org.springframework.grpc starters from the 1.0 era still work on Boot 4.0, but new work should use Boot's own starters. Migration is mostly dependency coordinates.
  • HTTP/2 is not optional. gRPC runs over HTTP/2. Behind load balancers or ingress controllers, confirm your proxy passes HTTP/2 or you will burn an afternoon on opaque connection errors. I have not personally hit this yet, so treat it as a warning from the docs rather than a scar.

What I Would Do Differently

If I were rebuilding that five-service order system today, I would start the proto contracts in a shared module on day one, generate clients in CI instead of by hand, and keep a thin REST layer at the public edge only. The biggest mistake teams make with gRPC is not the wire format. It is letting contracts live in three places at once: the proto file, the server code, and the client code, drifting independently. Spring Boot 4.1 cannot stop you from doing that. Only deciding upfront that the proto file is the single source of truth can.


I write about Java, Spring Boot, and AI infrastructure every week. Subscribe, it is free.

Have you moved any services from REST to gRPC? Did the binary payload savings show up in your metrics, or did the operational complexity eat the gains? I am especially curious about servlet-container setups behind ingress proxies. Tell me in the comments.

Further reading:

Top comments (0)