Hexagonal Architecture, also known as Ports and Adapters, is a design pattern introduced by Alistair Cockburn. It aims to create a loosely coupled application where the core logic is independent of external systems, such as user interfaces, databases, or third-party services. This independence makes the application more maintainable, testable, and adaptable to change.
Decoupling Core Logic from External Systems:
Ports and Adapters:
Testability:
Maintainability and Flexibility:
Domain-Driven Design (DDD):
Layered Architecture:
Hexagonal Architecture:
Clean Architecture:
Hexagonal Architecture:
Microservices Architecture:
Hexagonal Architecture:
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
Define the core business entity.
go// internal/core/domain/post.go
package domain
type Post struct {
ID int
Title string
Content string
}
Define the interfaces for the repository.
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.
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.
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.
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)
}
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() {
db, err := sql.Open("mysql", config.GetEnv("MYSQL_DSN", "user:password@tcp(127.0.0.1:3306)/myapp"))
if err != nil {
panic(err)
}
var postRepo ports.PostRepository = repositories.NewPostGORMRepo(db)
postService := services.NewPostService(postRepo)
postHandler := http.NewPostHandler(postService)
http.HandleFunc("/post", postHandler.GetPost)
http.HandleFunc("/create", postHandler.CreatePost)
http.ListenAndServe(":8080", nil)
}
By understanding and implementing Hexagonal Architecture principles, you can create flexible, maintainable, and testable applications that are resilient to changes in external systems.