Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
21 Sep 2025 · 5 min read ·Article 105 / 110
Go

105. Case Study: Communication Between Go and Java Microservices via Protobuf

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

Case Study: Communication Between Go and Java Microservices via Protobuf

Danger
Cross-language microservice interoperability has become a top priority in modern architecture scenarios. How can Go and Java talk to each other efficiently? Protocol Buffers (Protobuf) answers that challenge. Here is a concrete case study and tutorial on how to apply it.

Introduction

Microservices let us build systems that are scalable, modular, and can be developed in parallel. Quite often, different teams use different programming languages to implement their services, for example Go (Golang) and Java. The biggest challenge, however, is ensuring that data communication between services runs smoothly and efficiently.

This is where gRPC and Protocol Buffers (Protobuf) come in as a solution. With Protobuf, data serialization becomes far more lightweight than JSON/XML—reducing latency and overhead while enabling cross-language compatibility.

This article walks through a real-world case study: how to build communication between a Go microservice and a Java microservice, both communicating over gRPC with data serialization handled by Protobuf.

Case Study: Order Service (Go) and Payment Service (Java)

Imagine we have two key services in an e-commerce system:

  1. Order Service—written in Go.
  2. Payment Service—written in Java.

The Order Service will send a payment instruction to the Payment Service and then wait for a confirmation. The scheme is illustrated as follows:

MERMAID
sequenceDiagram
    participant Go-OrderService
    participant Java-PaymentService

    Go-OrderService->>Java-PaymentService: RequestPayment(orderId, amount)
    Java-PaymentService-->>Go-OrderService: PaymentResponse(status, transactionId)

Designing the Data Contract with Protobuf

To preserve interoperability, we design a Protobuf file (payment.proto) that represents the data contract, along with the service endpoint provided by the Payment Service.

proto
 1// payment.proto
 2syntax = "proto3";
 3
 4package payment;
 5
 6// Request from OrderService to PaymentService
 7message PaymentRequest {
 8  string order_id = 1;
 9  double amount = 2;
10}
11
12// Response from PaymentService to OrderService
13message PaymentResponse {
14  string status = 1;          // "SUCCESS" or "FAIL"
15  string transaction_id = 2;  // transaction ID if successful
16}
17
18// Service definition
19service PaymentService {
20  rpc MakePayment(PaymentRequest) returns (PaymentResponse);
21}

This file will later be used to generate the gRPC stubs for both services, both Go and Java.

Generating the Protobuf Stubs

Steps to generate the code:

1. Install the Protobuf Compiler (protoc)

bash
1# Ubuntu
2sudo apt-get install -y protobuf-compiler
3
4# Mac
5brew install protobuf

2. Generate the Stub in Go

Install the plugins:

bash
1go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
2go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Generate:

bash
1protoc --go_out=. --go-grpc_out=. payment.proto

3. Generate the Stub in Java

Add the Maven/Gradle dependencies:

xml
1<!-- build.gradle -->
2dependencies {
3    implementation 'io.grpc:grpc-protobuf:1.54.0'
4    implementation 'io.grpc:grpc-netty-shaded:1.54.0'
5    implementation 'io.grpc:grpc-stub:1.54.0'
6}

Generate via plugin/maven/protoc:

bash
1protoc --java_out=. --grpc-java_out=. payment.proto

Implementing the Payment Service (Java)

Let’s look at how the Payment Service implements the service according to the contract.

java
 1// Java PaymentServiceImpl.java
 2public class PaymentServiceImpl extends PaymentServiceGrpc.PaymentServiceImplBase {
 3    @Override
 4    public void makePayment(Payment.PaymentRequest request, StreamObserver<Payment.PaymentResponse> responseObserver) {
 5        String orderId = request.getOrderId();
 6        double amount = request.getAmount();
 7
 8        // Simulate a successful payment
 9        Payment.PaymentResponse response = Payment.PaymentResponse.newBuilder()
10                .setStatus("SUCCESS")
11                .setTransactionId("TXN-" + orderId)
12                .build();
13
14        responseObserver.onNext(response);
15        responseObserver.onCompleted();
16    }
17}

Bootstrapping the server:

java
1public static void main(String[] args) throws IOException, InterruptedException {
2    Server server = ServerBuilder.forPort(50051)
3            .addService(new PaymentServiceImpl())
4            .build()
5            .start();
6    server.awaitTermination();
7}

Implementing the Order Service (Go)

On the Go side, we build a gRPC client that sends a request to the Payment Service.

go
 1// order_service.go
 2package main
 3
 4import (
 5    "context"
 6    "log"
 7    "payment"  // generated from payment.proto
 8    "google.golang.org/grpc"
 9)
10
11func main() {
12    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
13    if err != nil {
14        log.Fatalf("Did not connect: %v", err)
15    }
16    defer conn.Close()
17    client := payment.NewPaymentServiceClient(conn)
18
19    req := &payment.PaymentRequest{
20        OrderId: "ORD-123",
21        Amount:  150000,
22    }
23
24    resp, err := client.MakePayment(context.Background(), req)
25    if err != nil {
26        log.Fatalf("Error calling MakePayment: %v", err)
27    }
28
29    log.Printf("Payment Status: %s, Transaction ID: %s", resp.GetStatus(), resp.GetTransactionId())
30}

Simulating the Communication

Steps to run the simulation:

  1. Run the Payment Service (Java, port 50051)
  2. Run the Order Service (Go)

The Order Service will send a payment request via gRPC and receive a successful response.

Expected Output

text
1Payment Status: SUCCESS, Transaction ID: TXN-ORD-123

Comparing Efficiency: Protobuf vs JSON

FormatSerialized SizeParsing LatencyInteroperabilityStrictness
ProtobufSmallLowStrong (multi-language)Strict
JSONLargerHigherGood (but parsers vary)Flexible

With Protobuf, the obvious advantages include:

  • Serialized messages are far smaller.
  • Parsing and serialization are faster.
  • The contract between services becomes “typed” and versioned.

Architecture Diagram

MERMAID
flowchart LR
    GS[Go Order Service] -- gRPC+Protobuf --> JS[Java Payment Service]

Best Practice Tips

  1. Maintain backward compatibility in your Protobuf schema, especially if the service is already deployed to production and there are clients on different versions.
  2. Automate code generation in your CI/CD pipeline so that every .proto change is immediately reflected in the stubs for each language.
  3. Monitor latency. Protobuf is indeed fast, but make sure the gRPC networking overhead stays low enough.
  4. Protocol versioning. Use new field numbers when adding features, and avoid deleting or changing the meaning of existing fields.

Conclusion

Pairing Go and Java with Protobuf shows that cross-language microservices can remain efficient, maintainable, and future-proof. With a single data contract, developers can focus purely on business logic—interoperability, scale, and speed are already guaranteed by Protobuf and gRPC.

Have you built cross-language microservices with Protobuf? Share your experience in the comments!

Related Articles

💬 Comments