In this section, we will walk through the implementation of Hexagonal Architecture in a Go application, focusing on defining ports and adapters and integrating external services.
We will build a simple blog application that supports creating and retrieving posts. The architecture will ensure that the core business logic is isolated from external systems, making the application more modular, maintainable, and testable.
The project structure will be organized to reflect Hexagonal Architecture principles, with distinct directories for core business logic, ports, and adapters.
gomyapp/
│
├── cmd/
│ └── myapp/
│ └── main.go
├── internal/
│ ├── adapters/
│ │ ├── http/
│ │ │ └── post_handler.go
│ │ ├── repositories/
│ │ │ └── post_repository.go
│ ├── core/
│ │ ├── domain/
│ │ │ └── post.go
│ │ └── ports/
│ │ └── post_repository.go
│ ├── services/
│ │ └── post_service.go
├── pkg/
│ └── config/
│ └── config.go
└── go.mod
First, define the core business entity, which in this case is a Post
.
go// internal/core/domain/post.go
package domain
type Post struct {
ID int
Title string
Content string
}
Define the interfaces for the repository. These ports serve as the contract between the core business logic and the external systems.
go// internal/core/ports/post_repository.go
package ports
import "myapp/internal/core/domain"
type PostRepository interface {
GetByID(id int) (*domain.Post, error)
Save(post *domain.Post) error
}
Implement the business logic using the defined ports. The service interacts with the repository via the port interface.
go// internal/services/post_service.go
package services
import (
"myapp/internal/core/domain"
"myapp/internal/core/ports"
)
type PostService struct {
Repo ports.PostRepository
}
func NewPostService(repo ports.PostRepository) *PostService {
return &PostService{Repo: repo}
}
func (s *PostService) GetPost(id int) (*domain.Post, error) {
return s.Repo.GetByID(id)
}
func (s *PostService) CreatePost(post *domain.Post) error {
return s.Repo.Save(post)
}
Implement the repository to interact with the database. This adapter fulfills the contract defined by the PostRepository
port.
go// internal/adapters/repositories/post_repository.go
package repositories
import (
"database/sql"
"myapp/internal/core/domain"
"myapp/internal/core/ports"
)
type PostGORMRepo struct {
DB *sql.DB
}
func NewPostGORMRepo(db *sql.DB) *PostGORMRepo {
return &PostGORMRepo{DB: db}
}
func (repo *PostGORMRepo) GetByID(id int) (*domain.Post, error) {
post := &domain.Post{}
row := repo.DB.QueryRow("SELECT id, title, content FROM posts WHERE id = ?", id)
err := row.Scan(&post.ID, &post.Title, &post.Content)
if err != nil {
return nil, err
}
return post, nil
}
func (repo *PostGORMRepo) Save(post *domain.Post) error {
_, err := repo.DB.Exec("INSERT INTO posts (title, content) VALUES (?, ?)", post.Title, post.Content)
return err
}
Implement the HTTP handlers to handle web requests. These adapters convert HTTP requests into calls to the core business logic.
go// internal/adapters/http/post_handler.go
package http
import (
"encoding/json"
"myapp/internal/core/domain"
"myapp/internal/services"
"net/http"
"strconv"
)
type PostHandler struct {
Service *services.PostService
}
func NewPostHandler(service *services.PostService) *PostHandler {
return &PostHandler{Service: service}
}
func (h *PostHandler) GetPost(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
post, err := h.Service.GetPost(id)
if err != nil {
http.Error(w, "Post not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(post)
}
func (h *PostHandler) CreatePost(w http.ResponseWriter, r *http.Request) {
var post domain.Post
if err := json.NewDecoder(r.Body).Decode(&post); err != nil {
http.Error(w, "Invalid input", http.StatusBadRequest)
return
}
if err := h.Service.CreatePost(&post); err != nil {
http.Error(w, "Could not create post", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
Set up the database connection.
go// pkg/config/config.go
package config
import (
"os"
)
func GetEnv(key string, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}
Wire everything together in the main application.
go// cmd/myapp/main.go
package main
import (
"database/sql"
"myapp/internal/adapters/http"
"myapp/internal/adapters/repositories"
"myapp/internal/core/ports"
"myapp/internal/services"
"myapp/pkg/config"
"net/http"
_ "github.com/go-sql-driver/mysql"
)
func main() {
dsn := config.GetEnv("MYSQL_DSN", "user:password@tcp(127.0.0.1:3306)/myapp")
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
postRepo := repositories.NewPostGORMRepo(db)
postService := services.NewPostService(postRepo)
postHandler := http.NewPostHandler(postService)
http.HandleFunc("/post", postHandler.GetPost)
http.HandleFunc("/create", postHandler.CreatePost)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
Integrating external services (such as third-party APIs or other microservices) follows the same principles of Hexagonal Architecture. Define ports for the interactions and create adapters to handle the specifics of the external service communication.
Let's say we want to send a notification whenever a new post is created. We will define a port for the notification service and implement an adapter to interact with a hypothetical external notification service.
go// internal/core/ports/notification_service.go
package ports
type NotificationService interface {
SendNotification(message string) error
}
go// internal/adapters/notification/email_notification.go
package notification
import (
"fmt"
"myapp/internal/core/ports"
)
type EmailNotificationService struct {
SMTPServer string
Port int
Username string
Password string
}
func NewEmailNotificationService(smtpServer string, port int, username, password string) *EmailNotificationService {
return &EmailNotificationService{
SMTPServer: smtpServer,
Port: port,
Username: username,
Password: password,
}
}
func (s *EmailNotificationService) SendNotification(message string) error {
// Simplified example of sending an email notification
fmt.Printf("Sending email notification: %s\n", message)
return nil
}
go// internal/services/post_service.go
package services
import (
"myapp/internal/core/domain"
"myapp/internal/core/ports"
)
type PostService struct {
Repo ports.PostRepository
Notification ports.NotificationService
}
func NewPostService(repo ports.PostRepository, notification ports.NotificationService) *PostService {
return &PostService{
Repo: repo,
Notification: notification,
}
}
func (s *PostService) GetPost(id int) (*domain.Post, error) {
return s.Repo.GetByID(id)
}
func (s *PostService) CreatePost(post *domain.Post) error {
err := s.Repo.Save(post)
if err != nil {
return err
}
return s.Notification.SendNotification("New post created: " + post.Title)
}
go// cmd/myapp/main.go
package main
import (
"database/sql"
"myapp/internal/adapters/http"
"myapp/internal/adapters/notification"
"myapp/internal/adapters/repositories"
"myapp/internal/core/ports"
"myapp/internal/services"
"myapp/pkg/config"
"net/http"
_ "github.com/go-sql-driver/mysql"
)
func main() {
dsn := config.GetEnv("MYSQL_DSN", "user:password@tcp(127.0.0.1:3306)/myapp")
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
postRepo := repositories.NewPostGORMRepo(db)
emailService := notification.NewEmailNotificationService("smtp.example.com", 587, "user", "pass")
postService := services.NewPostService(postRepo, emailService)
postHandler := http.NewPostHandler(postService)
http.HandleFunc("/post", postHandler.GetPost)
http.HandleFunc("/create", postHandler.CreatePost)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
By following the principles of Hexagonal Architecture, you can create a Go application that is modular, maintainable, and testable. The core business logic is decoupled from external systems through well-defined interfaces (ports) and concrete implementations (adapters). Integrating external services becomes straightforward, as you can add new adapters without modifying the core logic. This approach promotes clean code, better testability, and flexibility in adapting to changing requirements.