Building Microservices in Golang with gRPC and Protobuf

by Didin J. on Sep 02, 2025 Building Microservices in Golang with gRPC and Protobuf

Learn how to build gRPC microservices in Go with Protocol Buffers. Implement User, Inventory & Order services with service-to-service communication.

In today’s software landscape, microservices have become the go-to architecture for building scalable, modular, and maintainable applications. Instead of developing one large monolithic system, microservices break functionality into small, independent services that communicate with each other over lightweight protocols. This approach makes applications easier to scale, deploy, and evolve.

When it comes to building microservices, Golang (Go) is a top choice due to its simplicity, speed, and strong concurrency model. Combined with gRPC (Google Remote Procedure Call) and Protocol Buffers (Protobuf), Go becomes a powerhouse for building high-performance, type-safe, and schema-first APIs.

  • gRPC leverages HTTP/2, enabling features like streaming and multiplexing while offering significant performance gains over traditional REST/JSON APIs.

  • Protobuf provides a compact binary format and ensures a single source of truth for service contracts.

  • Together, they allow you to define clear service boundaries, achieve efficient communication, and maintain compatibility across services and clients written in different languages.

In this tutorial, you’ll learn step by step how to build microservices in Go using gRPC and Protobuf. We’ll define our service contracts with .proto files, generate Go code, implement services, and then wire them up into a small system. Finally, we’ll containerize everything with Docker and use Docker Compose for local development.

By the end, you’ll have a working set of microservices that can talk to each other using gRPC, along with an optional HTTP/JSON gateway for easy testing with tools like curl or Postman.


Prerequisites

Before we dive into building our Go microservices with gRPC and Protobuf, make sure you have the following tools installed on your system:

1. Go

  • Version 1.22+ is recommended.

  • You can download it from the official site: https://go.dev/dl

Verify installation:

go version

2. Protocol Buffers Compiler (protoc)

  • Required to compile .proto files into Go code.

  • Install version 3.21+: https://grpc.io/docs/protoc-installation/ 

    brew install protobuf

Verify installation:

protoc --version

3. Go Plugins for Protobuf and gRPC

Install the code generators for Go:

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

Make sure $GOPATH/bin (or $HOME/go/bin) is in your PATH.

4. Buf (Optional but Recommended)

Buf simplifies Protobuf development with linting, breaking-change detection, and code generation.

Install Buf:

# macOS
brew install bufbuild/buf/buf

# Linux
curl -sSL \
  https://github.com/bufbuild/buf/releases/latest/download/buf-Linux-x86_64 \
  -o /usr/local/bin/buf && chmod +x /usr/local/bin/buf

Verify installation:

buf --version

5. Docker & Docker Compose

We’ll containerize our services and run them together in a local environment.

  • Docker Installation Guide

  • Docker Compose Installation

Verify installation:

docker --version
docker compose version

✅ With these tools set up, you’re ready to start coding. Next, we’ll set up our project structure and initialize Go modules.


Project Setup & Structure

Before writing any code, let’s create a well-organized project layout. This makes it easier to separate concerns, keep services modular, and manage code generation.

1. Initialize a Go Module

First, create the project folder and initialize Go modules:

mkdir grpc-go-microservices && cd grpc-go-microservices
go mod init github.com/yourname/grpc-go-microservices

This creates a go.mod file that will track your dependencies.

2. Directory Layout

Here’s the directory structure we’ll use:

grpc-go-microservices/
├── buf.gen.yaml
├── buf.yaml
├── proto/                   # Protobuf definitions
│   ├── user/user.proto
│   ├── order/order.proto
│   └── inventory/inventory.proto
├── services/                # Each microservice
│   ├── user/
│   │   ├── main.go
│   │   ├── server/user_server.go
│   │   └── Dockerfile
│   ├── order/
│   │   ├── main.go
│   │   ├── server/order_server.go
│   │   └── Dockerfile
│   └── inventory/
│       ├── main.go
│       ├── server/inventory_server.go
│       └── Dockerfile
├── gateway/                 # Optional HTTP/JSON gateway
│   ├── main.go
│   └── Dockerfile
├── pkg/                     # Shared code
│   ├── interceptors/logging.go
│   └── config/config.go
├── gen/                     # Auto-generated code (from Protobuf)
├── go.mod
├── Makefile                 # Helper commands
└── docker-compose.yml       # Local orchestration

3. Buf Configuration Files

Buf simplifies managing .proto files. Add two configuration files at the root:

buf.yaml

version: v1
name: buf.build/your-username/djamware-microservices
deps: []

buf.gen.yaml

version: v1
plugins:
  - plugin: buf.build/protocolbuffers/go
    out: gen
    opt: paths=source_relative
  - plugin: buf.build/grpc/go
    out: gen
    opt: paths=source_relative

These configs tell Buf to:

  • Lint .proto files.

  • Generate Go, gRPC, and optional gRPC-Gateway code into gen/.

4. Makefile (Optional but Handy)

Add a Makefile to simplify common commands:

PROTO_DIR=proto
GEN_DIR=gen

.PHONY: proto
proto:
	buf generate

.PHONY: tidy
tidy:
	go mod tidy

.PHONY: run-usersvc
run-usersvc:
	go run ./services/usersvc

.PHONY: run-ordersvc
run-ordersvc:
	go run ./services/ordersvc

.PHONY: run-inventorysvc
run-inventorysvc:
	go run ./services/inventorysvc

5. Sample Proto

Create a new folder at the root project folder.

mkdir -p proto

create a proto/user.proto so Buf has something to compile:

syntax = "proto3";

package user;

option go_package = "djamware-microservices/gen/user;userpb";

service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  string id = 1;
  string name = 2;
  string email = 3;
}

Now you can regenerate code with:

make proto

✅ At this point, your project is structured and ready for Protobuf definitions.


Defining Protobuf APIs

Before writing any Go code, we need to define the API contracts for our microservices using Protocol Buffers (.proto files). Each microservice will have its own .proto file under the proto/ directory. These files describe the services, RPC methods, and message structures that clients and other services can use.

1. User Service (proto/user/user.proto)

syntax = "proto3";

package user;

option go_package = "gen/user;user";

// User message
message User {
  string id = 1;
  string name = 2;
  string email = 3;
}

// Requests and Responses
message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message CreateUserResponse {
  User user = 1;
}

message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  User user = 1;
}

// Service definition
service UserService {
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

2. Inventory Service (proto/inventory/inventory.proto)

syntax = "proto3";

package inventory;

option go_package = "gen/inventory;inventory";

// Inventory item
message Item {
  string id = 1;
  string name = 2;
  int32 quantity = 3;
}

// Requests and Responses
message CreateItemRequest {
  string name = 1;
  int32 quantity = 2;
}

message CreateItemResponse {
  Item item = 1;
}

message GetItemRequest {
  string id = 1;
}

message GetItemResponse {
  Item item = 1;
}

// Service definition
service InventoryService {
  rpc CreateItem(CreateItemRequest) returns (CreateItemResponse);
  rpc GetItem(GetItemRequest) returns (GetItemResponse);
}

3. Order Service (proto/order/order.proto)

The Order Service depends on both User and Inventory, so we import their .proto files.

syntax = "proto3";

package order;

option go_package = "gen/order;order";

message Order {
  string id = 1;
  string user_id = 2;
  string item_id = 3;
  int32 quantity = 4;
}

message CreateOrderRequest {
  string user_id = 1;
  string item_id = 2;
  int32 quantity = 3;
}

message CreateOrderResponse {
  Order order = 1;
}

message GetOrderRequest {
  string id = 1;
}

message GetOrderResponse {
  Order order = 1;
}

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
  rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
}

4. Regenerating the Code

Now regenerate the gRPC code with:

make proto

This will generate Go code under the gen/ directory for all three services.

✅ At this point, you have:

  • user.proto for user management

  • inventory.proto for managing stock

  • order.proto for handling purchases

Each service is isolated, but they can communicate through shared Protobuf definitions.


Implementing the User Service (gRPC Server + Business Logic in Go)

Now that we have generated the Go stubs from our Protobuf definitions, let’s implement the User Service. This will include a simple in-memory user repository, the gRPC server, and logic for creating and retrieving users.

1. Create the user-service Directory

Inside your project, add a folder:

mkdir -p services/user

Your structure now:

services/
  user/
    main.go

2. Implement the User Service

Open services/user/main.go and add:

package main

import (
	"context"
	"log"
	"net"
	"sync"

	pb "github.com/didinj/grpc-go-microservices/gen/proto/user" // Import the generated User proto package
	"github.com/google/uuid"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type userServer struct {
	pb.UnimplementedUserServiceServer
	mu    sync.RWMutex
	users map[string]*pb.User
}

func newUserServer() *userServer {
	return &userServer{
		users: make(map[string]*pb.User),
	}
}

func (s *userServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
	if req.GetName() == "" || req.GetEmail() == "" {
		return nil, status.Error(codes.InvalidArgument, "name and email are required")
	}

	id := uuid.NewString()
	u := &pb.User{
		Id:    id,
		Name:  req.GetName(),
		Email: req.GetEmail(),
	}

	s.mu.Lock()
	s.users[id] = u
	s.mu.Unlock()

	return &pb.CreateUserResponse{User: u}, nil
}

func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
	if req.GetId() == "" {
		return nil, status.Error(codes.InvalidArgument, "id is required")
	}

	s.mu.RLock()
	u, ok := s.users[req.GetId()]
	s.mu.RUnlock()

	if !ok {
		return nil, status.Errorf(codes.NotFound, "user %s not found", req.GetId())
	}

	return &pb.GetUserResponse{User: u}, nil
}

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterUserServiceServer(s, newUserServer())

	log.Println("✅ User service listening on :50051")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

3. Update the Import of the generated gen/proto/order/order.pb.go

import (
	reflect "reflect"
	sync "sync"
	unsafe "unsafe"

	inventory "github.com/didinj/grpc-go-microservices/gen/proto/inventory"
	user "github.com/didinj/grpc-go-microservices/gen/proto/user"
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)

3. Run the User Service

In your terminal:

cd services/user
go run main.go

You should see:

2025/09/02 10:44:48 ✅ User service listening on :50051

✅ At this point, we have a working User Service gRPC server with basic business logic.
We can create users and fetch them by ID using gRPC clients (which we’ll implement later for testing and service-to-service calls).


Implementing the Inventory Service

inventory.proto (for reference)

syntax = "proto3";

package inventory;

option go_package = "gen/inventory;inventory";

// Inventory item
message Item {
  string id = 1;
  string name = 2;
  int32 quantity = 3;
}

// Requests and Responses
message CreateItemRequest {
  string name = 1;
  int32 quantity = 2;
}

message CreateItemResponse {
  Item item = 1;
}

message GetItemRequest {
  string id = 1;
}

message GetItemResponse {
  Item item = 1;
}

// Service definition
service InventoryService {
  rpc CreateItem(CreateItemRequest) returns (CreateItemResponse);
  rpc GetItem(GetItemRequest) returns (GetItemResponse);
}

Go gRPC Server Implementation

services/inventory/main.go:

package main

import (
	"context"
	"log"
	"net"
	"sync"

	pb "github.com/didinj/grpc-go-microservices/gen/proto/inventory" // 👈 matches option go_package = "gen/inventory;inventory"
	"github.com/google/uuid"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/reflection"
	"google.golang.org/grpc/status"
)

type inventoryServer struct {
	pb.UnimplementedInventoryServiceServer
	mu    sync.RWMutex
	items map[string]*pb.Item
}

func newInventoryServer() *inventoryServer {
	return &inventoryServer{
		items: make(map[string]*pb.Item),
	}
}

func (s *inventoryServer) CreateItem(ctx context.Context, req *pb.CreateItemRequest) (*pb.CreateItemResponse, error) {
	if req.GetName() == "" {
		return nil, status.Error(codes.InvalidArgument, "name is required")
	}
	if req.GetQuantity() < 0 {
		return nil, status.Error(codes.InvalidArgument, "quantity must be >= 0")
	}

	id := uuid.NewString()
	item := &pb.Item{
		Id:       id,
		Name:     req.GetName(),
		Quantity: req.GetQuantity(),
	}

	s.mu.Lock()
	s.items[id] = item
	s.mu.Unlock()

	return &pb.CreateItemResponse{Item: item}, nil
}

func (s *inventoryServer) GetItem(ctx context.Context, req *pb.GetItemRequest) (*pb.GetItemResponse, error) {
	if req.GetId() == "" {
		return nil, status.Error(codes.InvalidArgument, "id is required")
	}

	s.mu.RLock()
	item, ok := s.items[req.GetId()]
	s.mu.RUnlock()

	if !ok {
		return nil, status.Errorf(codes.NotFound, "item %s not found", req.GetId())
	}

	return &pb.GetItemResponse{Item: item}, nil
}

func main() {
	lis, err := net.Listen("tcp", ":50052") // 👈 runs on a different port than UserService
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterInventoryServiceServer(s, newInventoryServer())

	// Enable reflection
	reflection.Register(s)

	log.Println("✅ Inventory service listening on :50052")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

Key Points

  • ✅ Follows the same structure as UserService.

  • ✅ Uses getters: req.GetName(), req.GetQuantity(), req.GetId().

  • ✅ Protects the items map with a mutex (sync.RWMutex).

  • ✅ Uses proper gRPC error codes (InvalidArgument, NotFound).

  • ✅ Runs on port :50052 so it doesn’t conflict with the UserService (:50051).


Implementing the Order Service

order.proto (for reference)

syntax = "proto3";

package order;

option go_package = "gen/order;order";

message Order {
  string id = 1;
  string user_id = 2;
  string item_id = 3;
  int32 quantity = 4;
}

message CreateOrderRequest {
  string user_id = 1;
  string item_id = 2;
  int32 quantity = 3;
}

message CreateOrderResponse {
  Order order = 1;
}

message GetOrderRequest {
  string id = 1;
}

message GetOrderResponse {
  Order order = 1;
}

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
  rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
}

Go gRPC Server Implementation

services/order/main.go:

package main

import (
	"context"
	"log"
	"net"
	"sync"

	invpb "github.com/didinj/grpc-go-microservices/gen/proto/inventory"
	orderpb "github.com/didinj/grpc-go-microservices/gen/proto/order"
	userpb "github.com/didinj/grpc-go-microservices/gen/proto/user"

	"github.com/google/uuid"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/reflection"
	"google.golang.org/grpc/status"
)

type orderServer struct {
	orderpb.UnimplementedOrderServiceServer
	mu     sync.RWMutex
	orders map[string]*orderpb.Order

	userClient userpb.UserServiceClient
	invClient  invpb.InventoryServiceClient
}

func newOrderServer(userClient userpb.UserServiceClient, invClient invpb.InventoryServiceClient) *orderServer {
	return &orderServer{
		orders:     make(map[string]*orderpb.Order),
		userClient: userClient,
		invClient:  invClient,
	}
}

func (s *orderServer) CreateOrder(ctx context.Context, req *orderpb.CreateOrderRequest) (*orderpb.CreateOrderResponse, error) {
	if req.GetUserId() == "" || req.GetItemId() == "" || req.GetQuantity() <= 0 {
		return nil, status.Error(codes.InvalidArgument, "user_id, item_id and positive quantity are required")
	}

	// ✅ Check user existence
	_, err := s.userClient.GetUser(ctx, &userpb.GetUserRequest{Id: req.GetUserId()})
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "user not found: %v", err)
	}

	// ✅ Check item existence
	itemResp, err := s.invClient.GetItem(ctx, &invpb.GetItemRequest{Id: req.GetItemId()})
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "item not found: %v", err)
	}

	// ✅ Check stock availability
	if itemResp.Item.GetQuantity() < req.GetQuantity() {
		return nil, status.Error(codes.FailedPrecondition, "not enough stock")
	}

	// ✅ Create order
	id := uuid.NewString()
	order := &orderpb.Order{
		Id:       id,
		UserId:   req.GetUserId(),
		ItemId:   req.GetItemId(),
		Quantity: req.GetQuantity(),
	}

	// Save order
	s.mu.Lock()
	s.orders[id] = order
	s.mu.Unlock()

	// ✅ Reduce stock (in-memory, simulated update)
	newQuantity := itemResp.Item.GetQuantity() - req.GetQuantity()
	itemResp.Item.Quantity = newQuantity

	s.mu.Lock()
	s.orders[id] = order
	s.mu.Unlock()

	return &orderpb.CreateOrderResponse{Order: order}, nil
}

func (s *orderServer) GetOrder(ctx context.Context, req *orderpb.GetOrderRequest) (*orderpb.GetOrderResponse, error) {
	ord, exists := s.orders[req.Id]
	if !exists {
		return nil, status.Errorf(codes.NotFound, "order %s not found", req.Id)
	}

	return &orderpb.GetOrderResponse{Order: ord}, nil
}

func main() {
	// Connect to UserService
	userConn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("failed to connect to user service: %v", err)
	}
	defer userConn.Close()
	userClient := userpb.NewUserServiceClient(userConn)

	// Connect to InventoryService
	invConn, err := grpc.Dial("localhost:50052", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("failed to connect to inventory service: %v", err)
	}
	defer invConn.Close()
	invClient := invpb.NewInventoryServiceClient(invConn)

	// Start OrderService
	lis, err := net.Listen("tcp", ":50053")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	orderpb.RegisterOrderServiceServer(s, newOrderServer(userClient, invClient))

	// Enable reflection
	reflection.Register(s)

	log.Println("✅ Order service listening on :50053")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

Key Points

  • OrderService depends on both UserService and InventoryService.

  • ✅ Uses grpc.Dial to connect to other microservices.

  • ✅ Validates user existence, item availability, and stock before creating an order.

  • ✅ Runs on port :50053.

  • ✅ Currently reduces stock in-memory (simulation).


Enabling gRPC Reflection in Go Services

1. Import the reflection package

In each service main.go (services/user/main.go, services/inventory/main.go, services/order/main.go), add:

import "google.golang.org/grpc/reflection"

2. Register reflection on the gRPC server

After creating the gRPC server, register reflection before calling Serve:

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterUserServiceServer(s, newUserServer())

	// Enable reflection
	reflection.Register(s)

	log.Println("✅ User service listening on :50051")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

Do the same in Inventory and Order services, changing the port and service registration accordingly.


Testing the Microservices with gRPCurl

Now that all three services are implemented, it’s time to test them. Instead of writing a client app right away, we’ll use grpcurl, a handy CLI tool for interacting with gRPC servers.

1. Install grpcurl

If you don’t already have it installed, grab it here:

# macOS (with Homebrew)
brew install grpcurl

# Ubuntu/Debian
sudo apt-get install grpcurl

# Or download from GitHub Releases
https://github.com/fullstorydev/grpcurl/releases

2. Running the Microservices

In separate terminals, start each service:

# Run User service
cd service/user
go main.go

# Run Inventory service
cd service/inventory
go main.go

# Run Order service
cd service/order
go main.go

By default, they’ll run on different ports (e.g., :50051, :50052, :50053).
Make sure they are all running before testing.

3. Testing the User Service

Create a new user:

grpcurl -plaintext -d '{
  "name": "Alice",
  "email": "[email protected]"
}' localhost:50051 user.UserService/CreateUser

Expected response:

{
  "user": {
    "id": "e2be9006-eda3-4673-a820-17d1a1668909",
    "name": "Alice",
    "email": "[email protected]"
  }
}

Retrieve the user:

grpcurl -plaintext -d '{"id": "e2be9006-eda3-4673-a820-17d1a1668909"}' localhost:50051 user.UserService/GetUser

Expected response:

{
  "user": {
    "id": "e2be9006-eda3-4673-a820-17d1a1668909",
    "name": "Alice",
    "email": "[email protected]"
  }
}

4. Testing the Inventory Service

Add an item to the inventory:

grpcurl -plaintext -d '{
  "name": "Laptop",
  "quantity": 10
}' localhost:50052 inventory.InventoryService/CreateItem

Expected response:

{
  "item": {
    "id": "879d887c-331a-40ff-b39c-72bf5b1cfe9c",
    "name": "Laptop",
    "quantity": 10
  }
}

Get item details:

grpcurl -plaintext -d '{"id": "879d887c-331a-40ff-b39c-72bf5b1cfe9c"}' localhost:50052 inventory.InventoryService/GetItem

Expected response:

{
  "item": {
    "id": "879d887c-331a-40ff-b39c-72bf5b1cfe9c",
    "name": "Laptop",
    "quantity": 10
  }
}

5. Testing the Order Service

Create a new order (requires existing user and item):

grpcurl -plaintext -d '{
  "user_id": "e2be9006-eda3-4673-a820-17d1a1668909",
  "item_id": "879d887c-331a-40ff-b39c-72bf5b1cfe9c",
  "quantity": 2
}' localhost:50053 order.OrderService/CreateOrder

Expected response:

{
  "order": {
    "id": "7508a063-b5d6-456c-84f1-c6eea92e5bfe",
    "userId": "e2be9006-eda3-4673-a820-17d1a1668909",
    "itemId": "879d887c-331a-40ff-b39c-72bf5b1cfe9c",
    "quantity": 2
  }
}

Get the order:

grpcurl -plaintext -d '{"id": "7508a063-b5d6-456c-84f1-c6eea92e5bfe"}' localhost:50053 order.OrderService/GetOrder

Expected response:

{
  "order": {
    "id": "7508a063-b5d6-456c-84f1-c6eea92e5bfe",
    "user_id": "e2be9006-eda3-4673-a820-17d1a1668909",
    "item_id": "879d887c-331a-40ff-b39c-72bf5b1cfe9c",
    "quantity": 2,
    "status": "CREATED"
  }
}

✅ At this point, you’ve successfully built and tested three gRPC microservices in Go!


Adding Service-to-Service Communication

Up until now, our User, Inventory, and Order services have been running independently. Each one can be tested using grpcurl. However, in a real microservices architecture, services often need to talk to each other.

For example:

  • When creating an Order, the Order Service should:

    1. Validate that the User exists by calling the User Service.

    2. Validate that the Item exists and is in stock by calling the Inventory Service.

    3. Only then create the order.

We’ll implement that flow using gRPC clients inside the Order Service.

Step 1: Connect to Other Services in service/order/main.go

Update the Order Service server struct to include gRPC clients for User and Inventory:

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"sync"

	"github.com/didinj/grpc-go-microservices/gen/proto/inventory"
	invpb "github.com/didinj/grpc-go-microservices/gen/proto/inventory"
	"github.com/didinj/grpc-go-microservices/gen/proto/order"
	orderpb "github.com/didinj/grpc-go-microservices/gen/proto/order"
	"github.com/didinj/grpc-go-microservices/gen/proto/user"
	userpb "github.com/didinj/grpc-go-microservices/gen/proto/user"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/reflection"
	"google.golang.org/grpc/status"
)

type orderServer struct {
	orderpb.UnimplementedOrderServiceServer
	mu     sync.RWMutex
	orders map[string]*orderpb.Order

	userClient userpb.UserServiceClient
	invClient  invpb.InventoryServiceClient
}

Step 2: Dial Other Services in main.go

In main.go of the Order Service, set up gRPC clients for User and Inventory:

func main() {
	// Dial User Service
	userConn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("failed to connect to user service: %v", err)
	}
	defer userConn.Close()
	userClient := user.NewUserServiceClient(userConn)

	// Dial Inventory Service
	inventoryConn, err := grpc.Dial("localhost:50052", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("failed to connect to inventory service: %v", err)
	}
	defer inventoryConn.Close()
	inventoryClient := inventory.NewInventoryServiceClient(inventoryConn)

	lis, err := net.Listen("tcp", ":50053")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	order.RegisterOrderServiceServer(s, &orderServer{
		orders:     make(map[string]*order.Order),
		userClient: userClient,
		invClient:  inventoryClient,
	})

	// Enable reflection
	reflection.Register(s)

	log.Println("✅ Order service listening on :50053")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

Step 3: Validate User and Inventory in CreateOrder

Modify the CreateOrder method:

func (s *orderServer) CreateOrder(ctx context.Context, req *order.CreateOrderRequest) (*order.CreateOrderResponse, error) {
	// 1. Check user exists
	_, err := s.userClient.GetUser(ctx, &user.GetUserRequest{Id: req.UserId})
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "user not found: %v", err)
	}

	// 2. Check item exists
	itemResp, err := s.invClient.GetItem(ctx, &inventory.GetItemRequest{Id: req.ItemId})
	if err != nil {
		return nil, status.Errorf(codes.FailedPrecondition, "item not found: %v", err)
	}

	if itemResp.Item.Quantity < req.Quantity {
		return nil, status.Errorf(codes.FailedPrecondition, "not enough stock")
	}

	// 3. Reduce stock (in real app, this should be transactional)
	itemResp.Item.Quantity -= req.Quantity

	// 4. Create order
	id := fmt.Sprintf("%d", len(s.orders)+1)
	ord := &order.Order{
		Id:       id,
		UserId:   req.UserId,
		ItemId:   req.ItemId,
		Quantity: req.Quantity,
	}
	s.orders[id] = ord

	return &order.CreateOrderResponse{Order: ord}, nil
}

Step 4: Test the Service-to-Service Flow

  1. Start all services:

    • User Service (:50051)

    • Inventory Service (:50052)

    • Order Service (:50053)

  2. Create a user

    grpcurl -plaintext -d '{"name":"Alice","email":"[email protected]"}' localhost:50051 user.UserService/CreateUser
  3. Create an item:

    grpcurl -plaintext -d '{"name":"Laptop","stock":10}' localhost:50052 inventory.InventoryService/CreateItem
  4. Create an order:

    grpcurl -plaintext -d '{"user_id":"1","item_id":"1","quantity":2}' localhost:50053 order.OrderService/CreateOrder

✅ Now the Order Service checks the User and the Inventory before creating an order.


Conclusion

In this tutorial, we built a simple yet powerful microservices architecture in Go using gRPC and Protocol Buffers. We started by defining .proto files to establish strong, language-neutral contracts between services, then generated Go code and implemented three core services:

  • User Service – for creating and fetching users.

  • Inventory Service – for managing items and their stock.

  • Order Service – for placing orders while validating users and checking inventory.

We also learned how to:

  • Enable server reflection to simplify testing with grpcurl.

  • Test services in isolation and in combination.

  • Add service-to-service communication, allowing the Order Service to talk to both User and Inventory Services securely and consistently.

With this foundation, you can expand your microservices ecosystem by:

  • Adding authentication & authorization (e.g., using gRPC interceptors + JWT).

  • Introducing databases for persistence instead of in-memory maps.

  • Using a service discovery mechanism or API gateway (e.g., Envoy or gRPC-Gateway).

  • Implementing observability with logging, metrics, and tracing.

By leveraging gRPC’s performance and strict contracts, you now have a scalable and maintainable base for building modern microservices in Go.

You can find the full source code on our GitHub.

That is just the basics. If you need more deep learning about the Golang (Go) language and frameworks, you can take the following cheap course:

Thanks!