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 layer
and 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
References
- https://www.youtube.com/watch?v=vKbVrsMnhDc
- https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/
- https://github.com/lucaskatayama/learn-hexagonal
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