home / skills / 89jobrien / steve / golang-enterprise-patterns
This skill helps you apply enterprise Go architecture patterns, enabling clean/hexagonal structures, DDD, and scalable production-ready code organization.
npx playbooks add skill 89jobrien/steve --skill golang-enterprise-patternsReview the files below or copy the command above to add this skill to your agents.
---
name: golang-enterprise-patterns
description: Enterprise-level Go architecture patterns including clean architecture,
hexagonal architecture, DDD, and production-ready application structure.
author: Joseph OBrien
status: unpublished
updated: '2025-12-23'
version: 1.0.1
tag: skill
type: skill
---
# Golang Enterprise Patterns
This skill provides guidance on enterprise-level Go application architecture, design patterns, and production-ready code organization.
## When to Use This Skill
- When designing new Go applications with complex business logic
- When implementing clean architecture or hexagonal architecture
- When applying Domain-Driven Design (DDD) principles
- When organizing large Go codebases
- When establishing patterns for team consistency
## Clean Architecture
### Layer Structure
```text
/cmd
/api - HTTP/gRPC entry points
/worker - Background job runners
/internal
/domain - Business entities and interfaces
/application - Use cases and application services
/infrastructure
/persistence - Database implementations
/messaging - Queue implementations
/http - HTTP client implementations
/interfaces
/api - HTTP handlers
/grpc - gRPC handlers
/pkg - Shared libraries (public)
```
### Dependency Rule
Dependencies flow inward only:
```text
Interfaces → Application → Domain
↓ ↓
Infrastructure (implements domain interfaces)
```
### Domain Layer
```go
// domain/user.go
package domain
import "time"
type UserID string
type User struct {
ID UserID
Email string
Name string
CreatedAt time.Time
}
// UserRepository defines the contract for user persistence
type UserRepository interface {
FindByID(ctx context.Context, id UserID) (*User, error)
FindByEmail(ctx context.Context, email string) (*User, error)
Save(ctx context.Context, user *User) error
Delete(ctx context.Context, id UserID) error
}
// UserService defines domain business logic
type UserService interface {
Register(ctx context.Context, email, name string) (*User, error)
Authenticate(ctx context.Context, email, password string) (*User, error)
}
```
### Application Layer
```go
// application/user_service.go
package application
type UserServiceImpl struct {
repo domain.UserRepository
hasher PasswordHasher
logger Logger
}
func NewUserService(repo domain.UserRepository, hasher PasswordHasher, logger Logger) *UserServiceImpl {
return &UserServiceImpl{repo: repo, hasher: hasher, logger: logger}
}
func (s *UserServiceImpl) Register(ctx context.Context, email, name string) (*domain.User, error) {
// Check if user exists
existing, err := s.repo.FindByEmail(ctx, email)
if err != nil && !errors.Is(err, domain.ErrNotFound) {
return nil, fmt.Errorf("checking existing user: %w", err)
}
if existing != nil {
return nil, domain.ErrUserAlreadyExists
}
user := &domain.User{
ID: domain.UserID(uuid.New().String()),
Email: email,
Name: name,
CreatedAt: time.Now(),
}
if err := s.repo.Save(ctx, user); err != nil {
return nil, fmt.Errorf("saving user: %w", err)
}
return user, nil
}
```
## Hexagonal Architecture (Ports & Adapters)
### Port Definitions
```go
// ports/primary.go - Driving ports (input)
package ports
type UserAPI interface {
CreateUser(ctx context.Context, req CreateUserRequest) (*UserResponse, error)
GetUser(ctx context.Context, id string) (*UserResponse, error)
}
// ports/secondary.go - Driven ports (output)
type UserStorage interface {
Save(ctx context.Context, user *domain.User) error
FindByID(ctx context.Context, id string) (*domain.User, error)
}
type NotificationSender interface {
SendWelcomeEmail(ctx context.Context, user *domain.User) error
}
```
### Adapter Implementations
```go
// adapters/postgres/user_repository.go
package postgres
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) Save(ctx context.Context, user *domain.User) error {
query := `INSERT INTO users (id, email, name, created_at) VALUES ($1, $2, $3, $4)`
_, err := r.db.ExecContext(ctx, query, user.ID, user.Email, user.Name, user.CreatedAt)
return err
}
```
## Domain-Driven Design (DDD)
### Aggregate Roots
```go
// domain/order/aggregate.go
package order
type Order struct {
id OrderID
customerID CustomerID
items []OrderItem
status OrderStatus
events []DomainEvent
}
func NewOrder(customerID CustomerID) *Order {
o := &Order{
id: OrderID(uuid.New().String()),
customerID: customerID,
status: StatusPending,
}
o.recordEvent(OrderCreated{OrderID: o.id, CustomerID: customerID})
return o
}
func (o *Order) AddItem(productID ProductID, quantity int, price Money) error {
if o.status != StatusPending {
return ErrOrderNotModifiable
}
o.items = append(o.items, OrderItem{
ProductID: productID,
Quantity: quantity,
Price: price,
})
return nil
}
func (o *Order) Submit() error {
if len(o.items) == 0 {
return ErrEmptyOrder
}
o.status = StatusSubmitted
o.recordEvent(OrderSubmitted{OrderID: o.id})
return nil
}
```
### Value Objects
```go
// domain/money.go
type Money struct {
amount int64 // cents
currency string
}
func NewMoney(amount int64, currency string) (Money, error) {
if amount < 0 {
return Money{}, ErrNegativeAmount
}
return Money{amount: amount, currency: currency}, nil
}
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, ErrCurrencyMismatch
}
return Money{amount: m.amount + other.amount, currency: m.currency}, nil
}
```
### Domain Events
```go
// domain/events.go
type DomainEvent interface {
EventName() string
OccurredAt() time.Time
}
type OrderCreated struct {
OrderID OrderID
CustomerID CustomerID
occurredAt time.Time
}
func (e OrderCreated) EventName() string { return "order.created" }
func (e OrderCreated) OccurredAt() time.Time { return e.occurredAt }
```
## Dependency Injection
### Wire-Style DI
```go
// wire.go
//+build wireinject
func InitializeApp(cfg *config.Config) (*App, error) {
wire.Build(
NewDatabase,
NewUserRepository,
NewUserService,
NewHTTPServer,
NewApp,
)
return nil, nil
}
```
### Manual DI (Preferred for Simplicity)
```go
// main.go
func main() {
cfg := config.Load()
db := database.Connect(cfg.DatabaseURL)
userRepo := postgres.NewUserRepository(db)
orderRepo := postgres.NewOrderRepository(db)
userService := application.NewUserService(userRepo)
orderService := application.NewOrderService(orderRepo, userRepo)
handler := api.NewHandler(userService, orderService)
server := http.NewServer(cfg.Port, handler)
server.Run()
}
```
## Error Handling Patterns
### Custom Error Types
```go
// domain/errors.go
type Error struct {
Code string
Message string
Err error
}
func (e *Error) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *Error) Unwrap() error { return e.Err }
var (
ErrNotFound = &Error{Code: "NOT_FOUND", Message: "resource not found"}
ErrUserAlreadyExists = &Error{Code: "USER_EXISTS", Message: "user already exists"}
ErrInvalidInput = &Error{Code: "INVALID_INPUT", Message: "invalid input"}
)
```
## Configuration Management
```go
// config/config.go
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
}
func Load() (*Config, error) {
cfg := &Config{}
cfg.Server.Port = getEnvInt("PORT", 8080)
cfg.Server.ReadTimeout = getEnvDuration("READ_TIMEOUT", 30*time.Second)
cfg.Database.URL = mustGetEnv("DATABASE_URL")
cfg.Database.MaxConns = getEnvInt("DB_MAX_CONNS", 25)
return cfg, nil
}
```
## Best Practices
1. **Keep domain pure** - No framework dependencies in domain layer
2. **Interface segregation** - Small, focused interfaces
3. **Dependency inversion** - Depend on abstractions, not concretions
4. **Explicit dependencies** - Pass dependencies via constructor
5. **Fail fast** - Validate at boundaries, trust internal code
6. **Make illegal states unrepresentable** - Use types to enforce invariants
This skill documents enterprise-level Go architecture patterns and a production-ready folder structure for building maintainable, testable services. It covers clean architecture, hexagonal (ports & adapters), Domain-Driven Design (DDD), dependency injection, error handling, and configuration management. The guidance focuses on keeping domain logic pure, explicit dependencies, and practical patterns for teams and large codebases.
The skill explains a layered project layout with clear responsibilities: domain, application, infrastructure, and interfaces, plus cmd and pkg for entry points and shared libraries. It defines ports (driving/driven), adapter examples (e.g., Postgres repositories), and DDD constructs like aggregates, value objects, and domain events. It also shows DI approaches (manual and wire-style), custom error types, and environment-driven configuration loading to produce reproducible, production-ready apps.
Should I always use Wire for dependency injection?
No. Wire is useful for large projects with complex wiring, but manual DI is simpler, more explicit, and often preferred for smaller services.
Where should interfaces live?
Place interfaces in the layer that depends on them (upstream). For example, domain and application layers define contracts; infrastructure provides implementations.