CODESAMPLE

Hexagonal Architecture - Go

Share on:

The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled, testable software by separating the core application logic from external concerns like databases, UI, and message queues. The core defines “ports” – interfaces that external actors interact with. “Adapters” implement these ports, translating external interactions into core-compatible actions and vice versa.

This Go example demonstrates a simple Hexagonal Architecture for a user service. The core package contains the application logic and defines the UserRepository port. The adapters package provides implementations for in-memory and (mock) external user repositories. The cmd package serves as the entry point, using an adapter to interact with the core. The separation allows swapping implementations (e.g., switching databases) without modifying core logic. This aligns with Go’s emphasis on interfaces and composition over inheritance.

// main.go
package main

import (
	"fmt"
	"hexagonal/core"
	"hexagonal/adapters"
)

func main() {
	// Choose an adapter (InMemoryRepository in this case)
	repo := adapters.NewInMemoryUserRepository()

	// Create the user service with the adapter
	userService := core.NewUserService(repo)

	// Interact with the service
	user, err := userService.GetUser(1)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("User: %+v\n", user)

	updatedUser, err := userService.UpdateUser(user.ID, "New Name")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("Updated User: %+v\n", updatedUser)
}

// core/user_service.go
package core

type UserRepository interface {
	GetUser(id int) (*User, error)
	UpdateUser(user *User) (*User, error)
}

type UserService struct {
	repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
	return &UserService{repo: repo}
}

func (s *UserService) GetUser(id int) (*User, error) {
	return s.repo.GetUser(id)
}

func (s *UserService) UpdateUser(id int, name string) (*User, error) {
	user, err := s.repo.GetUser(id)
	if err != nil {
		return nil, fmt.Errorf("user not found: %w", err)
	}
	user.Name = name
	return s.repo.UpdateUser(user)
}

type User struct {
	ID   int
	Name string
}


// adapters/user_repository.go
package adapters

import "fmt"

type InMemoryUserRepository struct {
	users map[int]*core.User
}

func NewInMemoryUserRepository() *InMemoryUserRepository {
	return &InMemoryUserRepository{
		users: map[int]*core.User{
			1: {ID: 1, Name: "Old Name"},
			2: {ID: 2, Name: "Another Name"},
		},
	}
}

func (r *InMemoryUserRepository) GetUser(id int) (*core.User, error) {
	user, ok := r.users[id]
	if !ok {
		return nil, fmt.Errorf("user with id %d not found", id)
	}
	return user, nil
}

func (r *InMemoryUserRepository) UpdateUser(user *core.User) (*core.User, error) {
	r.users[user.ID] = user
	return user, nil
}