logo

Lucas Katayama/Hexagonal Architecture

Created Wed, 14 Feb 2024 11:54:58 -0300 Modified Wed, 14 Feb 2024 11:54:58 -0300

Hexagonal Architecture

Using the Golang interface concept we can build a simple code architecture agnostict to the technology.

Port and Adapter

Think about an USB port.

A computer can communicate with a keyboard or a monitor through the USB port. It can send comands to monitor to show something on the screen or it can receive key press signal from keyboard.

So a USB is a Port which can be implemented and used by periferals though an Adapter.

flowchart LR
    subgraph Input
        direction LR;
        Keyboard --> KeyboardAdapter;
    end
    subgraph Core
        KeyboardAdapter -.-> USB1
        USB1 --> Controller
        Controller --> USB2
    end
    subgraph Output
        USB2 -.-> MonitorAdapter
        MonitorAdapter --> Monitor
    end

Main function

Let’s code that logic

The controller is the core which holds the business logic of your program. For that example, it receives data from the keyboard and do something and sends data to the screen.

Your main program will look like this:

package main


func main() {
    // keyboard send keypress data

    // send to usb1

    // receives data from usb1

    // send data to usb2

    // monitor shows on the screen
}

USB Port

The USB can receive and send data, so will look like this:

type USBPortReceiver interface {
	Receive(msg string) error
}

type USBPortSender interface {
	Send(msg string) error
}

Monitor

The monitor receives data from the port, so it is controlled by it. The implementation Adapter goes under the Keyboard domain and it show what is receives into the screen:

type Monitor struct {
}

func (m Monitor) Receive(msg string) error {
	log.Println("monitor received:", msg)
	fmt.Println(msg)
	return nil
}

Keyboard

The keyboard sends data to the port. So it uses it. The implementation Adapter goes under the Computer domain. Also, as the keyboard sends data to the computer, this sends to the screen:

type Computer struct {
	usb2Port USBPortReceiver
}

func (c Computer) Send(msg string) error {
	log.Println("computer received data:", msg)
	return c.usb2Port.Receive(msg)
}

This way the keyboard can use the USB1 port

type Keyboard struct {
	usb USBPortSender
}

func (k Keyboard) Type() error {
	a := "Hello John Doe"
	log.Println("typed:", a)
	return k.usb.Send(a)
}

Connecting everything

To connect everything, in the main function we can do :

package main

import (
	"fmt"
	"log"
)


type USBPortReceiver interface {
	Receive(msg string) error
}

type USBPortSender interface {
	Send(msg string) error
}


type Monitor struct {
}

func (m Monitor) Receive(msg string) error {
	log.Println("monitor received:", msg)
	fmt.Println(msg)
	return nil
}


type Computer struct {
	usb2Port USBPortReceiver
}

func (c Computer) Send(msg string) error {
	log.Println("computer received data:", msg)
	return c.usb2Port.Receive(msg)
}


type Keyboard struct {
	usb USBPortSender
}

func (k Keyboard) Type() error {
	a := "Hello John Doe"
	log.Println("typed:", a)
	return k.usb.Send(a)
}

func main() {
	m := Monitor{}
	c := Computer{
		usb2Port: m,
	}
	k := Keyboard{
		usb: c,
	}

	k.Type()
}

Running it:

 go run .
2020/07/26 15:45:44 typed: Hello John Doe
2020/07/26 15:45:44 computer received data: Hello John Doe
2020/07/26 15:45:44 monitor received: Hello John Doe
Hello John Doe

Why is this useful?

The same logic works with a REST server that connects to a database.

graph LR
    subgraph Core Layer
        Core --> DatabasePort
        CorePort -.-> Core
    end

    subgraph UI Layer
        Request -.-> Handler
        Handler -.-> CorePort
    end

    subgraph Infrastructure Layer
        DatabasePort --> MySQLAdapter
        MySQLAdapter --> db[(DB)]
    end

Still don’t get it?

Imagine you have the UI layer written with gin-gonic and you want to change to echo. Or you have a MySQL database in the Infrastructure layerand you want to change to Postgres:

graph LR

    subgraph Core Layer
        CorePort -.-> Core
        Core --> DatabasePort
        Core --> MessageBrokerPort
    end

    subgraph UI Layer
        HTTPRequest -.-> GinHandler
        HTTPRequest -.-> EchoHandler
        GinHandler -.-> CorePort
        EchoHandler -.-> CorePort
        CLI -.-> CLIHandler
        CLIHandler -.-> CorePort
        RabbitMQMessage -.-> RabbitMQHandler
        RabbitMQHandler -.-> CorePort
        gRPCCall -.-> gRPCHandler
        gRPCHandler -.-> CorePort
    end

    subgraph Infrastructure Layer
        DatabasePort --> MySQLAdapter
        DatabasePort --> PostgresAdapter
        MySQLAdapter --> mysql[(MySQL)]
        PostgresAdapter --> pq[(Postgres)]
        MessageBrokerPort --> RabbitMQAdapter
        MessageBrokerPort --> NatsAdapter
        NatsAdapter --> Nats[/Nats/]
        RabbitMQAdapter --> rmq[/RabbitMQ/]
    end

To change, you only need to implement the specific ports using the specific technology. Easier to change from gin-gonic to echo without breaking the Core which holds the Application Business Logic.

Moreover:

You can mock everything besides the core and test it isolated:

graph LR
    subgraph Core Layer
        CorePort -.-> Core
        Core --> DatabasePort
    end

    subgraph UI Layer
        MockHandler -.-> CorePort
    end

    subgraph Infrastructure Layer

        DatabasePort --> MockAdapter
    end

The business logic tests don’t need a specific technology to be implemented.

Or even test the other layers:

graph LR
    subgraph Core Layer
        CorePort --> MockCore
    end

    subgraph UI Layer
        HTTPRequest -.-> GinHandler
        HTTPRequest -.-> EchoHandler
        GinHandler -.-> CorePort
        EchoHandler -.-> CorePort
        CLI -.-> CLIHandler
        CLIHandler -.-> CorePort
        RabbitMQMessage -.-> RabbitMQHandler
        RabbitMQHandler -.-> CorePort
        gRPCCall -.-> gRPCHandler
        gRPCHandler -.-> CorePort
    end

Or Infrastructure

graph LR
    subgraph Core Layer
        MockCore --> DatabasePort
        MockCore --> MessageBrokerPort
    end

    subgraph Infrastructure Layer
        DatabasePort --> MySQLAdapter
        DatabasePort --> PostgresAdapter
        MySQLAdapter --> mysql[(MySQL)]
        PostgresAdapter --> pq[(Postgres)]
        MessageBrokerPort --> RabbitMQAdapter
        MessageBrokerPort --> NatsAdapter
        NatsAdapter --> Nats[/Nats/]
        RabbitMQAdapter --> rmq[/RabbitMQ/]
    end

hex

References

flowchart LR;
	subgraph Driver
		HTTP
	end
	subgraph Core
		direction TB;
		ControllerI
		subgraph Domain
			Models
		end

	end
	subgraph Driven
		DB
	end
	HTTP --> Core
	Core --> DB

	Domain --> ControllerI