API-First Polyglot Platform with Protocol Buffers and Backward Compatibility: A Complete Integration Tutorial¶
Objective: Build a production-ready API-first polyglot platform that integrates API governance with backward compatibility, protocol buffers for efficient serialization, polyglot interoperability design, and service decomposition strategies. This tutorial demonstrates how to build scalable, type-safe APIs that work seamlessly across Python, Go, Rust, and other languages.
This tutorial combines: - API Governance, Backward Compatibility Rules, and Cross-Language Interface Stability - API versioning and stability - Protocol Buffers with Python - Efficient data serialization - Polyglot Interoperability Design - Cross-language integration - Service Decomposition Strategy - Microservices architecture
1) Prerequisites¶
# Required tools
docker --version # >= 20.10
python --version # >= 3.10
go --version # >= 1.21
rust --version # >= 1.70
protoc --version # Protocol Buffers compiler
grpcurl --version # gRPC CLI tool
# Python packages
pip install grpcio grpcio-tools protobuf \
fastapi uvicorn \
openapi-generator \
prometheus-client
# Go packages
go get google.golang.org/grpc
go get github.com/grpc-ecosystem/grpc-gateway/v2
go get github.com/prometheus/client_golang
# Rust packages (Cargo.toml)
# tonic = "0.10"
# prost = "0.12"
Why: API-first polyglot platforms require Protocol Buffers for type-safe serialization, gRPC for efficient communication, API versioning for backward compatibility, and service decomposition for scalability.
2) Architecture Overview¶
We'll build an API-First E-Commerce Platform with polyglot services:
graph TB
subgraph "API Gateway Layer"
KONG[Kong Gateway<br/>API Versioning]
ROUTER[Version Router]
COMPAT[Compatibility Layer]
end
subgraph "Polyglot Services"
ORDER_PY[Order Service<br/>Python/FastAPI]
PAYMENT_GO[Payment Service<br/>Go/gRPC]
INVENTORY_RUST[Inventory Service<br/>Rust/Tonic]
NOTIFICATION_PY[Notification Service<br/>Python]
end
subgraph "Protocol Buffers"
PROTO_REPO[Proto Repository]
SCHEMA_REG[Schema Registry]
CODE_GEN[Code Generation]
end
subgraph "API Governance"
VERSIONING[Version Manager]
COMPATIBILITY[Compatibility Checker]
BREAKING[Breaking Change Detector]
CONTRACTS[API Contracts]
end
subgraph "Service Mesh"
ISTIO[Istio Service Mesh]
MESH[gRPC Mesh]
end
KONG --> ROUTER
ROUTER --> COMPAT
COMPAT --> ORDER_PY
COMPAT --> PAYMENT_GO
COMPAT --> INVENTORY_RUST
COMPAT --> NOTIFICATION_PY
ORDER_PY --> PROTO_REPO
PAYMENT_GO --> PROTO_REPO
INVENTORY_RUST --> PROTO_REPO
NOTIFICATION_PY --> PROTO_REPO
PROTO_REPO --> SCHEMA_REG
SCHEMA_REG --> CODE_GEN
CODE_GEN --> ORDER_PY
CODE_GEN --> PAYMENT_GO
CODE_GEN --> INVENTORY_RUST
VERSIONING --> KONG
COMPATIBILITY --> SCHEMA_REG
BREAKING --> SCHEMA_REG
CONTRACTS --> KONG
ORDER_PY --> ISTIO
PAYMENT_GO --> ISTIO
INVENTORY_RUST --> ISTIO
ISTIO --> MESH Platform Capabilities: 1. API Versioning: Multiple API versions with backward compatibility 2. Protocol Buffers: Type-safe, efficient serialization 3. Polyglot Services: Python, Go, Rust services communicate seamlessly 4. Service Decomposition: Domain-driven microservices
3) Repository Layout¶
api-first-polyglot/
├── docker-compose.yaml
├── proto/
│ ├── order/
│ │ ├── v1/
│ │ │ └── order.proto
│ │ └── v2/
│ │ └── order.proto
│ ├── payment/
│ │ └── v1/
│ │ └── payment.proto
│ └── inventory/
│ └── v1/
│ └── inventory.proto
├── services/
│ ├── order-service/
│ │ ├── Dockerfile
│ │ ├── requirements.txt
│ │ ├── app/
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ ├── api_v1.py
│ │ │ ├── api_v2.py
│ │ │ └── compatibility.py
│ │ └── generated/
│ │ └── order_pb2.py
│ ├── payment-service/
│ │ ├── Dockerfile
│ │ ├── go.mod
│ │ ├── main.go
│ │ ├── server.go
│ │ └── generated/
│ │ └── payment.pb.go
│ └── inventory-service/
│ ├── Dockerfile
│ ├── Cargo.toml
│ ├── src/
│ │ ├── main.rs
│ │ └── server.rs
│ └── generated/
│ └── inventory.rs
├── gateway/
│ ├── kong.yaml
│ └── version_router.py
├── governance/
│ ├── version_manager.py
│ ├── compatibility_checker.py
│ └── breaking_change_detector.py
└── scripts/
├── generate_proto.sh
└── validate_compatibility.sh
4) Protocol Buffer Schemas with Versioning¶
4.1) Order Service v1¶
Create proto/order/v1/order.proto:
syntax = "proto3";
package order.v1;
option go_package = "github.com/company/api-first-polyglot/proto/order/v1";
option python_package = "order.proto.v1";
option rust_package = "order::proto::v1";
// Order service API v1
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
rpc UpdateOrder(UpdateOrderRequest) returns (UpdateOrderResponse);
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse);
}
// Order messages
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
Address shipping_address = 3;
PaymentMethod payment_method = 4;
}
message CreateOrderResponse {
string order_id = 1;
OrderStatus status = 2;
int64 created_at = 3;
}
message GetOrderRequest {
string order_id = 1;
}
message GetOrderResponse {
Order order = 1;
}
message ListOrdersRequest {
string customer_id = 1;
int32 page_size = 2;
string page_token = 3;
}
message ListOrdersResponse {
repeated Order orders = 1;
string next_page_token = 2;
}
message UpdateOrderRequest {
string order_id = 1;
OrderStatus status = 2;
}
message UpdateOrderResponse {
Order order = 1;
}
message CancelOrderRequest {
string order_id = 1;
string reason = 2;
}
message CancelOrderResponse {
bool success = 1;
}
// Domain models
message Order {
string order_id = 1;
string customer_id = 2;
OrderStatus status = 3;
repeated OrderItem items = 4;
Money total_amount = 5;
Address shipping_address = 6;
int64 created_at = 7;
int64 updated_at = 8;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
Money price = 3;
Money subtotal = 4;
}
message Address {
string street = 1;
string city = 2;
string state = 3;
string zip_code = 4;
string country = 5;
}
message Money {
string currency = 1;
int64 amount_cents = 2;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_PROCESSING = 3;
ORDER_STATUS_SHIPPED = 4;
ORDER_STATUS_DELIVERED = 5;
ORDER_STATUS_CANCELLED = 6;
}
enum PaymentMethod {
PAYMENT_METHOD_UNSPECIFIED = 0;
PAYMENT_METHOD_CREDIT_CARD = 1;
PAYMENT_METHOD_PAYPAL = 2;
PAYMENT_METHOD_BANK_TRANSFER = 3;
}
4.2) Order Service v2 (Backward Compatible)¶
Create proto/order/v2/order.proto:
syntax = "proto3";
package order.v2;
option go_package = "github.com/company/api-first-polyglot/proto/order/v2";
option python_package = "order.proto.v2";
option rust_package = "order::proto::v2";
// Order service API v2 - Backward compatible with v1
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
rpc UpdateOrder(UpdateOrderRequest) returns (UpdateOrderResponse);
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse);
// New v2 methods
rpc GetOrderHistory(GetOrderHistoryRequest) returns (GetOrderHistoryResponse);
rpc BatchCreateOrders(BatchCreateOrdersRequest) returns (BatchCreateOrdersResponse);
}
// V2 extends v1 messages (backward compatible)
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
Address shipping_address = 3;
PaymentMethod payment_method = 4;
// New v2 fields (optional for backward compatibility)
OrderMetadata metadata = 5;
repeated string tags = 6;
string source = 7; // web, mobile, api
}
message CreateOrderResponse {
string order_id = 1;
OrderStatus status = 2;
int64 created_at = 3;
// New v2 fields
OrderMetadata metadata = 4;
string estimated_delivery_date = 5;
}
message GetOrderRequest {
string order_id = 1;
// New v2 fields
bool include_history = 2;
bool include_metadata = 3;
}
message GetOrderResponse {
Order order = 1;
// New v2 fields
repeated OrderEvent history = 2;
}
// New v2 messages
message GetOrderHistoryRequest {
string order_id = 1;
int32 limit = 2;
}
message GetOrderHistoryResponse {
repeated OrderEvent events = 1;
}
message BatchCreateOrdersRequest {
repeated CreateOrderRequest orders = 1;
}
message BatchCreateOrdersResponse {
repeated CreateOrderResponse orders = 1;
int32 success_count = 2;
int32 failure_count = 3;
}
message OrderMetadata {
map<string, string> custom_fields = 1;
string campaign_id = 2;
string referrer = 3;
}
message OrderEvent {
string event_id = 1;
string event_type = 2;
int64 timestamp = 3;
map<string, string> data = 4;
}
// Reuse v1 messages (import or redefine)
message Order {
string order_id = 1;
string customer_id = 2;
OrderStatus status = 3;
repeated OrderItem items = 4;
Money total_amount = 5;
Address shipping_address = 6;
int64 created_at = 7;
int64 updated_at = 8;
// New v2 fields
OrderMetadata metadata = 9;
repeated string tags = 10;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
Money price = 3;
Money subtotal = 4;
// New v2 fields
string variant_id = 5;
map<string, string> attributes = 6;
}
message Address {
string street = 1;
string city = 2;
string state = 3;
string zip_code = 4;
string country = 5;
// New v2 fields
string address_line_2 = 6;
string phone_number = 7;
}
message Money {
string currency = 1;
int64 amount_cents = 2;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_PROCESSING = 3;
ORDER_STATUS_SHIPPED = 4;
ORDER_STATUS_DELIVERED = 5;
ORDER_STATUS_CANCELLED = 6;
// New v2 statuses
ORDER_STATUS_REFUNDED = 7;
ORDER_STATUS_PARTIALLY_REFUNDED = 8;
}
enum PaymentMethod {
PAYMENT_METHOD_UNSPECIFIED = 0;
PAYMENT_METHOD_CREDIT_CARD = 1;
PAYMENT_METHOD_PAYPAL = 2;
PAYMENT_METHOD_BANK_TRANSFER = 3;
// New v2 payment methods
PAYMENT_METHOD_APPLE_PAY = 4;
PAYMENT_METHOD_GOOGLE_PAY = 5;
}
5) Python Order Service with Versioning¶
Create services/order-service/app/api_v1.py:
"""Order service API v1 implementation."""
from typing import List, Optional
import grpc
from concurrent import futures
import logging
from proto.order.v1 import order_pb2
from proto.order.v1 import order_pb2_grpc
from prometheus_client import Counter, Histogram
api_metrics = {
"requests_total": Counter("api_requests_total", "API requests", ["version", "method"]),
"request_duration": Histogram("api_request_duration_seconds", "Request duration", ["version", "method"]),
}
class OrderServiceV1(order_pb2_grpc.OrderServiceServicer):
"""Order service v1 implementation."""
def __init__(self, order_repository):
self.repository = order_repository
def CreateOrder(self, request, context):
"""Create order (v1)."""
api_metrics["requests_total"].labels(version="v1", method="CreateOrder").inc()
# Validate request
if not request.customer_id:
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
context.set_details("customer_id is required")
return order_pb2.CreateOrderResponse()
# Create order
order = self.repository.create_order(
customer_id=request.customer_id,
items=[self._convert_item(item) for item in request.items],
shipping_address=self._convert_address(request.shipping_address),
payment_method=request.payment_method
)
return order_pb2.CreateOrderResponse(
order_id=order.order_id,
status=order.status,
created_at=int(order.created_at.timestamp() * 1000)
)
def GetOrder(self, request, context):
"""Get order (v1)."""
api_metrics["requests_total"].labels(version="v1", method="GetOrder").inc()
order = self.repository.get_order(request.order_id)
if not order:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details(f"Order {request.order_id} not found")
return order_pb2.GetOrderResponse()
return order_pb2.GetOrderResponse(
order=self._convert_order(order)
)
def ListOrders(self, request, context):
"""List orders (v1)."""
api_metrics["requests_total"].labels(version="v1", method="ListOrders").inc()
orders = self.repository.list_orders(
customer_id=request.customer_id,
page_size=request.page_size or 20,
page_token=request.page_token
)
return order_pb2.ListOrdersResponse(
orders=[self._convert_order(order) for order in orders["items"]],
next_page_token=orders.get("next_page_token", "")
)
def UpdateOrder(self, request, context):
"""Update order (v1)."""
api_metrics["requests_total"].labels(version="v1", method="UpdateOrder").inc()
order = self.repository.update_order(
order_id=request.order_id,
status=request.status
)
return order_pb2.UpdateOrderResponse(
order=self._convert_order(order)
)
def CancelOrder(self, request, context):
"""Cancel order (v1)."""
api_metrics["requests_total"].labels(version="v1", method="CancelOrder").inc()
success = self.repository.cancel_order(
order_id=request.order_id,
reason=request.reason
)
return order_pb2.CancelOrderResponse(success=success)
def _convert_order(self, order) -> order_pb2.Order:
"""Convert domain order to protobuf."""
return order_pb2.Order(
order_id=order.order_id,
customer_id=order.customer_id,
status=order.status,
items=[self._convert_item(item) for item in order.items],
total_amount=order_pb2.Money(
currency=order.total_amount.currency,
amount_cents=order.total_amount.amount_cents
),
shipping_address=self._convert_address(order.shipping_address),
created_at=int(order.created_at.timestamp() * 1000),
updated_at=int(order.updated_at.timestamp() * 1000)
)
def _convert_item(self, item) -> order_pb2.OrderItem:
"""Convert domain item to protobuf."""
return order_pb2.OrderItem(
product_id=item.product_id,
quantity=item.quantity,
price=order_pb2.Money(
currency=item.price.currency,
amount_cents=item.price.amount_cents
),
subtotal=order_pb2.Money(
currency=item.subtotal.currency,
amount_cents=item.subtotal.amount_cents
)
)
def _convert_address(self, address) -> order_pb2.Address:
"""Convert domain address to protobuf."""
return order_pb2.Address(
street=address.street,
city=address.city,
state=address.state,
zip_code=address.zip_code,
country=address.country
)
def serve(port: int = 50051):
"""Start gRPC server."""
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
order_pb2_grpc.add_OrderServiceServicer_to_server(
OrderServiceV1(order_repository),
server
)
server.add_insecure_port(f"[::]:{port}")
server.start()
logging.info(f"Order service v1 started on port {port}")
server.wait_for_termination()
Create services/order-service/app/api_v2.py:
"""Order service API v2 implementation with backward compatibility."""
from typing import List, Optional
import grpc
from concurrent import futures
import logging
from proto.order.v2 import order_pb2
from proto.order.v2 import order_pb2_grpc
from services.order_service.app.api_v1 import OrderServiceV1 # Reuse v1 logic
from prometheus_client import Counter, Histogram
api_metrics = {
"requests_total": Counter("api_requests_total", "API requests", ["version", "method"]),
"request_duration": Histogram("api_request_duration_seconds", "Request duration", ["version", "method"]),
"v2_features_used": Counter("api_v2_features_used_total", "V2 features used", ["feature"]),
}
class OrderServiceV2(order_pb2_grpc.OrderServiceServicer):
"""Order service v2 implementation with backward compatibility."""
def __init__(self, order_repository, v1_service: OrderServiceV1):
self.repository = order_repository
self.v1_service = v1_service # Reuse v1 implementation
def CreateOrder(self, request, context):
"""Create order (v2) - backward compatible with v1."""
api_metrics["requests_total"].labels(version="v2", method="CreateOrder").inc()
# Check if v2 features are used
if request.metadata or request.tags or request.source:
api_metrics["v2_features_used"].labels(feature="metadata").inc()
# Convert v2 request to v1 format for core logic
v1_request = self._convert_v2_to_v1_request(request)
# Use v1 service for core logic (backward compatibility)
v1_response = self.v1_service.CreateOrder(v1_request, context)
# Enhance response with v2 fields
return order_pb2.CreateOrderResponse(
order_id=v1_response.order_id,
status=v1_response.status,
created_at=v1_response.created_at,
metadata=request.metadata if request.metadata else None,
estimated_delivery_date=self._calculate_delivery_date()
)
def GetOrder(self, request, context):
"""Get order (v2) - backward compatible with v1."""
api_metrics["requests_total"].labels(version="v2", method="GetOrder").inc()
# Get order using v1 service
v1_request = order_pb2.GetOrderRequest(order_id=request.order_id)
v1_response = self.v1_service.GetOrder(v1_request, context)
if context.code() != grpc.StatusCode.OK:
return order_pb2.GetOrderResponse()
# Enhance with v2 fields
response = order_pb2.GetOrderResponse(
order=self._enhance_order_v2(v1_response.order)
)
# Add history if requested
if request.include_history:
response.history.extend(self._get_order_history(request.order_id))
api_metrics["v2_features_used"].labels(feature="history").inc()
return response
def GetOrderHistory(self, request, context):
"""Get order history (v2 only)."""
api_metrics["requests_total"].labels(version="v2", method="GetOrderHistory").inc()
api_metrics["v2_features_used"].labels(feature="history").inc()
events = self.repository.get_order_history(
order_id=request.order_id,
limit=request.limit or 100
)
return order_pb2.GetOrderHistoryResponse(
events=[self._convert_event(event) for event in events]
)
def BatchCreateOrders(self, request, context):
"""Batch create orders (v2 only)."""
api_metrics["requests_total"].labels(version="v2", method="BatchCreateOrders").inc()
api_metrics["v2_features_used"].labels(feature="batch").inc()
results = []
success_count = 0
failure_count = 0
for order_request in request.orders:
try:
response = self.CreateOrder(order_request, context)
results.append(response)
success_count += 1
except Exception as e:
logging.error(f"Failed to create order: {e}")
failure_count += 1
return order_pb2.BatchCreateOrdersResponse(
orders=results,
success_count=success_count,
failure_count=failure_count
)
def _convert_v2_to_v1_request(self, v2_request):
"""Convert v2 request to v1 format."""
from proto.order.v1 import order_pb2 as v1_pb2
return v1_pb2.CreateOrderRequest(
customer_id=v2_request.customer_id,
items=v2_request.items,
shipping_address=v2_request.shipping_address,
payment_method=v2_request.payment_method
)
def _enhance_order_v2(self, v1_order):
"""Enhance v1 order with v2 fields."""
order = self.repository.get_order(v1_order.order_id)
return order_pb2.Order(
order_id=v1_order.order_id,
customer_id=v1_order.customer_id,
status=v1_order.status,
items=[self._enhance_item_v2(item) for item in v1_order.items],
total_amount=v1_order.total_amount,
shipping_address=self._enhance_address_v2(v1_order.shipping_address),
created_at=v1_order.created_at,
updated_at=v1_order.updated_at,
metadata=order.metadata if hasattr(order, 'metadata') else None,
tags=order.tags if hasattr(order, 'tags') else []
)
def _enhance_item_v2(self, v1_item):
"""Enhance v1 item with v2 fields."""
return order_pb2.OrderItem(
product_id=v1_item.product_id,
quantity=v1_item.quantity,
price=v1_item.price,
subtotal=v1_item.subtotal,
variant_id="", # Get from repository
attributes={} # Get from repository
)
def _enhance_address_v2(self, v1_address):
"""Enhance v1 address with v2 fields."""
return order_pb2.Address(
street=v1_address.street,
city=v1_address.city,
state=v1_address.state,
zip_code=v1_address.zip_code,
country=v1_address.country,
address_line_2="", # Get from repository
phone_number="" # Get from repository
)
def _get_order_history(self, order_id: str) -> List[order_pb2.OrderEvent]:
"""Get order history events."""
events = self.repository.get_order_history(order_id)
return [self._convert_event(event) for event in events]
def _convert_event(self, event) -> order_pb2.OrderEvent:
"""Convert domain event to protobuf."""
return order_pb2.OrderEvent(
event_id=event.event_id,
event_type=event.event_type,
timestamp=int(event.timestamp.timestamp() * 1000),
data=event.data
)
def _calculate_delivery_date(self) -> str:
"""Calculate estimated delivery date."""
from datetime import datetime, timedelta
delivery = datetime.utcnow() + timedelta(days=5)
return delivery.isoformat()
6) Go Payment Service¶
Create services/payment-service/main.go:
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "github.com/company/api-first-polyglot/proto/payment/v1"
)
type PaymentService struct {
pb.UnimplementedPaymentServiceServer
repository PaymentRepository
}
func (s *PaymentService) ProcessPayment(ctx context.Context, req *pb.ProcessPaymentRequest) (*pb.ProcessPaymentResponse, error) {
// Validate request
if req.OrderId == "" {
return nil, status.Error(codes.InvalidArgument, "order_id is required")
}
// Process payment
payment, err := s.repository.ProcessPayment(ctx, &Payment{
OrderID: req.OrderId,
Amount: req.Amount,
Method: req.Method,
Details: req.Details,
})
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.ProcessPaymentResponse{
PaymentId: payment.PaymentID,
Status: pb.PaymentStatus_PAYMENT_STATUS_COMPLETED,
ProcessedAt: payment.ProcessedAt.Unix(),
TransactionId: payment.TransactionID,
}, nil
}
func (s *PaymentService) RefundPayment(ctx context.Context, req *pb.RefundPaymentRequest) (*pb.RefundPaymentResponse, error) {
refund, err := s.repository.RefundPayment(ctx, &Refund{
PaymentID: req.PaymentId,
Amount: req.Amount,
Reason: req.Reason,
})
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.RefundPaymentResponse{
RefundId: refund.RefundID,
Success: true,
ProcessedAt: refund.ProcessedAt.Unix(),
}, nil
}
func (s *PaymentService) GetPaymentStatus(ctx context.Context, req *pb.GetPaymentStatusRequest) (*pb.GetPaymentStatusResponse, error) {
payment, err := s.repository.GetPayment(ctx, req.PaymentId)
if err != nil {
return nil, status.Error(codes.NotFound, err.Error())
}
return &pb.GetPaymentStatusResponse{
Payment: &pb.Payment{
PaymentId: payment.PaymentID,
OrderId: payment.OrderID,
Amount: payment.Amount,
Status: payment.Status,
Method: payment.Method,
CreatedAt: payment.CreatedAt.Unix(),
ProcessedAt: payment.ProcessedAt.Unix(),
TransactionId: payment.TransactionID,
},
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50052")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterPaymentServiceServer(s, &PaymentService{
repository: NewPaymentRepository(),
})
log.Printf("Payment service listening on :50052")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
7) Rust Inventory Service¶
Create services/inventory-service/src/main.rs:
use tonic::{transport::Server, Request, Response, Status};
use inventory::proto::inventory::v1::{
inventory_service_server::{InventoryService, InventoryServiceServer},
ReserveItemsRequest, ReserveItemsResponse,
ReleaseReservationRequest, ReleaseReservationResponse,
GetInventoryRequest, GetInventoryResponse,
};
pub mod proto {
pub mod inventory {
pub mod v1 {
tonic::include_proto!("inventory.v1");
}
}
}
#[derive(Debug, Default)]
pub struct InventoryServiceImpl {
repository: InventoryRepository,
}
#[tonic::async_trait]
impl InventoryService for InventoryServiceImpl {
async fn reserve_items(
&self,
request: Request<ReserveItemsRequest>,
) -> Result<Response<ReserveItemsResponse>, Status> {
let req = request.into_inner();
// Validate request
if req.items.is_empty() {
return Err(Status::invalid_argument("items cannot be empty"));
}
// Reserve items
let reservation = self.repository.reserve_items(&req.items).await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(ReserveItemsResponse {
reservation_id: reservation.id,
success: true,
}))
}
async fn release_reservation(
&self,
request: Request<ReleaseReservationRequest>,
) -> Result<Response<ReleaseReservationResponse>, Status> {
let req = request.into_inner();
self.repository.release_reservation(&req.reservation_id).await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(ReleaseReservationResponse {
success: true,
}))
}
async fn get_inventory(
&self,
request: Request<GetInventoryRequest>,
) -> Result<Response<GetInventoryResponse>, Status> {
let req = request.into_inner();
let inventory = self.repository.get_inventory(&req.product_id).await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(GetInventoryResponse {
product_id: inventory.product_id,
available_quantity: inventory.available_quantity,
reserved_quantity: inventory.reserved_quantity,
}))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50053".parse()?;
let service = InventoryServiceImpl::default();
Server::builder()
.add_service(InventoryServiceServer::new(service))
.serve(addr)
.await?;
Ok(())
}
8) API Version Manager¶
Create governance/version_manager.py:
"""API version management and routing."""
from typing import Dict, List, Optional
from dataclasses import dataclass
from enum import Enum
from datetime import datetime
from prometheus_client import Counter, Gauge
version_metrics = {
"api_versions_active": Gauge("api_versions_active", "Active API versions", ["service"]),
"version_requests": Counter("api_version_requests_total", "Version requests", ["service", "version"]),
"deprecated_version_usage": Counter("api_deprecated_version_usage_total", "Deprecated version usage", ["service", "version"]),
}
class VersionStatus(Enum):
"""API version status."""
ACTIVE = "active"
DEPRECATED = "deprecated"
SUNSET = "sunset"
@dataclass
class APIVersion:
"""API version definition."""
service_name: str
version: str
status: VersionStatus
released_at: datetime
deprecated_at: Optional[datetime] = None
sunset_at: Optional[datetime] = None
supported_clients: List[str] = None
breaking_changes: List[str] = None
class APIVersionManager:
"""Manages API versions and routing."""
def __init__(self):
self.versions: Dict[str, List[APIVersion]] = {} # service -> versions
self.default_versions: Dict[str, str] = {} # service -> default version
def register_version(self, version: APIVersion):
"""Register an API version."""
if version.service_name not in self.versions:
self.versions[version.service_name] = []
self.versions[version.service_name].append(version)
self.versions[version.service_name].sort(key=lambda v: v.version, reverse=True)
# Set as default if first version
if version.service_name not in self.default_versions:
self.default_versions[version.service_name] = version.version
version_metrics["api_versions_active"].labels(service=version.service_name).inc()
def get_version(
self,
service_name: str,
requested_version: Optional[str] = None
) -> Optional[APIVersion]:
"""Get API version for service."""
if service_name not in self.versions:
return None
versions = self.versions[service_name]
if requested_version:
# Find specific version
for version in versions:
if version.version == requested_version:
self._track_version_usage(service_name, requested_version, version.status)
return version
return None
else:
# Return default (latest active)
default = self.default_versions.get(service_name)
if default:
return self.get_version(service_name, default)
# Fallback to latest active
for version in versions:
if version.status == VersionStatus.ACTIVE:
return version
return None
def get_supported_versions(self, service_name: str) -> List[APIVersion]:
"""Get all supported versions for a service."""
if service_name not in self.versions:
return []
return [
v for v in self.versions[service_name]
if v.status in [VersionStatus.ACTIVE, VersionStatus.DEPRECATED]
]
def deprecate_version(
self,
service_name: str,
version: str,
deprecated_at: datetime,
sunset_at: Optional[datetime] = None
):
"""Deprecate an API version."""
api_version = self.get_version(service_name, version)
if api_version:
api_version.status = VersionStatus.DEPRECATED
api_version.deprecated_at = deprecated_at
api_version.sunset_at = sunset_at
def _track_version_usage(
self,
service_name: str,
version: str,
status: VersionStatus
):
"""Track version usage metrics."""
version_metrics["version_requests"].labels(
service=service_name,
version=version
).inc()
if status == VersionStatus.DEPRECATED:
version_metrics["deprecated_version_usage"].labels(
service=service_name,
version=version
).inc()
9) Compatibility Checker¶
Create governance/compatibility_checker.py:
"""API compatibility checking."""
from typing import Dict, List, Tuple, Any
from dataclasses import dataclass
from enum import Enum
import difflib
from prometheus_client import Counter
compatibility_metrics = {
"compatibility_checks": Counter("api_compatibility_checks_total", "Compatibility checks", ["result"]),
"breaking_changes_detected": Counter("api_breaking_changes_detected_total", "Breaking changes", ["change_type"]),
}
class ChangeType(Enum):
"""Change type classification."""
BACKWARD_COMPATIBLE = "backward_compatible"
BREAKING = "breaking"
ADDITIVE = "additive"
@dataclass
class SchemaChange:
"""Schema change definition."""
change_type: ChangeType
field_path: str
description: str
impact: str # low, medium, high
class CompatibilityChecker:
"""Checks API compatibility between versions."""
def check_compatibility(
self,
old_schema: Dict[str, Any],
new_schema: Dict[str, Any]
) -> Tuple[bool, List[SchemaChange]]:
"""Check compatibility between schemas."""
changes = []
# Check for breaking changes
breaking_changes = self._detect_breaking_changes(old_schema, new_schema)
changes.extend(breaking_changes)
# Check for additive changes
additive_changes = self._detect_additive_changes(old_schema, new_schema)
changes.extend(additive_changes)
is_compatible = len(breaking_changes) == 0
compatibility_metrics["compatibility_checks"].labels(
result="compatible" if is_compatible else "incompatible"
).inc()
if breaking_changes:
for change in breaking_changes:
compatibility_metrics["breaking_changes_detected"].labels(
change_type=change.change_type.value
).inc()
return is_compatible, changes
def _detect_breaking_changes(
self,
old_schema: Dict[str, Any],
new_schema: Dict[str, Any]
) -> List[SchemaChange]:
"""Detect breaking changes."""
breaking_changes = []
# Check for removed fields
old_fields = self._extract_fields(old_schema)
new_fields = self._extract_fields(new_schema)
removed_fields = set(old_fields) - set(new_fields)
for field in removed_fields:
breaking_changes.append(SchemaChange(
change_type=ChangeType.BREAKING,
field_path=field,
description=f"Field {field} was removed",
impact="high"
))
# Check for type changes
for field in old_fields & new_fields:
old_type = self._get_field_type(old_schema, field)
new_type = self._get_field_type(new_schema, field)
if old_type != new_type and not self._is_compatible_type(old_type, new_type):
breaking_changes.append(SchemaChange(
change_type=ChangeType.BREAKING,
field_path=field,
description=f"Field {field} type changed from {old_type} to {new_type}",
impact="high"
))
# Check for required field changes
old_required = self._get_required_fields(old_schema)
new_required = self._get_required_fields(new_schema)
newly_required = set(new_required) - set(old_required)
for field in newly_required:
breaking_changes.append(SchemaChange(
change_type=ChangeType.BREAKING,
field_path=field,
description=f"Field {field} is now required",
impact="medium"
))
return breaking_changes
def _detect_additive_changes(
self,
old_schema: Dict[str, Any],
new_schema: Dict[str, Any]
) -> List[SchemaChange]:
"""Detect additive (non-breaking) changes."""
additive_changes = []
old_fields = self._extract_fields(old_schema)
new_fields = self._extract_fields(new_schema)
added_fields = set(new_fields) - set(old_fields)
for field in added_fields:
# Check if field is optional
if not self._is_required(new_schema, field):
additive_changes.append(SchemaChange(
change_type=ChangeType.ADDITIVE,
field_path=field,
description=f"Optional field {field} was added",
impact="low"
))
return additive_changes
def _extract_fields(self, schema: Dict[str, Any], prefix: str = "") -> List[str]:
"""Extract all field paths from schema."""
fields = []
if "properties" in schema:
for field_name, field_schema in schema["properties"].items():
field_path = f"{prefix}.{field_name}" if prefix else field_name
fields.append(field_path)
# Recursively extract nested fields
if "properties" in field_schema:
fields.extend(self._extract_fields(field_schema, field_path))
return fields
def _get_field_type(self, schema: Dict[str, Any], field_path: str) -> Optional[str]:
"""Get field type from schema."""
parts = field_path.split(".")
current = schema
for part in parts:
if "properties" in current and part in current["properties"]:
current = current["properties"][part]
else:
return None
return current.get("type")
def _is_compatible_type(self, old_type: str, new_type: str) -> bool:
"""Check if type change is compatible."""
# Integer to number is compatible
if old_type == "integer" and new_type == "number":
return True
# String to string is compatible
if old_type == "string" and new_type == "string":
return True
return False
def _get_required_fields(self, schema: Dict[str, Any]) -> List[str]:
"""Get required fields from schema."""
return schema.get("required", [])
def _is_required(self, schema: Dict[str, Any], field_path: str) -> bool:
"""Check if field is required."""
parts = field_path.split(".")
current = schema
for i, part in enumerate(parts):
if "properties" in current and part in current["properties"]:
if i == len(parts) - 1:
# Last part - check if required
required = current.get("required", [])
return part in required
else:
current = current["properties"][part]
else:
return False
return False
10) Kong Gateway Configuration¶
Create gateway/kong.yaml:
_format_version: "3.0"
services:
- name: order-service-v1
url: http://order-service:50051
routes:
- name: order-v1-route
paths:
- /api/v1/orders
strip_path: false
plugins:
- name: request-transformer
config:
add:
headers:
- "X-API-Version:v1"
- name: order-service-v2
url: http://order-service:50052
routes:
- name: order-v2-route
paths:
- /api/v2/orders
strip_path: false
plugins:
- name: request-transformer
config:
add:
headers:
- "X-API-Version:v2"
- name: cors
config:
origins:
- "*"
methods:
- GET
- POST
- PUT
- DELETE
- name: rate-limiting
config:
minute: 100
hour: 1000
plugins:
- name: prometheus
config:
per_consumer: true
status_code_metrics: true
latency_metrics: true
bandwidth_metrics: true
upstream_health_metrics: true
11) Testing the System¶
11.1) Start Services¶
# Generate protobuf code
./scripts/generate_proto.sh
# Start services
docker compose up -d
# Start Kong
docker compose up -d kong
11.2) Test API Versioning¶
import grpc
from proto.order.v1 import order_pb2, order_pb2_grpc
from proto.order.v2 import order_pb2 as v2_pb2, order_pb2_grpc as v2_grpc
# Test v1 API
channel = grpc.insecure_channel('localhost:50051')
stub = order_pb2_grpc.OrderServiceStub(channel)
request = order_pb2.CreateOrderRequest(
customer_id="cust123",
items=[order_pb2.OrderItem(product_id="prod1", quantity=2)],
shipping_address=order_pb2.Address(
street="123 Main St",
city="City",
state="State",
zip_code="12345",
country="USA"
)
)
response = stub.CreateOrder(request)
print(f"V1 Order ID: {response.order_id}")
# Test v2 API (backward compatible)
v2_channel = grpc.insecure_channel('localhost:50052')
v2_stub = v2_grpc.OrderServiceStub(v2_channel)
v2_request = v2_pb2.CreateOrderRequest(
customer_id="cust123",
items=[v2_pb2.OrderItem(product_id="prod1", quantity=2)],
shipping_address=v2_pb2.Address(
street="123 Main St",
city="City",
state="State",
zip_code="12345",
country="USA"
),
metadata=v2_pb2.OrderMetadata(custom_fields={"campaign": "summer2024"}),
tags=["priority", "express"],
source="web"
)
v2_response = v2_stub.CreateOrder(v2_request)
print(f"V2 Order ID: {v2_response.order_id}")
print(f"Estimated Delivery: {v2_response.estimated_delivery_date}")
12) Best Practices Integration Summary¶
This tutorial demonstrates:
- API Governance: Version management, backward compatibility, and breaking change detection
- Protocol Buffers: Type-safe, efficient serialization across languages
- Polyglot Interoperability: Python, Go, Rust services communicate via gRPC
- Service Decomposition: Domain-driven microservices with clear boundaries
Key Integration Points: - Protocol Buffers enable type-safe cross-language communication - API versioning maintains backward compatibility - Service decomposition creates clear service boundaries - Compatibility checker prevents breaking changes - Kong gateway routes requests to appropriate versions
13) Next Steps¶
- Add OpenAPI/Swagger generation from protobuf
- Implement API rate limiting and quotas
- Add API analytics and usage tracking
- Implement API key management
- Add API documentation generation
This tutorial demonstrates how multiple best practices integrate to create a scalable, type-safe, polyglot API platform with comprehensive versioning and backward compatibility.