Comment bien architecturer son projet Golang en hexagonal ?
Comment bien architecturer son projet Golang en hexagonal ?
Quand on démarre un projet en Go, il est tentant de tout mettre dans un seul main.go ou de multiplier les packages sans réelle cohérence.
Le problème ? On finit vite avec une application rigide, difficile à tester et à faire évoluer.
C’est là qu’entre en jeu l’architecture hexagonale (aussi appelée ports & adapters).
Elle permet de séparer le cœur métier des interfaces techniques (API, DB, CLI…), et d’obtenir un code clair, testable et maintenable.
🧩 Les 3 couches principales
-
Domain (Core)
- Contient la logique métier pure.
- Aucun import vers des frameworks ou librairies externes.
- Exemples : entités, règles métier, interfaces (ports).
-
Application (Use Cases)
- Orchestration des cas d’usage.
- Exemples :
CreateUser,ProcessOrder.
-
Infrastructure (Adapters)
- Implémentations concrètes : HTTP, DB, cache, queues…
- Exemples :
PostgresUserRepository,GinHTTPHandler.
📂 Exemple de structure projet
/project
├── cmd/
│ └── app/ # point d’entrée main.go
├── internal/
│ ├── domain/ # entités + ports
│ │ └── user.go
│ ├── app/ # use cases
│ │ └── user_service.go
│ └── infra/ # implémentations techniques
│ ├── http/ # handlers REST
│ └── db/ # repository SQL
└── pkg/ # libs partagées
👨💻 Exemple de code
1. Domain — entité et port
// internal/domain/user.go
package domain
type User struct {
ID string
Name string
Email string
}
// Port : contrat pour persistance
type UserRepository interface {
Save(user *User) error
FindByEmail(email string) (*User, error)
}
2. Application — use case
// internal/app/user_service.go
package app
import "project/internal/domain"
type UserService struct {
repo domain.UserRepository
}
func NewUserService(r domain.UserRepository) *UserService {
return &UserService{repo: r}
}
func (s *UserService) RegisterUser(name, email string) (*domain.User, error) {
user := &domain.User{
ID: "uuid-generated", // simplifié
Name: name,
Email: email,
}
if err := s.repo.Save(user); err != nil {
return nil, err
}
return user, nil
}
3. Infrastructure — adapter DB (ex. in-memory)
// internal/infra/db/user_repo.go
package db
import "project/internal/domain"
type InMemoryUserRepo struct {
users map[string]*domain.User
}
func NewInMemoryUserRepo() *InMemoryUserRepo {
return &InMemoryUserRepo{users: make(map[string]*domain.User)}
}
func (r *InMemoryUserRepo) Save(user *domain.User) error {
r.users[user.Email] = user
return nil
}
func (r *InMemoryUserRepo) FindByEmail(email string) (*domain.User, error) {
if u, ok := r.users[email]; ok {
return u, nil
}
return nil, nil
}
4. Infrastructure — adapter HTTP (ex. avec Gin)
// internal/infra/http/user_handler.go
package http
import (
"net/http"
"project/internal/app"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
service *app.UserService
}
func NewUserHandler(r *gin.Engine, s *app.UserService) {
handler := &UserHandler{service: s}
r.POST("/users", handler.Register)
}
func (h *UserHandler) Register(c *gin.Context) {
var req struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.service.RegisterUser(req.Name, req.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
5. Entrypoint — main
// cmd/app/main.go
package main
import (
"project/internal/app"
"project/internal/infra/db"
"project/internal/infra/http"
"github.com/gin-gonic/gin"
)
func main() {
repo := db.NewInMemoryUserRepo()
service := app.NewUserService(repo)
r := gin.Default()
http.NewUserHandler(r, service)
r.Run(":8080")
}
✅ Avantages de cette approche
Testabilité : je peux tester UserService avec un repo in-memory, sans DB réelle.
Évolutivité : remplacer InMemoryUserRepo par PostgresUserRepo ne touche pas la logique métier.
Clarté : chaque couche a une responsabilité unique.
🚀 Conclusion
L’architecture hexagonale en Go n’est pas un dogme, mais un cadre pratique pour séparer votre métier de la technique. Avec cette approche, vous obtenez un projet plus clair, maintenable et prêt à évoluer.
🤝 Besoin d’aide ?
J’accompagne les équipes dans la mise en place d’architectures robustes en Go. Si vous voulez poser des bases solides ou refactorer un projet existant, je peux vous guider.
👉 Contactez-moi et gagnons ensemble du temps sur vos futurs développements Golang.