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

110. Case Study: Designing Consistent APIs for Multi-Language Microservices with Protobuf

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

title: 110. Case Study: Designing Consistent APIs for Multi-Language Microservices with Protobuf
author: Rifqi Ramadhan
date: 2024-06-10

Case Study: Designing Consistent APIs for Multi-Language Microservices with Protobuf

One of the biggest challenges in building a microservices architecture is ensuring API contract consistency across many services written in different programming languages (multi-language). As business needs grow more diverse, it’s not uncommon for a system to consist of services built on Go, Java, Python, and even Node.js that must communicate with one another efficiently, securely, and of course consistently.

In this article, I’ll walk through best practices for designing consistent APIs for multi-language microservices using Protocol Buffers (Protobuf), starting from a real-world case study, sample code, and a communication simulation, all the way to the implementation flow diagram.


Case Study: An E-Commerce Platform

Imagine an e-commerce platform made up of several core services:

  • Order Service (Go)
  • Inventory Service (Java)
  • User Service (Python)
  • Notification Service (Node.js)

How can we make sure every service can communicate without having to worry about differences in data structures, type conversions, and documentation that’s hard to keep in sync?

Danger
Answer: WITH A PROTOBUF-BASED API DESIGN.

Why Protobuf?

Before getting into the technical details, let’s compare a few ways to design an API:

MethodStrengthsWeaknesses
REST+JSONHuman-readable, broad toolingNot strict, prone to type mismatches, slow parsing
GraphQLFlexible queries, self-documentingOver-fetching, learning curve, slow execution
gRPC + ProtobufFast, strict schema, cross-language toolingLess familiar, more complex initial setup

Protobuf forces us to define the contract explicitly, which can then be used to generate client/server code across various languages. For this use-case, it’s a winner!


Designing Protobuf-Based Service Contracts

Example Contract: Order Service

File: order.proto

proto
 1syntax = "proto3";
 2
 3package ecommerce.order.v1;
 4
 5message OrderRequest {
 6  string user_id = 1;
 7  repeated Item items = 2;
 8}
 9
10message Item {
11  string sku = 1;
12  int32 quantity = 2;
13}
14
15message OrderResponse {
16  string order_id = 1;
17  string status = 2; // PENDING, CONFIRMED, FAILED
18}
19
20service OrderService {
21  rpc PlaceOrder (OrderRequest) returns (OrderResponse);
22}

Explanation

  • The data structure definitions for OrderRequest, Item, and OrderResponse are kept simple, explicit, and reusable across languages.
  • The OrderService service describes the functions that are available: for example, an RPC to process an order.

Generating Code for Multiple Languages

After defining the .proto, developers can generate the client/server code:

bash
1# For Go
2protoc --go_out=. --go-grpc_out=. order.proto
3
4# For Java
5protoc --java_out=. --grpc-java_out=. order.proto
6
7# For Python
8python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. order.proto

Each language gets a generated library that is 100% consistent with the .proto definition.


Simulating Interaction Between Microservices

Let’s simulate the Order Service (Go) calling the Inventory Service (Java) using Protobuf.

Order Service (Go) — Calling a Stock Check

proto
 1// inventory/v1/inventory.proto
 2syntax = "proto3";
 3
 4package ecommerce.inventory.v1;
 5
 6message StockRequest {
 7  string sku = 1;
 8}
 9
10message StockResponse {
11  int32 available_quantity = 1;
12}
13
14service InventoryService {
15  rpc CheckStock(StockRequest) returns (StockResponse);
16}

Calling from the Order Service (Go)

go
1conn, _ := grpc.Dial("inventory-service:50051", grpc.WithInsecure())
2client := inventorypb.NewInventoryServiceClient(conn)
3
4resp, err := client.CheckStock(context.Background(), &inventorypb.StockRequest{
5  Sku: "SKU1234",
6})
7fmt.Printf("Stok: %d\n", resp.AvailableQuantity)
Danger
Key Point: Type safety and consistent data structures are preserved even across different implementations.

Order Process Flow Diagram

MERMAID
sequenceDiagram
    actor User
    participant OrderService (Go)
    participant InventoryService (Java)
    participant UserService (Python)
    participant NotificationService (NodeJS)

    User->>OrderService (Go): PlaceOrder(OrderRequest)
    OrderService (Go)->>InventoryService (Java): CheckStock(StockRequest)
    InventoryService (Java)-->>OrderService (Go): StockResponse
    OrderService (Go)->>UserService (Python): GetUserDetail(UserID)
    UserService (Python)-->>OrderService (Go): UserDetail
    OrderService (Go)->>NotificationService (NodeJS): SendNotif(Order)
    NotificationService (NodeJS)-->>OrderService (Go): NotifResponse
    OrderService (Go)-->>User: OrderResponse

Best Practices for Designing Protobuf for Multi-Language Microservices

Here are a few recommendations for maintaining consistency and maintainability:

  1. Use a Version in the Package Name

    • Example: package ecommerce.order.v1;
    • Makes breaking changes easier without disrupting older clients.
  2. Document Important Fields

    • Add clear comments to every field, and use a doc/extractor plugin if needed.
  3. Never Change Field Tags

    • Tags (the numbers on each field: sku = 1) must not change. Renaming a field requires a new one (for example, add sku_code = 3 and deprecate sku = 1).
  4. Protobuf Lint

    • Use a linter / protobuf-lint in CI so every schema definition follows a consistent style.
  5. Shared Protobuf Repo

    • All teams/services point to the same repository so the schema is always up to date.

Case Study: Adding a Field Without a Breaking Change

Before:

proto
1message OrderResponse {
2  string order_id = 1;
3  string status = 2;
4}

After (Field Added):

proto
1message OrderResponse {
2  string order_id = 1;
3  string status = 2;
4  string payment_reference = 3;
5}

All older clients can still process the message! Newer clients will receive payment_reference when it’s provided.


Benefits for Business and Engineering

  • Freedom of Language Choice: Teams can pick the best language/tools without fear of integration getting stuck.
  • Type Safety: Developers can be confident the incoming/outgoing data matches the schema.
  • API-first Documentation: The .proto file doubles as formal API documentation.
  • Automation & Code Generation: Updating the schema immediately triggers re-generation of the client/server interfaces.

Conclusion

Choosing Protobuf for multi-language microservices communication significantly reduces friction, speeds up development, and keeps the contract consistent across developer teams. The case study above proves that a strong and consistent API design can support the scale of a continuously growing digital business without sacrificing engineering discipline.

Ready to adopt Protobuf for your microservices ecosystem? Don’t forget, start with a clean, cross-language, and automated schema design. Happy coding!


References:


Related Articles

💬 Comments