dev.ms

Golang, gRPC & Protobuf stories

gRPC API, part 2: Business Logic

Posted at — Dec 22, 2019

In the first part I was talking about building simplest gRPC API. But the real world is complex and real APIs rarely look like a calculator. Today we will be looking at layered application which consists of number of parts or layers. Our task is to expose data from database to the world through gRPC API. This time we will be writing Order API.

Let’s go!

Application layers

Our application has several layers such as:

Transport: gRPC

Transport layer is a mechanism which passes data between data source (business logic) and client. It sends what it was told to send and it knows how to prepare data for transfer, i.e. encode/decode. We use gRPC, which uses HTTP/2 as transport.

This layer consists of two parts:

Business logic: service

Business logic does some meaningful work, it takes data somewhere, very often from database and processes it somehow. It can call other external (micro)services for any purposes. It can cache data and so on.

This part of code doesn’t know anything about how this processed data will be transferred to the client, i.e. it’s transport-independent and it exposes an interface which will be used by transport layer.

Database: Repo

Repo is one more part of our application which works directly with database and any other data store. It also exposes an interface which can be used by Business Logic.

All this layers all together look like this:

App layers

Connect business logic and transport

In the previous article, part called gRPC server implementation shows how easily server can be implemented, but that part didn’t include any interactions with service layer. So, let’s fix it!

Our app will have next file structure:

% tree
.
├── client
│   └── main.go
├── pb
│   ├── order.pb.go
│   └── order.proto
├── server
│   ├── grpc_server.go
│   └── main.go
└── service
    └── service.go

First of all we define API interface with Protobuf.

Protobuf service: order.proto

syntax = "proto3";
option go_package = "pb";

service OrderService {
  rpc Get(GetRequest) returns (GetResponse);
  rpc Add(AddRequest) returns (AddResponse);
}

message Item {
  int64 id = 1;
  string name = 2;
  int32 qty = 3;
}

message Order {
  int64 id = 1;
  string number = 2;
  string status = 3;
  int64 created_at = 4;
  repeated Item items = 5;
}

message GetRequest {
  int64 id = 1;
}

message GetResponse {
  Order order = 1;
}

message AddRequest {
  message Body {
    Order order = 1;
  }

  Body body = 1;
}

message AddResponse {
  int64 id = 1;
  string status = 2;
}

This API has two methods: * Get which returns an Order by ID. * Add which adds an Order.

Each of these methods accepts request object, message with <method>Request name and returns response, message with <method>Response. Later we’ll implement gRPC server and it will be a place where we connect our transport level and business logic, but first of all we have to define business logic itself. For simplicity we will not use repo, but we assume it used inside service.

Business logic: service.go

package service

import (
	"context"
	"errors"
	"strconv"
)

type (
	OrderItem struct {
		ID    int
		Name  string
		Qty   int
		State string
	}

	Order struct {
		ID      int
		Num     string
		Status  string
		Items   []OrderItem
		Comment string
	}

	// OrderSvc is an interface which represents business logic.
	OrderSvc interface {
		Get(context.Context, int) (*Order, error)
		Add(context.Context, *Order) (int, error)
	}
)

type osvc struct{}

func New() OrderSvc {
	return &osvc{}
}

// Get returns an Order by ID with OrderItems.
//
// Actually this function have to call repo, but for simplicity it
// returns faked data.
func (s *osvc) Get(ctx context.Context, id int) (*Order, error) {
	switch id {
	case 1, 2, 3:
		items := []OrderItem{}

		for i := 0; i < id; i++ {
			items = append(items, OrderItem{
				ID:   (i + 1) * 10,
				Name: "order_item_" + strconv.Itoa(i),
				Qty:  (i + 1) * 2,
			})
		}

		return &Order{
			ID:      id,
			Num:     "order_" + strconv.Itoa(id),
			Status:  "created",
			Items:   items,
			Comment: "none",
		}, nil
	}

	return nil, errors.New("order not found")
}

// Add does something to store order and returns faked added ID.
func (s *osvc) Add(ctx context.Context, o *Order) (int, error) {
	return 5, nil
}

Business logic exposes an interface OrderSvc which is different from pb.OrderServiceServer it even has similar methods.

gRPC server implementation: grpc_server.go

// server implements gRPC Server OrderServiceServer interface from
// pb/order.pb.go.
type server struct {
	osvc service.OrderSvc
}

func (s *server) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) {
	log.Printf("GET request: %#v\n", req)
	o, err := s.osvc.Get(ctx, int(req.Id)) // call business logic
	if err != nil {
		return nil, err
	}

	items := []*pb.Item{}

	// transform OrderItem into []*pb.Items field by field.
	for _, i := range o.Items {
		items = append(items, &pb.Item{
			Id:   int64(i.ID),
			Name: i.Name,
			Qty:  int32(i.Qty),
		})
	}

	// transform Order into *pb.Order field by field because
	// service.Order and pb.Order are two different structure types.
	order := &pb.Order{
		Id:        int64(o.ID),
		Number:    o.Num,
		Status:    o.Status,
		CreatedAt: time.Now().Unix(),
		Items:     items,
	}

	return &pb.GetResponse{
		Order: order,
	}, nil
}

func (s *server) Add(ctx context.Context, req *pb.AddRequest) (*pb.AddResponse, error) {
	log.Printf("ADD request: %#v\n", req)
	o := req.Body.Order

	// Here we have to done opposite work, convert pb.Order into service.Order.
	id, err := s.osvc.Add(ctx, &service.Order{
		ID:      int(o.Id), // as well as cast particular fields.
		Num:     o.Number,
		Status:  o.Status,
		Items:   nil, // skip it
		Comment: "new order",
	})
	if err != nil {
		return nil, err
	}

	return &pb.AddResponse{
		Id:     int64(id),
		Status: "added",
	}, nil
}

It’s time to build and run our server and client. First we start server:

% cd server
% go build && ./server

at this time it has nothing to say.

Then we’ll build and start client:

% cd client
% go buils && ./client

after we run these commands client will print next info:

% ./client
order:
ID: 2, Number: order_2, Status: created
    item: ID: 10, Name: order_item_0, Qty: 2
    item: ID: 20, Name: order_item_1, Qty: 4

new order ID: 5, Status: added

while server, in its turn, prints which request and responses it have got:

2019/12/15 13:02:35 GET request: &pb.GetRequest{Id:2, XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
2019/12/15 13:02:35 ADD request: &pb.AddRequest{Body:(*pb.AddRequest_Body)(0xc000174630), XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}

Conclusion

gRPC API is pretty simple and straightforward to implement, but at the same time it could be difficult to link transport and business logic, because of this it is necessary to transform all structures between service and transport layers.