110. Case Study: Designing Consistent APIs for Multi-Language Microservices with Protobuf
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?
Why Protobuf?
Before getting into the technical details, let’s compare a few ways to design an API:
| Method | Strengths | Weaknesses |
|---|---|---|
| REST+JSON | Human-readable, broad tooling | Not strict, prone to type mismatches, slow parsing |
| GraphQL | Flexible queries, self-documenting | Over-fetching, learning curve, slow execution |
| gRPC + Protobuf | Fast, strict schema, cross-language tooling | Less 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
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
OrderServiceservice 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:
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.protoEach 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
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)
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)Order Process Flow Diagram
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:
Use a Version in the Package Name
- Example:
package ecommerce.order.v1; - Makes breaking changes easier without disrupting older clients.
- Example:
Document Important Fields
- Add clear comments to every
field, and use a doc/extractor plugin if needed.
- Add clear comments to every
Never Change Field Tags
- Tags (the numbers on each field:
sku = 1) must not change. Renaming a field requires a new one (for example, addsku_code = 3and deprecatesku = 1).
- Tags (the numbers on each field:
Protobuf Lint
- Use a linter / protobuf-lint in CI so every schema definition follows a consistent style.
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:
1message OrderResponse {
2 string order_id = 1;
3 string status = 2;
4}After (Field Added):
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
.protofile 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: