Post

Spring Boot + gRPC — High-Performance Inter-Service Communication

Build high-performance microservices with Spring Boot and gRPC. Covers Protocol Buffers, unary and server-streaming RPCs, the grpc-spring-boot-starter, error handling, deadlines, and performance comparison with REST.

Spring Boot + gRPC — High-Performance Inter-Service Communication

The Problem

Microservices talk to each other constantly. In most Spring Boot projects, that means REST over HTTP/1.1 with JSON payloads. It works, but the overhead adds up:

  • Text-based serialization — JSON is human-readable but expensive to parse. Every field name is sent on every request.
  • One request per connection — HTTP/1.1 opens a new connection (or waits for an existing one) for each call. Head-of-line blocking is real.
  • No contract enforcement — OpenAPI specs are optional and often drift from the actual implementation.
  • No streaming — Polling or WebSockets are bolted on as afterthoughts.

For internal service-to-service calls where humans never read the payload, we can do much better.


What Is gRPC?

gRPC is a high-performance RPC framework originally developed at Google. It uses Protocol Buffers (protobuf) for serialization and HTTP/2 for transport.

sequenceDiagram
    participant Client as gRPC Client
    participant Server as gRPC Server

    Client->>Server: HTTP/2 stream — binary protobuf request
    Server-->>Client: HTTP/2 stream — binary protobuf response
    Note over Client,Server: Single TCP connection, multiplexed streams

diagram

Key properties:

FeatureDescription
Protocol BuffersBinary serialization — 3-10x smaller than JSON, 20-100x faster to parse
HTTP/2Multiplexed streams over a single TCP connection, header compression
Code generationStrongly typed client and server stubs generated from .proto files
StreamingFirst-class support for unary, server-streaming, client-streaming, and bidirectional streaming
DeadlinesBuilt-in timeout propagation across service boundaries
graph LR
    A[.proto file] --> B[protoc compiler]
    B --> C[Java server stubs]
    B --> D[Java client stubs]
    B --> E[Go / Python / etc.]

diagram


What We Are Building

A multi-module Spring Boot project with:

  1. proto — Shared .proto definitions and generated stubs
  2. grpc-server — Exposes a PaymentService over gRPC on port 9090
  3. grpc-client — REST API (port 8080) that calls the gRPC server internally
flowchart LR
    Browser["Browser / curl"] -->|REST HTTP| Client[grpc-client :8080]
    Client -->|gRPC HTTP/2| Server[grpc-server :9090]

diagram


Step 1: Define the Proto

Create proto/src/main/proto/payment.proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.anupam.grpc.proto";
option java_outer_classname = "PaymentProto";

package payment;

message PaymentRequest {
  string payment_id = 1;
}

message ListPaymentsRequest {
  string user_id = 1;
  int32 page_size = 2;
}

message PaymentResponse {
  string payment_id = 1;
  string user_id = 2;
  double amount = 3;
  string currency = 4;
  string status = 5;
  string created_at = 6;
}

service PaymentService {
  // Unary RPC — one request, one response
  rpc GetPayment(PaymentRequest) returns (PaymentResponse);

  // Server streaming — one request, stream of responses
  rpc ListPayments(ListPaymentsRequest) returns (stream PaymentResponse);
}

The protobuf-maven-plugin compiles this into Java classes during mvn compile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
    <groupId>org.xolstice.maven.plugins</groupId>
    <artifactId>protobuf-maven-plugin</artifactId>
    <version>0.6.1</version>
    <configuration>
        <protocArtifact>
            com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
        </protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>
            io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
        </pluginArtifact>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>compile-custom</goal>
            </goals>
        </execution>
    </executions>
</plugin>

After compilation, you get:

  • PaymentRequest, PaymentResponse, ListPaymentsRequest — message classes
  • PaymentServiceGrpc — base classes for server implementation and client stubs

Step 2: Server Implementation

The grpc-server module uses net.devh:grpc-server-spring-boot-starter to integrate gRPC into Spring Boot seamlessly.

Application class

1
2
3
4
5
6
@SpringBootApplication
public class GrpcServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(GrpcServerApplication.class, args);
    }
}

PaymentGrpcService

Annotate with @GrpcService — the starter auto-registers it with the gRPC server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@GrpcService
public class PaymentGrpcService extends PaymentServiceGrpc.PaymentServiceImplBase {

    private final Map<String, PaymentResponse> payments = new ConcurrentHashMap<>();

    @Override
    public void getPayment(PaymentRequest request,
                           StreamObserver<PaymentResponse> responseObserver) {
        String paymentId = request.getPaymentId();

        if (paymentId == null || paymentId.isBlank()) {
            responseObserver.onError(Status.INVALID_ARGUMENT
                    .withDescription("Payment ID must not be empty")
                    .asRuntimeException());
            return;
        }

        PaymentResponse payment = payments.get(paymentId);

        if (payment == null) {
            responseObserver.onError(Status.NOT_FOUND
                    .withDescription("Payment not found: " + paymentId)
                    .asRuntimeException());
            return;
        }

        responseObserver.onNext(payment);
        responseObserver.onCompleted();
    }

    @Override
    public void listPayments(ListPaymentsRequest request,
                             StreamObserver<PaymentResponse> responseObserver) {
        String userId = request.getUserId();
        int pageSize = request.getPageSize() > 0 ? request.getPageSize() : 10;

        payments.values().stream()
                .filter(p -> p.getUserId().equals(userId))
                .limit(pageSize)
                .forEach(responseObserver::onNext);

        responseObserver.onCompleted();
    }
}

Configuration

1
2
3
4
5
6
7
spring:
  application:
    name: grpc-server

grpc:
  server:
    port: 9090

The pattern for every gRPC method:

  1. Validate the request
  2. Do the work
  3. Call responseObserver.onNext(response) (once for unary, multiple times for streaming)
  4. Call responseObserver.onCompleted()
  5. On failure, call responseObserver.onError(statusException)

Step 3: Client Implementation

The grpc-client module uses net.devh:grpc-client-spring-boot-starter to inject gRPC stubs.

PaymentClientService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class PaymentClientService {

    private static final Logger log = LoggerFactory.getLogger(PaymentClientService.class);

    @GrpcClient("payment-service")
    private PaymentServiceGrpc.PaymentServiceBlockingStub paymentStub;

    public PaymentResponse getPayment(String paymentId) {
        log.info("Requesting payment: {}", paymentId);
        PaymentRequest request = PaymentRequest.newBuilder()
                .setPaymentId(paymentId)
                .build();
        return paymentStub.getPayment(request);
    }

    public List<PaymentResponse> listPayments(String userId, int pageSize) {
        log.info("Listing payments for user: {}", userId);
        ListPaymentsRequest request = ListPaymentsRequest.newBuilder()
                .setUserId(userId)
                .setPageSize(pageSize)
                .build();

        Iterator<PaymentResponse> iterator = paymentStub.listPayments(request);
        List<PaymentResponse> payments = new ArrayList<>();
        iterator.forEachRemaining(payments::add);
        return payments;
    }
}

The @GrpcClient("payment-service") annotation injects a stub configured by the name in application.yml.

REST Controller

Expose gRPC results as a standard REST API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RestController
@RequestMapping("/api/payments")
public class PaymentController {

    private final PaymentClientService paymentClientService;

    public PaymentController(PaymentClientService paymentClientService) {
        this.paymentClientService = paymentClientService;
    }

    @GetMapping("/{paymentId}")
    public ResponseEntity<?> getPayment(@PathVariable String paymentId) {
        try {
            PaymentResponse response = paymentClientService.getPayment(paymentId);
            return ResponseEntity.ok(toMap(response));
        } catch (StatusRuntimeException e) {
            return handleGrpcError(e);
        }
    }

    @GetMapping
    public ResponseEntity<?> listPayments(
            @RequestParam String userId,
            @RequestParam(defaultValue = "10") int pageSize) {
        try {
            List<PaymentResponse> responses =
                    paymentClientService.listPayments(userId, pageSize);
            return ResponseEntity.ok(responses.stream()
                    .map(this::toMap).toList());
        } catch (StatusRuntimeException e) {
            return handleGrpcError(e);
        }
    }
}

Client Configuration

1
2
3
4
5
6
7
8
9
10
11
12
spring:
  application:
    name: grpc-client

server:
  port: 8080

grpc:
  client:
    payment-service:
      address: static://localhost:9090
      negotiation-type: plaintext

Step 4: Server Streaming

The ListPayments RPC demonstrates server streaming. Instead of returning a single response, the server sends multiple PaymentResponse messages over the same HTTP/2 stream:

sequenceDiagram
    participant Client as grpc-client
    participant Server as grpc-server

    Client->>Server: ListPaymentsRequest (userId=USER-1)
    Server-->>Client: PaymentResponse (PAY-001)
    Server-->>Client: PaymentResponse (PAY-002)
    Server-->>Client: PaymentResponse (PAY-003)
    Server-->>Client: Stream completed

diagram

On the server side, you call onNext() for each item:

1
2
3
4
5
6
payments.values().stream()
        .filter(p -> p.getUserId().equals(userId))
        .limit(pageSize)
        .forEach(responseObserver::onNext);

responseObserver.onCompleted();

On the client side, you receive an Iterator:

1
2
Iterator<PaymentResponse> iterator = paymentStub.listPayments(request);
iterator.forEachRemaining(payments::add);

This is powerful for:

  • Large result sets — Stream rows instead of buffering the entire list in memory
  • Real-time feeds — Push data as it becomes available
  • Progress updates — Report processing status incrementally

gRPC vs REST Comparison

AspectREST + JSONgRPC + Protobuf
SerializationJSON (text, ~verbose)Protobuf (binary, compact)
TransportHTTP/1.1 (one req/conn)HTTP/2 (multiplexed streams)
Payload sizeLarger (field names repeated)3-10x smaller
Parse speedSlower (text parsing)20-100x faster
Code generationOptional (OpenAPI)Built-in and required
StreamingWorkarounds (SSE, WebSocket)Native (4 patterns)
Browser supportNativeRequires grpc-web proxy
Human readabilityEasy to debug with curlBinary — needs tooling
ContractCan drift from specProto file is the contract
ToolingMature ecosystemGrowing (grpcurl, Postman)

When to use gRPC:

  • Internal service-to-service communication
  • High-throughput, low-latency requirements
  • Streaming use cases
  • Polyglot environments (one proto, many languages)

When to stick with REST:

  • Public APIs consumed by browsers
  • Simple CRUD with few calls
  • Team familiarity and tooling preferences

Error Handling

gRPC uses status codes instead of HTTP status codes. Map them to HTTP when exposing a REST facade:

gRPC StatusHTTP StatusTypical Use
OK200Success
INVALID_ARGUMENT400Bad request data
NOT_FOUND404Resource doesn’t exist
ALREADY_EXISTS409Duplicate creation
PERMISSION_DENIED403Insufficient permissions
UNAUTHENTICATED401Missing/invalid credentials
DEADLINE_EXCEEDED504Timeout
UNAVAILABLE503Server temporarily down
INTERNAL500Unexpected server error

In the server, return errors with meaningful descriptions:

1
2
3
responseObserver.onError(Status.NOT_FOUND
        .withDescription("Payment not found: " + paymentId)
        .asRuntimeException());

In the client controller, translate to HTTP:

1
2
3
4
5
6
7
8
9
10
11
12
13
private ResponseEntity<Map<String, String>> handleGrpcError(StatusRuntimeException e) {
    HttpStatus httpStatus = switch (e.getStatus().getCode()) {
        case NOT_FOUND -> HttpStatus.NOT_FOUND;
        case INVALID_ARGUMENT -> HttpStatus.BAD_REQUEST;
        case PERMISSION_DENIED -> HttpStatus.FORBIDDEN;
        case UNAUTHENTICATED -> HttpStatus.UNAUTHORIZED;
        case DEADLINE_EXCEEDED -> HttpStatus.GATEWAY_TIMEOUT;
        case UNAVAILABLE -> HttpStatus.SERVICE_UNAVAILABLE;
        default -> HttpStatus.INTERNAL_SERVER_ERROR;
    };
    return ResponseEntity.status(httpStatus)
            .body(Map.of("error", e.getStatus().getDescription()));
}

Deadlines

Always set deadlines to avoid hanging calls:

1
2
PaymentServiceGrpc.PaymentServiceBlockingStub stubWithDeadline =
        paymentStub.withDeadlineAfter(5, TimeUnit.SECONDS);

If the server doesn’t respond within the deadline, the client receives DEADLINE_EXCEEDED.


Common Problems

ProblemCauseSolution
UNAVAILABLE: io exceptionServer not running or wrong portVerify grpc.server.port matches client address
UNIMPLEMENTEDMethod not overridden in service implExtend the correct ImplBase class and override the method
Could not find protocOS classifier not detectedAdd os-maven-plugin as a build extension
Stubs not generatedMissing compile-custom goalEnsure both compile and compile-custom goals are configured
DEADLINE_EXCEEDEDSlow server or network issuesIncrease deadline or optimize server logic
ClassNotFoundException for proto classesProto module not built firstRun mvn install from the parent to build modules in order
negotiation-type errorTLS mismatchUse plaintext for local dev, tls for production

Full Working Example

The complete source code for this project is available on GitHub:

github.com/AnupamSinha/spring-boot-examples/tree/main/14-grpc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Clone and build
git clone https://github.com/AnupamSinha/spring-boot-examples/tree/main/14-grpc
cd spring-boot-grpc
mvn clean install

# Terminal 1 — start gRPC server
cd grpc-server && mvn spring-boot:run

# Terminal 2 — start REST client
cd grpc-client && mvn spring-boot:run

# Terminal 3 — test
curl http://localhost:8080/api/payments/PAY-001
curl "http://localhost:8080/api/payments?userId=USER-1&pageSize=10"

References

This post is licensed under CC BY 4.0 by the author.