Serverless Lock-in doesn't exist (if your Team knows Hexagoxal Architecture): an example in Go
Published: 2021-04-26 16:00:17

### Scope

Serverless architecture built on top of the *PaaS* solutions offered from the different Cloud provider are fantastic.

Nonetheless many Companies remain prudent about it because of their **fears about Lock-in**.

How to avoid Cloud Lock-In when to decide to develop and deploy your application using one or more Serverless components in AWS, Google Cloud, Azure or another provider?

What is the Hexagonal Architecture and how this architectural style can help you to avoid lock-in when using serverless services on the Cloud?

In this post I'll try to answer these questions implementing a simple Hexagonal web application in GO, and demonstrating how apply this pattern can be a good strategy to avoid or mitigate vendor Lock-In.


### What is "Hexagonal Architecture"

There are tons of articles, posts and materials about this argument, starting from the original definition of *[Ports and Adapters](https://alistair.cockburn.us/hexagonal-architecture/)* provided by Alistair Cockburn.

I will try to provide a very simple explanation.

According to this architectural style, an application should be divided in different components which are build around a Core component.

The **Core** is the hearth of the application: it contains the **Domain Entities** and the business logic of the provided application functionalities, called **Use Cases**.


![Hexagonal Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/alikccw5y6zz9jbk8jvk.png)

The Core component is completed **isolated** from the external system/components and its implementation must be independent from the input sources and output destinations.

This means the Core component must provide an ingress to permits the different external *input components* to access the core functionalities of the application.

The ingresses to the Core component are called **Input Ports** and usually they are implemented as abstract components (usually *Interface*) which defines how the external components access the internal functionalities.

In a similar way, when the Core component needs to communicate with the external world, it defines specific **Output Ports**, which defines the way it will communicate with the external components.

The different input sources and output destinations are called **Adapter**. The Input Adapter are often called **Driver Adapter**, the Output Adapter are called **Driven Adapter**)

Basically in our application we have different Input/Output Adapters which communicate with the Core components using Input and Output Ports.

Simple, right?

### Software Architecture

But how to map it in a software architecture?

Let's consider a simple web application which receive http requests from a browser, apply some business logic on the received data and then save the data into one or more repositories.

![Hexagonal SW Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rxj7xocfq6ee0qqya0wd.png)

In this application:
- *HttpController* is an input Adapter 
- *ServicePort* is an interface which expose the Core functionalities of the application
- *Service* is an implementation of the ServicePort interface, and contains the business logic of the application
- *RepositoryPort* is an interface which defines how my Service component will communicate with a repository
- *SQLRepository* and *MemoryRepository* are implementations of the RepositoryPort interface, providing the repository integration with the different repository infrastructure/technology

But wait, why do I need to design my application in this way?

#### Benefits

Using the Hexagonal Architecture style provided some well known benefits:

1. **Easier to test in isolation**: each layer of the application is easier to test because you can simply mock any dependencies
2. **Independent from external services**: you can develop the core functionalities without take care of the external system/technologies used to ingest or store the data
3. **Agnostic to the outside world**: the application can interacts with different Drivers without to change its business logic 
4. **The Ports and Adapters** are replaceable: you can add/remove any ports/adapter without change the internal business logic
5. **Separation of the different rates of change**: Adapters and Core components have different evolution
5. **High Maintainability**: changes in one area of the application doesn’t really affect others, make it easily to make fast changes of the application

### Moving to the Cloud

Now let's image in the future we change strategy and decide to:

- migrate our application into the **AWS Cloud**
- deploy the application in the AWS Lambda **FaaS** service
- to use AWS DynamoDB as Serverless NoSql repository

How complex will be to adapt our Application to this new strategy?

The answer is simple: if we used Hexagonal Architecture during the design of our application, changing the entry point and repository of our application is simple as to write 2 new Adapters, without to modify our Core components!

![AWS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v1ql1xvrmunysok6dg7z.png)

What if in the future we decides to move our application in 
Google Cloud using Functions and Firestore Services?

Well, it's just a matter of write 2 new Adapters:

![GCP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4uu5amvmgfj1tqrhwhj.png)

### Talk is Cheap, show me the code!

It's time to get our hands dirty, so let's consider a simple mobile App which permits to create sport Team and to invite Players (team-app).
The mobile App interacts with our Back-End Application which expose come simple APIs:

- to create a New Team
- to retrieve the List of the Teams
- to invite a Player to connect to the Team
- to retrive the List of the Players of a specific Team

How we could design the back-end application using an Hexagonal Architecture?

First of all we need to define the entry point for our application, which will receive the requests from the mobile App. We will call this component *HttpHandler*.

![Mobile-App](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qqjl201i0mcsor5l5pq7.png)

The HttpHandler will expose 4 different APIs:

- */teams* (POST with the json body of a Team) --> return the new Team
- */teams* (GET)  --> return an array of Team
- */teams/:team_id/invite-player* (POST with the json body of a Player) --> return the new Player
- */teams/:team_id/players* (GET) --> return the List of the Players of a Team

It will communicate with the Core components using the interface *TeamServicePort* which expose the methods:

- *createNewTeam(Team team)* --> return the new *Team*
- *listTeams()* --> return *Team[]*
- *invitePlayer(Player player)* --> return the new *Player*
- *listTeamPlayers(int team_id)* --> return *Player[]*

The Core of our Back-end application will contain the implementation of TeamServicePort interface (*TeamService*), which contains the business logic of the methods exposed by the interface.

In particular the TeamService will apply the following business logic:

- *createNewTeam*: store the new Team in the repository and return the saved data
- *listTeams*: query the repository in order to retrieve all the stored Teams
- *invitePlayer*: save the player information into the repository
- *listTeamPlayers*: query the repository in order to retrieve all the stored Players for a given team_id

Let's implement our Back-End application in GO.

#### Go Application

Our Go application is organized in different packages:

* ports
 * team_service_port.go
 * repository_port.go
* core
  * domain
        * team.go
        * player.go
  * services
        * team_service.go
* adapters
  * http_handler.go
  * memory_repository.go
* main.go

#####Domain Entities

*team.go*

```go
type Team struct {
	ID    int64  `json:"id"`
	Name  string `json:"name"`
	Sport string `json:"sport"`
}
```

*player.go*

```go
type Player struct {
	ID        int64  `json:"id"`
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
	Email     string `json:"email" binding:"required"`
	TeamID    int64  `json:"team_id"`
}
```

#####Ports

This interface defines the functionalities of our applications (use cases). 
The interface is used from the input adapter to communicate with the core component:

*team_service_port.go*

```go
type TeamServicePort interface {
	CreateNewTeam(team domain.Team) (domain.Team, error)
	ListTeams() ([]domain.Team, error)
	InvitePlayer(player domain.Player) (domain.Player, error)
	ListTeamPlayers(team_id int64) ([]domain.Player, error)
}
```


*repository_port.go*

This interface defines the functionalities that a repository needs to implement in order to permits our application to store the domain entities.
The interface is used from the Core service in order to persist our Entities:

```go
type RepositoryPort interface {
	GetTeams() ([]domain.Team, error)
	SaveTeam(team domain.Team) (domain.Team, error)
	SavePlayer(member domain.Player) (domain.Player, error)
	GetPlayersByTeam(team_id int64) ([]domain.Player, error)
}
```


##### Core Service

This Adapter is the entry point for our http request; it manages the http requests using the interface TeamServicePort as port to communicate with the Core component:

TeamService is the heart of our application; it's an implementation of the TeamServicePort and contains the business logic of our Back-End application.
The Service uses the repository interface in order to persists the domain entities:

*team_service.go*
```go
type teamService struct {
	repository ports.RepositoryPort
}

func New(repository ports.RepositoryPort) *teamService {
	return &teamService{
		repository: repository,
	}
}

func (srv *teamService) CreateNewTeam(team domain.Team) (domain.Team, error) {
	team, err := srv.repository.SaveTeam(team)
	if err != nil {
		return domain.Team{}, err
	}
	return team, nil
}

func (srv *teamService) ListTeams() ([]domain.Team, error) {
	teams, err := srv.repository.GetTeams()
	if err != nil {
		return nil, err
	}
	return teams, nil
}

func (srv *teamService) InvitePlayer(player domain.Player) (domain.Player, error) {
	player, err := srv.repository.SavePlayer(player)
	if err != nil {
		return domain.Player{}, err
	}
	return player, nil
}

func (srv *teamService) ListTeamPlayers(team_id int64) ([]domain.Player, error) {
	players, err := srv.repository.GetPlayersByTeam(team_id)
	if err != nil {
		return nil, err
	}
	return players, nil
}
```


#####Adapters
*http_handler.go*

```go
type httpHandler struct {
	service ports.TeamServicePort
}

func NewHttpHandler(service ports.TeamServicePort) *httpHandler {
	return &httpHandler{
		service: service,
	}
}

func (hdl *httpHandler) RetrieveTeams(c *gin.Context) {
	teams, err := hdl.service.ListTeams()
	if err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": err.Error()})
		return
	}
	c.JSON(http.StatusOK, teams)
}

func (hdl *httpHandler) CreateNewTeam(c *gin.Context) {
	var input domain.Team
	if err := c.ShouldBindJSON(&input); err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": err.Error()})
		return
	}
	team, err := hdl.service.CreateNewTeam(input)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": err.Error()})
		return
	}
	c.JSON(http.StatusOK, team)
}

func (hdl *httpHandler) ListTeamPlayers(c *gin.Context) {
	team_id, err := strconv.ParseInt(c.Param("team_id"), 10, 64)
	players, err := hdl.service.ListTeamPlayers(team_id)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": err.Error()})
		return
	}
	c.JSON(http.StatusOK, players)
}

func (hdl *httpHandler) InvitePlayer(c *gin.Context) {
	var input domain.Player
	if err := c.ShouldBindJSON(&input); err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": err.Error()})
		return
	}
	player, err := hdl.service.InvitePlayer(input)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": err.Error()})
		return
	}
	c.JSON(http.StatusOK, player)
}

```


*memory_repository.go*

It's a simple implementation of a memory repository; it's used from our core service:

```go
var (
	team_counter   int64 = 1
	player_counter int64 = 1
)

type memoryRepository struct {
	inMemoryTeams   map[int64]domain.Team
	inMemoryPlayers map[int64][]domain.Player
}

func NewMemoryRepository() *memoryRepository {
	inMemoryTeams := map[int64]domain.Team{}
	inMemoryPlayers := map[int64][]domain.Player{}
	return &memoryRepository{inMemoryTeams: inMemoryTeams, inMemoryPlayers: inMemoryPlayers}
}

func (repo *memoryRepository) SaveTeam(team domain.Team) (domain.Team, error) {
	if team.ID == 0 {
		team.ID = team_counter
		team_counter++
	}
	repo.inMemoryTeams[team.ID] = team
	fmt.Println(repo.inMemoryTeams)
	return team, nil
}

func (repo *memoryRepository) GetTeams() ([]domain.Team, error) {
	var teams []domain.Team = make([]domain.Team, 0)
	for _, team := range repo.inMemoryTeams {
		teams = append(teams, team)
	}
	return teams, nil
}

func (repo *memoryRepository) SavePlayer(player domain.Player) (domain.Player, error) {
	if player.ID == 0 {
		player.ID = player_counter
		player_counter++
	}

	teamPlayers := repo.inMemoryPlayers[player.TeamID]
	fmt.Println("teamPlayers", teamPlayers)
	teamPlayers = append(teamPlayers, player)
	repo.inMemoryPlayers[player.TeamID] = teamPlayers
	fmt.Println(repo.inMemoryPlayers)
	return player, nil
}

func (repo *memoryRepository) GetPlayersByTeam(team_id int64) ([]domain.Player, error) {
	players := repo.inMemoryPlayers[team_id]
	if players == nil {
		players = []domain.Player{}
	}
	return players, nil
}

```

#####Application Main

In order to start our application we need to create an instance of the MemoryRepository:
```go
memoryRepository := adapters.NewMemoryRepository()
```
Then we need to create an instance of the Core Service (injecting our MemoryRepository)...
```go
teamService := services.New(memoryRepository)
```
...and an instance of the HttpHandler (injecting our teamService implementation):
```go
httpHandler := adapters.NewHttpHandler(teamService)
```

Finally we can configure our web server routes (using the Gin framework): 

```go
	r := gin.Default()

	v1 := r.Group("/api/v1")
	{
		teams := v1.Group("/teams")
		{
			teams.GET("", httpHandler.RetrieveTeams)
			teams.POST("", httpHandler.CreateNewTeam)
			teams.POST(":team_id/invite-player", httpHandler.InvitePlayer)
			teams.GET(":team_id/players", httpHandler.ListTeamPlayers)
		}
	}
	r.Run()
```
This is the entire main file:

*main.go*
```go
func main() {
	memoryRepository := adapters.NewMemoryRepository()
	teamService := services.New(memoryRepository)
	httpHandler := adapters.NewHttpHandler(teamService)

	r := gin.Default()

	v1 := r.Group("/api/v1")
	{
		teams := v1.Group("/teams")
		{
			teams.GET("", httpHandler.RetrieveTeams)
			teams.POST("", httpHandler.CreateNewTeam)
			teams.POST(":team_id/invite-player", httpHandler.InvitePlayer)
			teams.GET(":team_id/players", httpHandler.ListTeamPlayers)
		}
	}
	r.Run()
}
```

Our Hexagonal is now ready, and if we launch it (with *go build main.go*) it will expose the following rest endpoint:

GET - http://localhost:8080/api/v1/teams
POST - http://localhost:8080/api/v1/teams
GET - http://localhost:8080/api/v1/teams/:team_id/players
POST - http://localhost:8080/api/v1/teams/:team_id/invite-player

#### Moving our App to the Cloud

As we discussed before, move our GO Application to the Cloud is as simple as to write one or more new Adapters.

Let's image we want to deploy our Backend App as [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html); we need just to replace our HttpHandler with one more Lambda Functions, for example one function to manage the Team Entity and another to manage the Player Entity.

That's all!

For our example which use the Go Gin framework, to minimize the changes we could also use the great [AWS Lambda Go API Proxy](https://github.com/awslabs/aws-lambda-go-api-proxy). You can find an example of lambda function in the article [repository](https://github.com/thecillu/hexagonal-architecture/blob/main/aws_lambda_team.go)

What about the repository?

Well, if you need to replace the MemoryRepository with another Serverless components, you need just to write the new adapter implementing the RepositoryPort interface.

What about to change Cloud provider?

Again, you need to write only the new adapters to use the new Provider Serverless services.

### Conclusion

Serverless Architecture are really powerful, but if you don't design your application in the right way, the cost to migrate to another cloud provider in the future could high.

Hexagonal Architecture is a really simple and clean pattern *you should use for your Cloud Native application*, it's not important if it runs on K8S, on a FaaS Service, on VM, or in other containerized platforms.

Design your applications following this architectural style will help you to avoid or mitigate the Cloud Lock-In Phobia.


### Suorce code
You can find the source code here:

{% github thecillu/hexagonal-architecture %}