Hexagonal Architecture¶
Understanding the architecture pattern and dependency rules in HexaGo-generated projects.
Core Principle¶
The dependency rule: adapters → services → domain
External World Adapters Services Domain
(HTTP, DB) → (primary/ → (services/ → (domain/
secondary) ports/) entities)
- Core has zero external dependencies
- Adapters implement interfaces defined by the core
- Dependency direction is always inward
Layer Structure¶
Domain Layer (internal/core/domain/)¶
Pure business entities with no external imports:
// No imports from adapters or external packages
type User struct {
ID uuid.UUID
Email string
Name string
CreatedAt time.Time
}
func (u *User) Validate() error {
if u.Email == "" {
return ErrEmailRequired
}
return nil
}
Contains:
- Entities (objects with unique identity)
- Value objects (immutable, compared by value)
- Domain errors
Services Layer (internal/core/services/)¶
Business logic and use cases. Defines port interfaces:
type UserService struct {
store Store
}
type Store interface {
GetUser(ctx context.Context, id string) (*User, error)
SaveUser(ctx context.Context, user *User) error
DeleteUser(ctx context.Context, id string) error
}
Contains:
- Service structs with business logic
- Port interfaces (what the core needs from outside)
- Use case implementations
Adapters Layer (internal/adapters/)¶
External interfaces — implements ports defined by services.
| Direction | Type | Examples |
|---|---|---|
| Primary (inbound) | Driven by external actors | HTTP handlers, gRPC servers, CLI commands, queue consumers |
| Secondary (outbound) | Drives external systems | Database repositories, external API clients, cache adapters, notifiers |
// Primary adapter - HTTP handler
type UserHandler struct {
service *services.UserService
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
// Adapts HTTP request → service call → HTTP response
}
// Secondary adapter - database repository
type SQLiteUserRepository struct {
db *sql.DB
}
func (r *SQLiteUserRepository) GetUser(ctx context.Context, id string) (*User, error) {
// Implements the Store port interface
}
Port Patterns¶
Defining Ports¶
Ports are interfaces defined in the services layer:
// internal/core/ports/store.go
package ports
type Store interface {
GetUser(ctx context.Context, id string) (*domain.User, error)
SaveUser(ctx context.Context, user *domain.User) error
ListUsers(ctx context.Context) ([]*domain.User, error)
DeleteUser(ctx context.Context, id string) error
}
Implementing Ports¶
Adapters implement these interfaces:
// internal/adapters/secondary/database/user_repository.go
package database
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) GetUser(ctx context.Context, id string) (*domain.User, error) {
// Implementation
}
var _ ports.Store = (*UserRepository)(nil) // Compile-time interface check
Key Interfaces (Example)¶
These are common port interfaces in HexaGo projects:
| Interface | Purpose | Methods |
|---|---|---|
Store |
Data persistence | Get, Save, List, Delete |
FeedProvider |
External data feeds | History, Subscribe, Ping |
Notifier |
Notifications | SendSignal, SendUpdate, SendError |
Reporter |
Report generation | WriteSimulation, WriteBacktest |
Dependency Injection¶
Services receive dependencies via constructors:
func NewUserService(store ports.Store, logger Logger) *UserService {
return &UserService{
store: store,
logger: logger,
}
}
The cmd/run.go wire-up assembles the application:
// cmd/run.go
func run() error {
// Create adapters
repo := database.NewUserRepository(db)
notifier := telegram.NewClient(apiKey)
// Create services with adapter implementations
userSvc := services.NewUserService(repo, logger)
// Create primary adapters (handlers)
handler := http.NewUserHandler(userSvc)
// Start server with handler
return httpServer.Serve(handler.Routes())
}
Validation¶
Run architecture validation after any code changes:
Checks performed:
- ✓ Core domain has no external dependencies
- ✓ Services only depend on domain and ports
- ✓ Adapters don't import from other adapters
- ✓ Proper dependency direction (adapters → core)
- ✓ Proper package organization and naming
Naming Variants¶
Default (primary-secondary)¶
DDD / Driver-Driven¶
When using --adapter-style driver-driven:
internal/
├── adapters/
│ ├── driver/ # Inbound (drives the application)
│ └── driven/ # Outbound (driven by the application)
Anti-Patterns to Avoid¶
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Core imports adapters | Violates dependency rule | Define port in domain, implement in adapter |
| Business logic in handlers | Leaky abstraction | Move to services |
| Database types in domain | External dependency in core | Use domain types, map in adapter |
| Direct HTTP calls in services | Tight coupling | Create external client adapter |