Spring Boot Tutorial
gRPC Services with Spring Boot
For communication between internal services, JSON over HTTP is not always the best fit: payloads are verbose, contracts are informal, and streaming is awkward. gRPC uses Protocol Buffers — a compact binary format defined by .proto contracts — over HTTP/2, generates type-safe client and server code in many languages, and supports streaming in both directions.
Spring Boot 4.1 brings gRPC support into Spring Boot itself, with starters for servers and clients, auto-configuration, health, observability and test support. This lesson defines a service contract, implements a gRPC server with @GrpcService, calls it from a client, uses server streaming, handles errors, and compares gRPC with REST.
Contracts with Protocol Buffers
You describe messages and services in a .proto file in src/main/proto. The build's protobuf plugin generates Java message classes and gRPC stubs — an abstract ...ImplBase class to extend on the server, and blocking, async and future stubs for clients. Because the contract is shared and versioned, clients in Java, Go, Python or Node generate compatible code from the same file. Field numbers must never be reused, which keeps old and new clients compatible.
Server Side
Add spring-boot-starter-grpc-server, extend the generated base class, and annotate it with @GrpcService. Spring Boot starts a Netty-based gRPC server (port 9090 by default, configurable with spring.grpc.server.port), registers reflection so tools like grpcurl can discover services, exposes the standard gRPC health service when Actuator is present, and records observations (metrics and traces) for every call.
Client Side
Add spring-boot-starter-grpc-client and configure named channels, e.g. spring.grpc.client.channel.catalog.target=static://localhost:9090 (Spring Boot 4.1 renamed the older channels.<name>.address properties). Create stubs from channels obtained through the GrpcChannelFactory, or register stub beans declaratively with @ImportGrpcClients. Always set deadlines on calls.
Streaming and Errors
gRPC supports unary calls, server streaming (one request, a stream of responses), client streaming and bidirectional streaming. Errors are reported with a Status code (NOT_FOUND, INVALID_ARGUMENT, UNAVAILABLE, DEADLINE_EXCEEDED...) rather than HTTP status codes; map your domain exceptions to statuses on the server.
gRPC or REST?
Use gRPC for internal, high-volume service-to-service calls, polyglot environments, and streaming. Keep REST (or GraphQL) for public APIs and browser clients — browsers cannot call gRPC directly without gRPC-Web or a gateway translation. Many systems expose REST at the edge and use gRPC internally.
Examples
The contract: src/main/proto/catalog.proto
syntax = "proto3";
package webnest.catalog.v1;
option java_multiple_files = true;
option java_package = "com.webnest.catalog.grpc";
service CatalogService {
rpc GetCourse (GetCourseRequest) returns (Course);
rpc ListLessons (ListLessonsRequest) returns (stream Lesson); // server streaming
}
message GetCourseRequest { string slug = 1; }
message ListLessonsRequest { string course_slug = 1; }
message Course {
int64 id = 1;
string slug = 2;
string title = 3;
int32 lessons = 4;
}
message Lesson {
int32 position = 1;
string title = 2;
}
(The build generates Course, GetCourseRequest, ..., and CatalogServiceGrpc with CatalogServiceImplBase and client stubs.
Spring Initializr adds the protobuf build plugin configuration when you select gRPC.)
Implementing the server
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-grpc-server</artifactId>
</dependency>
@GrpcService
public class CatalogGrpcService extends CatalogServiceGrpc.CatalogServiceImplBase {
private final CourseRepository courses;
public CatalogGrpcService(CourseRepository courses) {
this.courses = courses;
}
@Override
public void getCourse(GetCourseRequest request, StreamObserver<Course> response) {
courses.findBySlug(request.getSlug()).ifPresentOrElse(c -> {
response.onNext(Course.newBuilder()
.setId(c.getId()).setSlug(c.getSlug()).setTitle(c.getTitle()).setLessons(c.getLessonCount())
.build());
response.onCompleted();
}, () -> response.onError(Status.NOT_FOUND
.withDescription("No course " + request.getSlug()).asRuntimeException()));
}
@Override
public void listLessons(ListLessonsRequest request, StreamObserver<Lesson> response) {
courses.lessonsOf(request.getCourseSlug()).forEach(l ->
response.onNext(Lesson.newBuilder().setPosition(l.position()).setTitle(l.title()).build()));
response.onCompleted();
}
}
# application.yml
spring:
grpc:
server:
port: 9090
gRPC Server started, listening on port 9090
grpcurl -plaintext -d '{"slug":"spring-boot"}' localhost:9090 webnest.catalog.v1.CatalogService/GetCourse
{ "id": "2", "slug": "spring-boot", "title": "Spring Boot", "lessons": 118 }
grpcurl -plaintext -d '{"slug":"nope"}' localhost:9090 webnest.catalog.v1.CatalogService/GetCourse
ERROR: Code: NotFound Message: No course nope
Calling the service from another Spring Boot application
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-grpc-client</artifactId>
</dependency>
# application.yml
spring:
grpc:
client:
channel:
catalog:
target: static://catalog-service:9090
@Configuration
public class GrpcClientConfig {
@Bean
CatalogServiceGrpc.CatalogServiceBlockingStub catalogStub(GrpcChannelFactory channels) {
return CatalogServiceGrpc.newBlockingStub(channels.createChannel("catalog"));
}
}
@Service
public class CourseLookup {
private final CatalogServiceGrpc.CatalogServiceBlockingStub catalog;
public CourseLookup(CatalogServiceGrpc.CatalogServiceBlockingStub catalog) {
this.catalog = catalog;
}
public String title(String slug) {
return catalog.withDeadlineAfter(2, TimeUnit.SECONDS)
.getCourse(GetCourseRequest.newBuilder().setSlug(slug).build())
.getTitle();
}
public List<String> lessonTitles(String slug) {
List<String> titles = new ArrayList<>();
catalog.withDeadlineAfter(5, TimeUnit.SECONDS)
.listLessons(ListLessonsRequest.newBuilder().setCourseSlug(slug).build())
.forEachRemaining(l -> titles.add(l.getPosition() + ". " + l.getTitle()));
return titles;
}
}
title("spring-boot") -> Spring Boot
lessonTitles("spring-boot") -> [1. Spring vs Spring Boot vs Spring MVC, 2. Spring Boot Project Setup, ...]
(catalog down) -> StatusRuntimeException: UNAVAILABLE
Common Mistakes
- Reusing or renumbering protobuf field numbers, silently breaking older clients.
- Calling gRPC without deadlines, so a stuck server blocks clients indefinitely.
- Returning generic UNKNOWN errors instead of meaningful Status codes.
- Exposing gRPC directly to browsers, which need gRPC-Web or a REST gateway.
- Using the older spring.grpc.client.channels.<name>.address properties with Spring Boot 4.1 (now channel.<name>.target).
Key Points to Remember
- gRPC uses Protocol Buffers contracts over HTTP/2 with generated, type-safe stubs.
- Spring Boot 4.1 provides spring-boot-starter-grpc-server and -client with auto-configuration.
- @GrpcService registers a server implementation; the server listens on port 9090 by default.
- Clients configure spring.grpc.client.channel.<name>.target and create stubs from GrpcChannelFactory.
- Use gRPC internally for efficient, streaming, polyglot calls; keep REST for public and browser APIs.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.