home / skills / 89jobrien / steve / golang-enterprise-patterns

golang-enterprise-patterns skill

/steve/skills/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-patterns

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
8.4 KB
---
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

Overview

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.

How this skill works

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.

When to use it

  • Designing new Go services with non-trivial business logic and team collaboration
  • Adopting clean architecture or hexagonal architecture to decouple business rules from frameworks
  • Applying Domain-Driven Design for complex domains with aggregates, invariants, and domain events
  • Organizing and scaling large monorepos or multi-service codebases for clarity and testability
  • Defining team conventions for interfaces, dependency injection, and error handling

Best practices

  • Keep the domain layer framework-free and focused on business rules
  • Depend on abstractions: define interfaces in upstream layers and implement them downstream
  • Pass dependencies explicitly via constructors; prefer manual DI for simplicity
  • Make illegal states unrepresentable with types and validate at boundaries (fail fast)
  • Use small, focused interfaces to enable easier testing and substitution

Example use cases

  • Implementing a user registration and authentication flow with domain services and repository adapters
  • Building an order processing system using aggregates, value objects, and domain events
  • Creating an HTTP or gRPC API that uses ports for input and storage adapters for persistence
  • Migrating a legacy codebase into layered packages to separate concerns and improve testability
  • Setting up environment-driven configuration and connection wiring for production deployments

FAQ

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.