Course topics

By WebNest Studio

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

Java
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;
}
Output
(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

Java
<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
Output
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

Java
<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;
    }
}
Output
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.