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-serverandspring-boot-starter-grpc-client, withspring-grpc-core:1.1.0andgrpc-java 1.80.0managed by the Boot BOM. You never declare spring-grpc yourself. -
Annotation-driven servers. Annotate a bean with
@GrpcServiceand Boot registers and exposes it. No server builder, no lifecycle glue. -
Injected, configured clients. Declare a channel target in
application.propertiesand inject a type-safe stub like it is aRestClient. -
An in-process test transport.
@AutoConfigureTestGrpcTransportswaps 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;
}
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();
}
}
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
{
"message": "Hello 'Spring'"
}
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
Import the stub and inject it:
@ImportGrpcClients(@GrpcClient(HelloWorldGrpc.HelloWorldBlockingStub.class))
@SpringBootApplication
public class ClientApplication {
// ...
}
@Bean
CommandLineRunner runner(HelloWorldGrpc.HelloWorldBlockingStub stub) {
return args -> {
HelloRequest request = HelloRequest.newBuilder()
.setName("Jamil")
.build();
System.out.println(stub.sayHello(request).getMessage());
};
}
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'");
}
}
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, setserver.http2.enabled=true, and thenspring.grpc.server.portis ignored in favor ofserver.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-nettyand addio.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.grpcstarters 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:
- Spring Boot 4.1.0 release announcement
- Spring Boot gRPC reference documentation
- Spring gRPC project
- Getting Started with Spring gRPC by Dan Vega, whose hello-grpc example shaped my first run
Top comments (0)