4 Microservice project setup

This chapter covers

It is very normal to ask, “How should I structure my project?” before writing the first line of your Go microservice project. The answer to this question might seem difficult initially, but it is easy to apply some common software architecture patterns that help solve challenges such as building modular projects to have testable components. Let’s see how to apply those principles to a Go microservice project and perform some tests to see how gRPC endpoints work.

4.1 Hexagonal architecture

Hexagonal architecture (https://alistair.cockburn.us/hexagonal-architecture/), proposed by Alistair Cockburn in 2005, is an architectural pattern that aims to build loosely coupled application components that can be connected via ports and adapters. In this pattern, the consumer opens the application at a port via an adapter, and the output is sent through a port to an adapter. Therefore, hexagonal architecture is also known as a ports and adapters system. Using ports and adapters creates an abstraction layer that isolates the application’s core from external dependencies. Now that we understand the general components of hexagonal architecture, let’s dive deeper into each.

4.1.1 Application

An application is a technology-agnostic component that contains the business logic that orchestrates functionalities or use cases. A hexagon represents the application that receives write and read queries from the ports and sends them to external actors, such as database and third-party services, via ports. A hexagon visually represents multiple port/adapter combinations for an application and shows the difference between the left side (or driving side) and right side (or driven side).

4.1.2 Actors

Actors are designed to interact with humans, other applications, and any other software or hardware device. There are two types of actors: driver (or primary) and driven (or secondary).

Driver actors are responsible for triggering communication with the application to invoke a service on it. Command-line interfaces (CLIs), controllers, are good examples of driver actors since they take user input and send it to the application via a port.

Driven actors expect to see communication triggered by the application itself. For example, an application triggers a communication to save data into MySQL.

4.1.3 Ports

Ports are generally interfaces that contain information about interactions between an actor and an application. Driver ports have a set of actions, and actors should implement them. Driver ports contain a set of actions that the application provides and exposes to the public.

4.1.4 Adapters

Adapters deal primarily with transforming a request from an actor to an application, and vice versa. Data transformation helps the application understand the requests that come from actors. For example, a specific driver adapter can transform a technology-specific request into a call to an application service. In the same way, a driven adapter can convert a technology-agnostic request from the application into a technology-specific request on the driven port.

As you can see in figure 4.1, the application has a hexagon that contains business logic, and adapters can orchestrate the hexagon by using ports. CLI and web applications are two candidates for adapters; data is saved into MySQL or sent to another application.

Figure 4.1 Hexagonal architecture with ports allows external actors to use, implement, and orchestrate business logic.

Using gRPC makes implementing hexagonal architecture easier because we become familiar with adapters out of the box by using gRPC stubs to access other services. gRPC can also be used to handle business models with the help of proto messages, which is especially helpful for duplicating models between hexagonal layers for better portability. Now that we understand the overall picture of the hexagonal architecture, let’s look at how to start the implementation of a Go microservice.

4.2 Order service implementation

A clean architecture deserves a well-defined project structure. If we aim to use hexagonal architecture for microservices, having meaningful folder names that represent the isolation level is important. Let’s look at the proper folder structure for a microservice project that uses hexagonal architecture for clear isolation between modules, as shown in figure 4.2.

Figure 4.2 Project folder structure of a Go microservice written with hexagonal architecture

4.2.1 Project folders

While there are no written rules for a hexagonal architecture folder, the following folders are common in typical Go projects:

Application, port, and adapter folders can be located inside an internal folder to separate operational functionalities, such as infra and deployment from application core logic. A cmd folder can also define an application’s entry point, which also contains dependency injection mechanisms, such as preparing a database connection and passing it to the application layer. Finally, there can be utility folders, such as config, to provide a configuration structure so that consumers will know the possible parameters they can pass while running the application. Now that we understand what the folder structure looks like (see figure 4.2), let’s look at how to implement the project step by step.

4.2.2 Initializing a Go project

A Go module helps you to create projects for better modularity and easy dependency management. Use the following code to create a microservice project called order and initialize it as a Go project:

mkdir -p microservices/order
cd microservices/order
go mod init GitHub.com/<username>/microservices/order   

<username> is your Github username.

The go mod init command accepts a VCS URL to prepare the dependency structure. When you add this module as a dependency to another project, that project will resolve the available tag from the VCS you provided for module initialization. After the initialization, the go.mod file will be created, and initially it will contain only module information and the supported Go version:

module github.com/huseyinbabal/microservices/order  
 
go 1.17                                             

Base URL of the module

Supported Go version

As a final step for initialization, go to the order folder and create the following folders. Notice “-p” is used for creating parent folders that do not exist:

mkdir cmd
mkdir config
mkdir -p internal/adapters/db
mkdir -p internal/adapters/grpc
mkdir -p internal/application/core/api
mkdir -p internal/application/core/domain
mkdir -p internal/ports

4.2.3 Implementing the application core

Even though there was only one hexagonal layer in previous examples for simplicity, there may be multiple layers in most use cases. In hexagonal architecture, outer layers (outer hexagons) depend on inner layers (inner hexagons), which makes it easier to implement the application core first and then implement outer layers to depend on them. For example, the web or CLI layer depends on the application layer, as shown in figure 4.1. Since all the operations are performed on the Order domain object in the application layer, let’s create the necessary Go file, add structs inside it, and add domain methods, once needed:

touch internal/application/domain/order.go

Domain objects in Go are primarily specified by structs that contain field type, field name, and serialization config via tags (https://pkg.go.dev/encoding/json#Marshal). For example, to specify a CustomerID field with an int64 type of order and specify a field name as customer_id after JSON serialization (e.g., to save it in MongoDB), you can use the following code:

CustomerID int64       `json:"customer_id"`

order.go contains the following content:

package domain
 
import (
    "time"
)
 
type OrderItem struct {
    ProductCode string  `json:"product_code"`                    
    UnitPrice   float32 `json:"unit_price"`                      
    Quantity    int32   `json:"quantity"`                        
}
 
type Order struct {
    ID         int64       `json:"id"`                           
    CustomerID int64       `json:"customer_id"`                  
    Status     string      `json:"status"`                       
    OrderItems []OrderItem `json:"order_items"`                  
    CreatedAt  int64       `json:"created_at"`                   
}
 
func NewOrder(customerId int64, orderItems []OrderItem) Order {  
    return Order{
        CreatedAt:  time.Now().Unix(),
        Status:     "Pending",
        CustomerID: customerId,
        OrderItems: orderItems,
    }
}

Unique code of the product

Price of a single product

Count of the product

Unique identifier of the order

Owner of the order

Status of the order

List of items purchased in an order

Order creation time

Function to create default order

In this example, we simply implemented the Order data structure with an OrderItem relation and introduced a method called NewOrder to create an order. There is another package under the application, api, which contains another Go file to control the state of a specific order. During application initialization, we expect to see a dependency injection mechanism to inject a DB adapter (concrete implementation for a specific DB technology) into the application so that the API can store the state of a particular order in the database without needing to know the adaptor’s internals. This is why the application depends on the interface ports.DBPort instead of the DB adapter’s concrete implementation. The API interface uses this port to access the real DB adapter to save order information in the database with the PlaceOrder method:

touch internal/application/api/api.go

The content of api.go is as follows:

package api
 
import (
    "github.com/huseyinbabal/microservices/order/internal/application/core/
      domain"                                                    
    "github.com/huseyinbabal/microservices/order/internal/ports"   
)
 
type Application struct {
    db ports.DBPort                                                
}
 
func NewApplication(db ports.DBPort) *Application {
    return &Application{
        db: db,                                                    
    }
}
 
func (a Application) PlaceOrder(order domain.Order) (domain.Order, error) {
    err := a.db.Save(&order)                                       
    if err != nil {
        return domain.Order{}, err
    }
    return order, nil
}

Package for the order domain object

Ports for the DB adapter

The API depends on DBPort.

DBPort is passed during the app’s initialization.

Order is saved through the DB port.

The Application struct has a dependency in the DB layer, and you can see the NewApplication method that helps create an instance of the Application. Once you create an instance of the Application, you will see that each instance has a PlaceOrder method that uses DB dependency to save the order in the database. The original packages are used in the import statements, but you can use your package URL instead of github.com/huseyinbabal/microservices/order.... Now that we understand how the application core is structured, let’s look at how to implement ports so that they can be used in the application and the adapters.

4.2.4 Implementing ports

Ports are just interfaces that contain general methods in each hexagonal layer. For example, we implemented the PlaceOrder method in the previous section, and if you look at it carefully, you can see that it implements the PlaceOrder method of the APIPort interface. The PlaceOrder method simply accepts and saves a domain object in the database. With this information, we can assume that whenever we want to create an application, we need to pass the DB adapter to it. The application saves an order in the database using the reference db via the receiver function.

Let’s start implementing the API port with touch internal/ports/api.go, which contains an interface with just one method, PlaceOrder, as follows:

package ports
 
import "github.com/huseyinbabal/microservices/order/internal/application/core/domain"
 
type APIPort interface {
    PlaceOrder(order domain.Order) (domain.Order, error)
}

While APIPort is used for core application functionalities, DBPort helps the application fulfill its functionalities. Let’s create a DB port using the touch internal/ports/db.go file. DBPort is a very simple interface that contains the Get and Save methods, and, of course, it depends on the application domain model, which is Order:

package ports
 
import "github.com/huseyinbabal/microservices/order/internal/application/
 core/domain"
 
type DBPort interface {
    Get(id string) (domain.Order, error)   
    Save(*domain.Order) error              
}

Gets Order by its unique ID

Saves the Order domain into the database

The application depends on DBPort via the interface, but we need to pass a concrete implementation during initialization, so let’s look at what concrete implementations of ports look like.

4.2.5 Implementing adapters

We must implement Save and Get methods to allow the application to save Order in the database. The ORM (Object Relational Mapping) library would be suitable for database-related operations in an effort to eliminate extra effort while constructing SQL queries. GORM is a very popular ORM library in the Go world, and we will use it for our project. Let’s get GORM and MySQL driver dependency with the following command after you go to the root directory of order project:

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

Now we are ready to create our DB file and add dependencies to your go.mod file. As you can see, GORM has an abstraction over DB drivers that you can easily use: touch internal/adapters/db/db.go. This file contains a struct for database models and related functions to manage their state. For the Order service, we have two simple models, Order and OrderItem, in which the Order model has a one-to-many relationship with OrderItem, as you can see in figure 4.3.

Figure 4.3 One-to-many relationship between orders and order_items table

One of the best things about an ORM library is the ability to define these relationships with simple conventions, such as referencing a field in one model and applying it to another. A typical example of the Order model is that its field OrderItem refers to another struct. To set up a proper relation, there should be a reference ID on the second struct, OrderId, in our case. Finally, you can embed GORM to mark a struct as a domain entity, .Model, into the struct. This augments your domain model with built-in fields, such as ID, CreatedAt, UpdatedAt, and DeletedAt. GORM detects this relationship, creates tables in the proper order, and connects them. As you can see, the field names in figure 4.3 are in snake case. This serialization strategy is applied to the table structure before it is applied to the database, with the help of struct tags. Then we are free to add necessary packages and structs to the db.go file:

package db
 
import (
    "fmt"
    "github.com/huseyinbabal/microservices/order/internal/application/core/
      domain"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)
 
type Order struct {
    gorm.Model               
    CustomerID int64
    Status     string
    OrderItems []OrderItem   
}
 
type OrderItem struct {
    gorm.Model
    ProductCode string
    UnitPrice   float32
    Quantity    int32
    OrderID     uint         
}

Adds entity metadata such as ID to struct

Reference to OrderItem

Back reference to Order model

Having struct definitions is not enough to persist data into the database. We need to add gorm.DB as a dependency to our adapter:

type Adapter struct {
    db *gorm.DB
}

Let’s assume we are implementing a DB adapter. To create the adapter, you need to pass a gorm.DB reference to it. Now that we understand how to add a DB reference, let’s see how adapter functions use this DB reference to manage the state of order models.

The data source URL is a common parameter used to create a reference for database connections. We can create a connection to a provided data source URL by using a DB driver, which, in our case, is a MySQL driver, aliased by gorm/mysql. Error handling is also important here so that we understand whether the connection is successful and so that we can decide whether we should continue the initialization of the application. The following function is a good candidate for opening a connection to the database:

func NewAdapter(dataSourceUrl string) (*Adapter, error) {
    db, openErr := gorm.Open(mysql.Open(dataSourceUrl), &gorm.Config{})
    if openErr != nil {
        return nil, fmt.Errorf("db connection error: %v", openErr)
    }
 
    err := db.AutoMigrate(&Order{}, OrderItem{})     
    if err != nil {
        return nil, fmt.Errorf("db migration error: %v", err)
    }
    return &Adapter{db: db}, nil
}

Be sure the tables are created correctly.

The NewAdapter function creates an Adapter instance that we can use internally. It is important to understand how the Adapter instance is delegated to functions via receiver functions. For example, to get Order information, we can query the database using the Adapter instance and return it after converting it to a domain model the application can understand. Using the same notation, we can accept the Order domain model as a parameter, transform it to a DB entity, and save the order information in the database. You can see the Get and Save methods in the following two code snippets:

func (a Adapter) Get(id string) (domain.Order, error) {      
    var orderEntity Order
    res := a.db.First(&orderEntity, id)                      
    var orderItems []domain.OrderItem
    for _, orderItem := range orderEntity.OrderItems {       
        orderItems = append(orderItems, domain.OrderItem{
            ProductCode: orderItem.ProductCode,
            UnitPrice:   orderItem.UnitPrice,
            Quantity:    orderItem.Quantity,
        })
    }
    order := domain.Order{                                   
        ID:         int64(orderEntity.ID),
        CustomerID: orderEntity.CustomerID,
        Status:     orderEntity.Status,
        OrderItems: orderItems,
        CreatedAt:  orderEntity.CreatedAt.UnixNano(),
    }
    return order, res.Error
}
func (a Adapter) Save(order *domain.Order) error {           
    var orderItems []OrderItem
    for _, orderItem := range order.OrderItems {             
        orderItems = append(orderItems, OrderItem{
            ProductCode: orderItem.ProductCode,
            UnitPrice:   orderItem.UnitPrice,
            Quantity:    orderItem.Quantity,
        })
    }
    orderModel := Order{                                     
        CustomerID: order.CustomerID,
        Status:     order.Status,
        OrderItems: orderItems,
    }
    res := a.db.Create(&orderModel)                          
    if res.Error == nil {
        order.ID = int64(orderModel.ID)
    }
    return res.Error
} 

The Get method returns the domain.Order core model.

Finds by ID and puts it into orderEntity

Converts Order Items

Converts Order

Accepts the domain.Order core model

Converts Order Items

Converts Order

Saves data into the database

Get and Save methods reference Adapter via receiver functions, and those methods can access DB dependency to write/read order information. Now that we understand how a DB adapter is implemented to save and get order information in this application, let’s look at how to introduce gRPC as another adapter.

4.2.6 Implementing a gRPC adapter

The primary motivation for implementing a gRPC adapter is to provide an interface for the end user to use order functionalities. This interface contains request and response objects that are used during data exchange. The protocol buffer compiler generates request objects, response objects, and service communication layer implementations. The order module for Golang is in github.com/huseyinbabal/microservices-proto/ golang/order. In previous chapters, we mentioned maintaining .proto files and their generations in a separate repository, and now we will depend on that repository to fulfill the gRPC server. Be careful about using the GitHub username; you need to replace it with yours if you are maintaining .proto files on your own.

The gRPC server adapter depends on APIPort, which contains the core functionalities’ application module contract. It also depends on gRPC internals generated within the github.com/huseyinbabal/microservices-proto/golang/order repository, which mostly provides forward compatibility. The following code demonstrates this:

type Adapter struct {
    api  ports.APIPort              
    port int                        
    order.UnimplementedOrderServer  
}

Core application dependency

Port to serve gRPC on

Forward compatibility support

We can use the same notation to create an adapter for the gRPC server:

func NewAdapter(api ports.APIPort, port int) *Adapter {
    return &Adapter{api: api, port: port}
}

Now that we’ve defined the gRPC server adapter and created an instance, let’s see how to run this server. The gRPC server requires a socket to listen for requests from clients. To test the gRPC interface, we will perform requests via grpcurl (https://github.com/fullstorydev/grpcurl), a command-line application that helps you easily send gRPC requests by testing gRPC endpoints, something we do mostly with cURL and http endpoints. This is possible if you enable reflection on the server side or if you need to tell grpcurl about the location of the .proto files you use in your request. Here’s the running logic for a simple gRPC server:

func (a Adapter) Run() {
    var err error
 
    listen, err := net.Listen("tcp", fmt.Sprintf(":%d", a.port))
    if err != nil {
        log.Fatalf("failed to listen on port %d, error: %v", a.port, err)
    }
 
    grpcServer := grpc.NewServer()
    order.RegisterOrderServer(grpcServer, a)   
    if config.GetEnv() == "development" {      
        reflection.Register(grpcServer)
    }
 
    if err := grpcServer.Serve(listen); err != nil {
        log.Fatalf("failed to serve grpc on port ")
    }
 
}

This method is autogenerated by protoc.

Enables reflection to make grpcurl

This example simply creates a gRPC server that you can make gRPC calls against to handle order-related operations.

Thus far, we’ve managed to run the gRPC server, but no endpoint has been enabled. To introduce the Create endpoint support, we accept the CreateOrder request from the end user and process it. This creates a new order out of the gRPC request and uses the PlaceOrder function from APIPort:

func (a Adapter) Create(ctx context.Context, request *order.CreateOrderRequest) (*order.CreateOrderResponse, error) {  
    var orderItems []domain.OrderItem
    for _, orderItem := range request.OrderItems {                     
        orderItems = append(orderItems, domain.OrderItem{
            ProductCode: orderItem.ProductCode,
            UnitPrice:   orderItem.UnitPrice,
            Quantity:    orderItem.Quantity,
        })
    }
    newOrder := domain.NewOrder(request.UserId, orderItems)            
    result, err := a.api.PlaceOrder(newOrder)                          
    if err != nil {
        return nil, err
    }
    return &order.CreateOrderResponse{OrderId: result.ID}, nil 
}

CreateOrderRequest is generated from proto.

Converts OrderItems to create a new order

Creates a new order domain object

Places order via APIPort

To apply gRPC support, you can start creating the necessary files: touch internal/ adapters/grpc/grpc.go and touch internal/adapters/grpc/server.go.

grpc.go is for defining the handlers, and server.go mostly runs the server and register endpoints inside the grpc.go file. To be able to use the request-response object for the gRPC application, we need to download and add a dependency to the order application by running it in the order service root folder:

go get github.com/huseyinbabal/microservices-proto/golang/order

Now we are ready to use gRPC models for the Create endpoint by adding the following code to the grpc.go file:

package grpc
 
import (
    "context"
    "github.com/huseyinbabal/microservices-proto/golang/order"
    "github.com/huseyinbabal/microservices/order/internal/application/core/
      domain"
)
 
func (a Adapter) Create(ctx context.Context, request 
 *order.CreateOrderRequest) (*order.CreateOrderResponse, error) {
    var orderItems []domain.OrderItem
    for _, orderItem := range request.OrderItems {
        orderItems = append(orderItems, domain.OrderItem{
            ProductCode: orderItem.ProductCode,
            UnitPrice:   orderItem.UnitPrice,
            Quantity:    orderItem.Quantity,
        })
    }
    newOrder := domain.NewOrder(request.UserId, orderItems)
    result, err := a.api.PlaceOrder(newOrder)
    if err != nil {
        return nil, err
    }
    return &order.CreateOrderResponse{OrderId: result.ID}, nil
}

The Create method accepts an order creation request from the client, converts it to the Order domain model, and calls PlaceOrder via api dependency. As a final step, the following code creates a listener socket and runs the gRPC server. This will also let the consumer call the Create endpoint for order creation. You can simply add it to server.go:

package grpc
 
import (
    "fmt"
    "github.com/huseyinbabal/microservices-proto/golang/order"     
    "github.com/huseyinbabal/microservices/order/config"           
    "github.com/huseyinbabal/microservices/order/internal/ports"
    "google.golang.org/grpc/reflection"                            
    "log"
    "net"
 
    "google.golang.org/grpc"
)
 
type Adapter struct {
    api  ports.APIPort
    port int
    order.UnimplementedOrderServer
}
 
func NewAdapter(api ports.APIPort, port int) *Adapter {
    return &Adapter{api: api, port: port}
}
 
func (a Adapter) Run() {
    var err error
 
    listen, err := net.Listen("tcp", fmt.Sprintf(":%d", a.port))
    if err != nil {
        log.Fatalf("failed to listen on port %d, error: %v", a.port, err)
    }
 
    grpcServer := grpc.NewServer()
    order.RegisterOrderServer(grpcServer, a)
    if config.GetEnv() == "development" {
        reflection.Register(grpcServer)
    }
 
    if err := grpcServer.Serve(listen); err != nil {
        log.Fatalf("failed to serve grpc on port ")
    }
 
}

Order service golang objects

Another package that contains app configs

Reflection to perform grpcurl

Now that we understand ports and adapters in hexagonal architecture, let’s look at how to combine them all via dependency injection and then run the application.

4.2.7 Dependency injection and running the application

The 12-factor app is a methodology for building applications that encourages you to

Automation will be addressed in upcoming chapters; the contract between underlying operating systems is the key topic for our current use case. To understand what configuration parameters are available, let’s create a config package and implement configuration logic. This is typical configuration management for applications, and in this example, we will do it through environment variables, as suggested in the 12-factor app (https://12factor.net/config).

In the touch config/config.go file, the main logic in the config.go file will expose environment variable values to developers via functions. The order application needs the following environment variables to function properly:

The application will fail to start if there is a missing environment variable. Making the application fail fast due to a missing configuration is better than silently allowing it to start, which might cause major inconsistencies due to empty environment variable values (e.g., empty API_URL). We can use the following implementation to properly address these concerns and read environment variable values:

package config
 
import (
    "log"
    "os"
    "strconv"
)
 
func GetEnv() string {
    return getEnvironmentValue("ENV")                    
}
 
func GetDataSourceURL() string {
    return getEnvironmentValue("DATA_SOURCE_URL")        
}
 
func GetApplicationPort() int {
    portStr := getEnvironmentValue("APPLICATION_PORT")   
    port, err := strconv.Atoi(portStr)
 
    if err != nil {
        log.Fatalf("port: %s is invalid", portStr)
    }
 
    return port
}
func getEnvironmentValue(key string) string {
    if os.Getenv(key) == "" {                            
        log.Fatalf("%s environment variable is missing.", key)
    }
 
    return os.Getenv(key)
}

Possible values for development/production

Database connection URL

Order service port

GetEnv returns the string.

This is an example of reading an environment variable from which you can get a value or error. Now we are ready to get the configuration through environment variables; that means we can prepare adapters and plug them into application ports. Let’s create our application endpoint with the following command:

touch cmd/main.go

The DB adapter needs a data source URL to connect and return an instance for a DB reference. The core application needs this DB adaptor to modify order objects in the database. Finally, the gRPC adapter needs a core application and a specific port to get the gRPC up and running via the Run method:

package main
 
import (
    "github.com/huseyinbabal/microservices/order/config"
    "github.com/huseyinbabal/microservices/order/internal/adapters/db"
    "github.com/huseyinbabal/microservices/order/internal/adapters/grpc"
    "github.com/huseyinbabal/microservices/order/internal/application/core/
      api"
    "log"
)
 
func main() {
    dbAdapter, err := db.NewAdapter(config.GetDataSourceURL())
    if err != nil {
        log.Fatalf("Failed to connect to database. Error: %v", err)
    }
 
    application := api.NewApplication(dbAdapter)
    grpcAdapter := grpc.NewAdapter(application, config.GetApplicationPort())
    grpcAdapter.Run()
}

As you can see, the gRPC needs a database, which is MySQL. Docker (https://www.docker.com/), an OS-level virtualization to deliver software in containers, can help us quickly run a MySQL database with a predefined database and user:

docker run -p 3306:3306 \
    -e MYSQL_ROOT_PASSWORD=verysecretpass \
    -e MYSQL_DATABASE=order mysql

In this case, our data source URL is

root:verysecretpass@tcp(127.0.0.1:3306)/order

To run the Order service application, you can use the following:

DATA_SOURCE_URL=root:verysecretpass@tcp(127.0.0.1:3306)/order \
APPLICATION_PORT=3000 \
ENV=development \
go run cmd/main.go

If you get dependency errors, you can execute go mod tidy to reorganize dependencies and rerun the application. Now that we understand how to run an application, let’s look at a running gRPC service.

4.2.8 Calling a gRPC endpoint

The Order application has the Order service and Create rpc inside it. To send a CreateOrder request, you can pass CreateOrderRequest as a JSON to grpcurl:

grpcurl -d '{"user_id": 123, "order_items": [{"product_code": "prod", 
 "quantity": 4, "unit_price": 12}]}' -plaintext localhost:3000 
 Order/Create 0

This is similar to cURL, which accepts a request payload with a -d parameter. The -plaintext parameter is used to disable TLS during gRPC communication. The Order service will return a response after a successful request:

{
  "orderId": "1"
}

This is a simple response, but we will see advanced scenarios and proper exception-handling mechanisms in upcoming chapters.

Summary