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 theUserService
(: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:
-
Validate that the User exists by calling the User Service.
-
Validate that the Item exists and is in stock by calling the Inventory Service.
-
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
-
Start all services:
-
User Service (
:50051
) -
Inventory Service (
:50052
) -
Order Service (
:50053
)
-
-
Create a user:
grpcurl -plaintext -d '{"name":"Alice","email":"[email protected]"}' localhost:50051 user.UserService/CreateUser
-
Create an item:
grpcurl -plaintext -d '{"name":"Laptop","stock":10}' localhost:50052 inventory.InventoryService/CreateItem
-
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:
- Real-World GoLang Project: Car Management System
- Building GUI Applications with Fyne and Go (Golang)
- AWS Cognito Using Golang
- Google Go Programming for Beginners (golang)
- Building a module in Go (Golang)
- Go/Golang Professional Interview Questions
Thanks!