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.
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.
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).
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.
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.
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.
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.
While there are no written rules for a hexagonal architecture folder, the following folders are common in typical Go projects:
Application folder—This folder contains microservice business logic, which is a combination of the domain model that refers to a business entity and an API that exposes core functionalities to other modules.
Port folder—This folder contains contract information for integration between the core application and third parties. This can be a contract about accessing core application features or about specifying available features for a database system, if one is used for the persistence layer.
Adapter folder—This folder contains concrete implementation for using contracts that are defined in ports. For example, gRPC can be an adapter with a concrete implementation that handles requests and uses an API port to access core functionalities, such as if you have an application with some functionalities and will expose it to customers. The functionalities can be CreateProduct
, GetProduct
, and so on, and you can expose them to the customer via REST, gRPC, and other adaptors, which will use the contracts of those functionalities, as defined in the port layer. We will revisit this topic and look at more advanced examples in later sections of this chapter.
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.
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 ❷
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
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 identifier of the order
❼ List of items purchased in an order
❾ 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
❹ 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.
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 ❷ }
❷ 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.
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.
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
❸ 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
❺ Accepts the domain.Order core model
❽ 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.
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 ❸ }
❸ 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
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.
The 12-factor app is a methodology for building applications that encourages you to
Use a declarative setup for infrastructure and for application environment automation to quickly deploy to any environment, such as dev
, staging
, or prod
Have a clean contract between underlying operating systems so that the same application can be executed on any environment with different parameters
Use continuous deployment to minimize divergence between environments
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:
ENV
—This is for separating the prod and non-prod environments. For example, you can enable a debug-level log for non-prod environments and have an info level on the prod environment.
APPLICATION_PORT
—This is the port on which the Order service will be served.
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
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.
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.
Hexagonal architecture helps you isolate layers in your microservice to implement testable and clean applications.
Ports are used for defining contracts between layers, while adapters are concrete implementations that can be plugged into ports to make the core application available to the end user.
Implementing the application core first and then continuing with the outer layers is helpful.
GORM is an ORM library for Go with good abstraction for database-related operations.
Twelve-factor applications have good use cases for microservices in that application configurations can be passed through environment variables and that you can configure them based on the environment.
Combining layers in hexagonal architecture is done with dependency injection.
grpcurl
provides a cURL-like behavior to handle order data by calling gRPC endpoints.