Skip to main content

Command Palette

Search for a command to run...

How to Get Started with gRPC in Java

Updated
10 min read
P
System Generalist with a background in Developer Productivity and Batch Processing.

gRPC is an open source RPC framework that enables efficient communication using Protocol Buffer based binary protocol over HTTP/2.

Unlike REST, gRPC requires both client and server to share the same Protocol Buffer contract. This setup works well for internal service-to-service communication within an organization, but makes it less suitable for public APIs where you can't control or coordinate client updates.

In this tutorial, you'll build a complete gRPC service from scratch. You'll set up a Gradle project, define a service contract using Protocol Buffers, implement both server and client, and test your implementation with a load testing tool.

Exchange Format between Applications

The communication contract between applications is set by defining IDL. This allows for the generation of code stubs required to implement the client and server.

Code stubs are language-specific implementations generated from your Protocol Buffer contract. They provide type-safe methods for serialization, deserialization, and network communication, eliminating the manual plumbing required for custom protocols.

Prerequisites

To follow this tutorial, you'll need:

  • Java 25

  • Gradle 9.3.0+

  • Basic understanding of the Gradle build tool. You can use Maven as well (Not covered in the article)

  • Familiarity with client-server architecture (Preferably REST)

  • A code editor (IntelliJ IDEA or VS Code) is optional but recommended.

Protobuf integration with Gradle

If you prefer the hard way, you can try using Protocol Buffer Compiler (Protoc) directly to generate code stubs.

But eventually, you'll need a flow that allows for easier integration of IDL with client and server implementation.

Basic Gradle Structure

You can use gradle init to setup project with appropriate options. This guide covers the Blank Project option in Gradle with Kotlin as DSL (instead of Groovy)

Gradle plugins allow for extended capabilities. You can use Protobuf Gradle Plugin to compile and generate stubs directly as a Gradle task, making it easier to compile the IDL, generate and deploy stub code as part of CI/CD.

Note: The versioning for the protobuf Gradle plugin is different from language dependencies like protobuf-java.

plugins {
    // Apply the application plugin to add support for building a CLI application in Java.
    application
    id("com.google.protobuf") version "0.9.6"
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

Next comes the language-specific dependencies and version for

  • Java, OpenJDK Runtime Environment (build 25.0.2+10-69)

  • gRPC 1.78.0

  • Protobuf 4.33.4

  • JUnit 5.14.2

  • Mockito 5.21.0 (Not used in the guide)

There are also specific configuration settings to

  • Decouple the project JDK from Gradle provided versions.

  • Configure all proto compilation tasks to use gRPC Plugin

  • Configure tests to run by JUnit Platform Engine (However, not used in the project)

  • Parameterized mainClass when running jar/gradle.

The finalized content of build.gradle.kts in project root path

plugins {
    // Apply the application plugin to add support for building a CLI application in Java.
    application
    id("com.google.protobuf") version "0.9.6"
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

group = "gstest"
version = "1.0.0"

java {
    sourceCompatibility = JavaVersion.VERSION_25
    targetCompatibility = JavaVersion.VERSION_25
}

val grpcVersion = "1.78.0"
val protobufVersion = "4.33.4"
val protocVersion = protobufVersion

val junitVersion = "5.14.2"
val mockitoVersion = "5.21.0"


dependencies {
    implementation("io.grpc:grpc-protobuf:$grpcVersion")
    implementation("io.grpc:grpc-services:$grpcVersion")
    implementation("io.grpc:grpc-stub:$grpcVersion")

    // examples/advanced need this for JsonFormat
    implementation("com.google.protobuf:protobuf-java-util:$protobufVersion")

    runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion")

    testImplementation("io.grpc:grpc-testing:$grpcVersion")
    testImplementation("io.grpc:grpc-inprocess:$grpcVersion")

    // Use JUnit Jupiter for testing.
    testImplementation(platform("org.junit:junit-bom:$junitVersion"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    testImplementation("org.mockito:mockito-core:$mockitoVersion")
    testImplementation("org.mockito:mockito-junit-jupiter:$mockitoVersion")
}

// Apply a specific Java toolchain to ease working on different environments.
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:$protocVersion"
    }
    plugins {
        create("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion"
        }
    }
    generateProtoTasks {
        all().forEach {
            it.plugins {
                create("grpc")
            }
        }
    }
}

application {
    // Define the main class for the application.
    mainClass = "org.example.App"
}

tasks.named<Test>("test") {
    // Use JUnit Platform for unit tests.
    useJUnitPlatform()
}

application {
    mainClass.set(project.findProperty("mainClass") as String?  ?: "gstest.App")
}

Creating IDL

Taking RoutingGuide example from grpc-java repo, a subset type and service definitions are included.

This file should be created at <project_root>/src/main/proto/route_guide.proto

syntax = "proto3";

package org.example.grpc;

option java_multiple_files = true;
option java_outer_classname = "RouteGuideProto";
option java_package = "org.example.grpc";

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

// A feature names something at a given point.
//
// If a feature could not be named, the name is empty.
message Feature {
  // The name of the feature.
  string name = 1;

  // The point where the feature is detected.
  Point location = 2;
}

service RouteGuide {
  // A simple RPC.
  //
  // Obtains the feature at a given position.
  //
  // A feature with an empty name is returned if there's no feature at the given
  // position.
  rpc GetFeature(Point) returns (Feature) {}
}

Compiling IDL

./gradlew :app:generateProto

The generated classes can be found in the build folder

root:~/code/grpc-gs$ tree build/generated/sources/
build/generated/sources/
├── annotationProcessor
│   └── java
│       └── main
├── headers
│   └── java
│       └── main
└── proto
    └── main
        ├── grpc
        │   └── org
        │       └── example
        │           └── grpc
        │               └── RouteGuideGrpc.java
        └── java
            └── org
                └── example
                    └── grpc
                        ├── Feature.java
                        ├── FeatureOrBuilder.java
                        ├── Point.java
                        ├── PointOrBuilder.java
                        └── RouteGuideProto.java

Writing Server

Depending on how you handle server lifecycle, additional utilities will be needed.

Prevent Termination

By default server start method is non-blocking, which means if the main thread exits, the JVM will shut down along with the daemon threads, killing the server.

For graceful shutdown, you can use server.awaitTermination();. Once JVM triggers shutdown hook (ex, due to SIGTERM), the server stops accepting new requests and waits until 30s for existing requests to complete before exiting.

Shutdown hook [Optional]

A function that can be called on shutdown, helps in cleanup.

Service

The service is implemented by extending RouteGuideImplBase which is an AsyncService for non-blocking behaviour. For the server, the application usually creates and passes StreamObserver, which is used for receiving and sending messages.

The default service implementation(...ImplBase) also extends BindableService, which provides a default implementation for service binding to the server.

Server Code

package gstest;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import org.example.grpc.Feature;
import org.example.grpc.Point;
import org.example.grpc.RouteGuideGrpc;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class RouteGuideServer {

    private Server server;
    private final int port;

    public RouteGuideServer(int port) {
        this.port = port;
    }

    public void start() throws IOException {
        server = ServerBuilder.forPort(port)
                .addService(new RouteGuideService())
                .build()
                .start();
        System.out.println("Server started, listening on port " + port);

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Shutting down gRPC server...");
            try {
                RouteGuideServer.this.stop();
            } catch (InterruptedException e) {
                e.printStackTrace(System.err);
            }
            System.out.println("Server shut down.");
        }));
    }

    public void stop() throws InterruptedException {
        if (server != null) {
            server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
        }
    }

    private void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }

    private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {

        @Override
        public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
            Feature feature = Feature.newBuilder()
                    .setName("Feature at (" + request.getLatitude() + ", " + request.getLongitude() + ")")
                    .setLocation(request)
                    .build();

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

    public static void main(String[] args) throws IOException, InterruptedException {
        int port = 8980;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        RouteGuideServer server = new RouteGuideServer(port);
        server.start();
        server.blockUntilShutdown();
    }
}

Note: This is an example of default implementation. You may opt for custom implementation depending on specific requirements, backpressure(Reactive), complex async flow, or per-method custom logic(BindableService), to name a few.

Writing Client

ConnectionManager

ManagedChannel is used for lifecycle connection management. It handles connection pooling and client-side load balancing among other aspects.

Client Stub

BlockingStub will be used for unary exchange, but other options for client stub offer more capabilities. You can use a specific Stub depending on client requirements

Stub TypeCreation MethodReturn TypeStreaming SupportException HandlingBest For
BlockingStubnewBlockingStub(channel)Direct resultServer streaming onlyUnchecked StatusRuntimeExceptionLegacy code, simple sync calls
BlockingV2StubnewBlockingV2Stub(channel)Direct resultServer streaming onlyChecked StatusExceptionNew sync code, safety-critical
AsyncStubnewStub(channel)void (callback)All: unary, client, server, bidirectionalCallback onError()High-performance, streaming
FutureStubnewFutureStub(channel)ListenableFuture<T>Unary onlyExecutionException in futureAsync composition, parallel calls

Client Code

package gstest;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import org.example.grpc.Feature;
import org.example.grpc.Point;
import org.example.grpc.RouteGuideGrpc;

import java.util.concurrent.TimeUnit;

public class RouteGuideClient {

    private final ManagedChannel channel;
    private final RouteGuideGrpc.RouteGuideBlockingStub blockingStub;

    public RouteGuideClient(String host, int port) {
        channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext()
                .build();
        blockingStub = RouteGuideGrpc.newBlockingStub(channel);
    }

    public void shutdown() throws InterruptedException {
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }

    public Feature getFeature(int latitude, int longitude) {
        Point request = Point.newBuilder()
                .setLatitude(latitude)
                .setLongitude(longitude)
                .build();

        return blockingStub.getFeature(request);
    }

    public static void main(String[] args) throws InterruptedException {
        String host = "localhost";
        int port = 8980;

        if (args.length >= 1) {
            host = args[0];
        }
        if (args.length >= 2) {
            port = Integer.parseInt(args[1]);
        }

        RouteGuideClient client = new RouteGuideClient(host, port);
        try {
            int latitude = 409146138;
            int longitude = -746188906;

            System.out.println("Requesting feature at (" + latitude + ", " + longitude + ")...");
            Feature feature = client.getFeature(latitude, longitude);
            System.out.println("Received feature: " + feature.getName());
            System.out.println("Location: (" + feature.getLocation().getLatitude() + ", " + feature.getLocation().getLongitude() + ")");
        } finally {
            client.shutdown();
        }
    }
}

Testing message exchange

Server

root:~/code/grpc-gs$ ./gradlew run -PmainClass=gstest.RouteGuideServer
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

Server started, listening on port 8980

Client

root:~/code/grpc-gs$ ./gradlew run -PmainClass=gstest.RouteGuideClient
Reusing configuration cache.
> Task :run
Requesting feature at (409146138, -746188906)...
Received feature: Feature at (409146138, -746188906)
Location: (409146138, -746188906)

BUILD SUCCESSFUL in 2s
6 actionable tasks: 1 executed, 5 up-to-date
Configuration cache entry reused.

So far, you built a client and server program. Given an IDL, other gRPC clients can also communicate with the server. In the next section, you'll use a benchmarking tool, which uses IDL as well as a client Stub to send the request.

Load Testing

Although the server has a minimal footprint, you can use a load test for benchmarking your implementations.

VM Spec: 4 Core, 16 GB

Ghz, a gRPC benchmarking tool, is used for load testing.

Test Configuration

  • 100 Concurrent Users
  • 100k Total Requests

Sequential Request per user upto 100k Requests

ghz --insecure --proto ./src/main/proto/route_guide.proto --call org.example.grpc.RouteGuide.GetFeature -d '{"latitude": 409146138, "longitude": -746188906}' -c 100 -n 100000 localhost:8980

Summary:
  Count:    100000
  Total:    12.98 s
  Slowest:    160.89 ms
  Fastest:    0.22 ms
  Average:    9.22 ms
  Requests/sec:    7704.96

Response time histogram:
  0.222   [1]     |
  16.288  [90254] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  32.355  [8162]  |∎∎∎∎
  48.421  [1033]  |
  64.488  [341]   |
  80.554  [109]   |
  96.620  [0]     |
  112.687 [41]    |
  128.753 [1]     |
  144.820 [43]    |
  160.886 [15]    |

Latency distribution:
  10 % in 3.03 ms 
  25 % in 5.04 ms 
  50 % in 7.73 ms 
  75 % in 10.96 ms 
  90 % in 16.09 ms 
  95 % in 21.02 ms 
  99 % in 41.25 ms 

Status code distribution:
  [OK]   100000 responses

The script generated close to 7.7K QPS during which utilization momentarily rises to 20%, but goes down immediately.

CPU Utlilization during Load Test (1-min Window)

What You Built

You've built a complete gRPC service in Java from the ground up. Throughout this tutorial, you've:

  • Created a type-safe service contract using Protocol Buffers
  • Configured a Gradle project for gRPC development and CI/CD integration
  • Implemented graceful shutdown handling with proper connection cleanup
  • Achieved 7,700+ requests/second, establishing a performance baseline

The IDL-first approach means any gRPC client (Python, Go, Node.js, etc.) can communicate with your server using the same proto file to generate language-specific stubs. Your client and server remain loosely coupled while maintaining compile-time type safety.

Next Steps

Now that you have a working foundation, explore these topics to build production-ready gRPC services:

  • Streaming RPCs: Server streaming, client streaming, and bidirectional streaming
  • Authentication: TLS/SSL encryption and token-based auth
  • Interceptors: Add logging, metrics, and request tracing
  • Error handling: Proper status codes and error propagation
  • Advanced features: Deadlines, metadata, and load balancing strategies

Reference: gRPC Guides

Common Issues and Troubleshooting

Issue 1: Unexpected plugin files generated

If you see these files generated as part of Gradle build:

new file:   plugin/src/functionalTest/java/org/example/GrpcGsPluginFunctionalTest.java
new file:   plugin/src/main/java/org/example/GrpcGsPlugin.java
new file:   plugin/src/test/java/org/example/GrpcGsPluginTest.java

Check settings.gradle.kts and comment out this line if present:

include("plugin")

Issue 2: Proto files in non-standard location

The client and server stubs won't generate if your proto files aren't in src/main/proto/ and you haven't configured the custom location.

If your proto files are not in the standard src/main/proto/ location, you'll need to override the source directory:

sourceSets {
    main {
        proto {
            srcDirs("src/main/java/gstest/proto")
        }
    }
}