# Table of Contents

1. [Overview](#overview)
2. [SOLID Principles](#solid-principles)
3. [Hexagonal Architecture](#hexagonal-architecture)
4. [Domain Layer](#domain-layer)
5. [Ports](#ports)
6. [Services](#services)
7. [Adapters](#adapters)
8. [Provider Pattern](#provider-pattern)
9. [Directory Structure](#directory-structure)

---

## Overview

Production-ready Go backend template using hexagonal (ports & adapters) architecture with DDD principles.

### Tech Stack

- **Gin** - HTTP web framework
- **PostgreSQL/pgx** - Database with connection pooling
- **OpenTelemetry** - Distributed tracing
- **Prometheus** - Metrics
- **Keycloak** - JWT authentication via keyfunc/v3

### When to Use

- Building REST API services in Go requiring production readiness
- Projects requiring clean separation of concerns
- Applications needing observability (logging, metrics, tracing)
- Services with PostgreSQL caching layer for high performance

---

## SOLID Principles

### Single Responsibility (SRP)

Each component has one reason to change.

**Good:**
```go
type UserService interface {
    GetByNIP(ctx context.Context, nip string) (*User, error)
    Create(ctx context.Context, user *User) error
    List(ctx context.Context) ([]User, error)
}
```

**Bad:**
```go
type AdminService interface {
    GetUser()
    SendEmail()        // Wrong: Not user-related
    ProcessPayment()   // Wrong: Different domain
    GenerateReport()   // Wrong: Reporting concern
}
```

### Dependency Inversion (DIP)

High-level modules depend on abstractions, not concrete implementations.

**Good:**
```go
type userService struct {
    userRepo outbound.UserRepository  // Interface, not concrete
}

func NewUserService(repo outbound.UserRepository) UserService {
    return &userService{userRepo: repo}
}
```

**Bad:**
```go
type userService struct {
    db *sql.DB  // Concrete PostgreSQL driver!
}

func NewUserService() *userService {
    return &userService{
        db: connectPostgres(),  // Hard-coded dependency
    }
}
```

**Test vs Production (LSP compliance):**
```go
mockRepo := &mockUserRepository{}              // Test
pgRepo := persistence.NewUserRepository(pool)  // Production

svc := usecase.NewUserService(mockRepo)  // Works
svc := usecase.NewUserService(pgRepo)    // Works
```

---

## Hexagonal Architecture

```
                    ┌─────────────────────────────────┐
                    │         Application            │
                    │                                │
  ┌─────────┐      │  ┌─────────────────────────┐   │
  │  Input  │────────►│     Domain (Core)      │◄──────┐
  │(REST/gRPC)│      │  │  Entities, Services    │       │
  └─────────┘      │  │  Business Rules         │       │
                   │  └─────────────────────────┘   │   ┌─────────┐
                   │              │                │◄──│ Output  │
                   │              ▼                │   │(DB/Cache│
                   │  ┌─────────────────────────┐ │   │/Ext API)│
                   │  │        Ports            │ │   └─────────┘
                   │  │  (Interfaces/Contracts) │ │
                   │  └─────────────────────────┘ │
                   └─────────────────────────────────┘
```

**Key Principles:**

1. Domain contains business logic, no external dependencies
2. Ports define interfaces (contracts)
3. Adapters implement ports and handle external concerns
4. Services use ports, not adapters

---

## Domain Layer

### Entities

Pure business entities with no external dependencies:

```go
package domain

import "time"

type User struct {
    NIP         string    `db:"nip" json:"nip"`
    Name        string    `db:"name" json:"name"`
    Email       string    `db:"email" json:"email"`
    IsActive    bool      `db:"is_active" json:"is_active"`
    LastLoginAt time.Time `db:"last_login_at" json:"last_login_at"`
    CreatedAt   time.Time `db:"created_at" json:"created_at"`
    UpdatedAt   time.Time `db:"updated_at" json:"updated_at"`
    Roles       []string  `db:"-" json:"roles,omitempty"`
}
```

### Value Objects

```go
package domain

import "errors"

type NIP string

func (n NIP) Validate() error {
    if len(n) == 0 {
        return errors.New("nip is required")
    }
    if len(n) > 20 {
        return errors.New("nip must not exceed 20 characters")
    }
    return nil
}
```

### Domain Events

```go
package event

type EventType string

const (
    UserCreated      EventType = "user.created"
    UserUpdated      EventType = "user.updated"
    UserDeactivated  EventType = "user.deactivated"
)

type UserCreatedEvent struct {
    NIP       string
    Name      string
    Email     string
    Timestamp time.Time
}

func (e *UserCreatedEvent) EventType() EventType { return UserCreated }
func (e *UserCreatedEvent) Timestamp() time.Time  { return e.Timestamp }
```

---

## Ports

### Inbound Ports (Service Interfaces)

Define contracts for use cases:

```go
package inbound

import (
    "context"
    "myapp/internal/core/domain"
)

type UserService interface {
    GetByNIP(ctx context.Context, nip string) (*domain.User, error)
    Create(ctx context.Context, user *domain.User) error
    List(ctx context.Context) ([]domain.User, error)
}
```

### Outbound Ports (Repository Interfaces)

Define contracts for external dependencies:

```go
package outbound

import (
    "context"
    "time"
    "myapp/internal/core/domain"
)

type UserRepository interface {
    GetByNIP(ctx context.Context, nip string) (*domain.User, error)
    Create(ctx context.Context, user *domain.User) error
    Update(ctx context.Context, user *domain.User) error
    List(ctx context.Context) ([]domain.User, error)
}

type CacheRepository interface {
    Get(ctx context.Context, key string) ([]byte, error)
    Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
    Delete(ctx context.Context, key string) error
    Exists(ctx context.Context, key string) (bool, error)
}
```

---

## Services

Implement inbound ports, depend on outbound ports:

```go
package usecase

import (
    "context"
    "encoding/json"
    "time"

    "myapp/internal/core/domain"
    "myapp/internal/core/port/inbound"
    "myapp/internal/core/port/outbound"

    "github.com/pkg/errors"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
)

var tracer = otel.Tracer("usecase")

type userService struct {
    userRepo  outbound.UserRepository
    cacheRepo outbound.CacheRepository
}

func NewUserService(
    userRepo outbound.UserRepository,
    cacheRepo outbound.CacheRepository,
) inbound.UserService {
    return &userService{
        userRepo:  userRepo,
        cacheRepo: cacheRepo,
    }
}

func (s *userService) GetByNIP(ctx context.Context, nip string) (*domain.User, error) {
    ctx, span := tracer.Start(ctx, "UserService.GetByNIP",
        trace.WithAttributes(attribute.String("user.nip", nip)),
    )
    defer span.End()

    // Cache-aside pattern
    cacheKey := "user:" + nip
    cached, err := s.cacheRepo.Get(ctx, cacheKey)
    if err == nil && cached != nil {
        span.AddEvent("cache_hit")
        var user domain.User
        if err := json.Unmarshal(cached, &user); err == nil {
            return &user, nil
        }
    }

    span.AddEvent("cache_miss")
    user, err := s.userRepo.GetByNIP(ctx, nip)
    if err != nil {
        span.RecordError(err)
        return nil, errors.Wrap(err, "failed to get user")
    }

    if user == nil {
        return nil, nil
    }

    // Cache result
    if data, err := json.Marshal(user); err == nil {
        _ = s.cacheRepo.Set(ctx, cacheKey, data, 5*time.Minute)
    }

    return user, nil
}
```

---

## Adapters

### Inbound (Driving) Adapters

```go
package v1

import (
    "net/http"

    "myapp/internal/core/port/inbound"
    "myapp/pkg/response"

    "github.com/gin-gonic/gin"
)

type UserHandler struct {
    svc inbound.UserService
}

func NewUserHandler(svc inbound.UserService) *UserHandler {
    return &UserHandler{svc: svc}
}

func (h *UserHandler) GetByNIP(c *gin.Context) {
    resp := response.Response{}
    defer resp.Render(c)

    nip := c.Param("nip")
    user, err := h.svc.GetByNIP(c.Request.Context(), nip)
    if err != nil {
        resp.SetError(err, http.StatusInternalServerError)
        return
    }
    if user == nil {
        resp.SetError(errors.New("user not found"), http.StatusNotFound)
        return
    }

    resp.Data = user
    resp.StatusCode = http.StatusOK
}

func (h *UserHandler) RegisterRoutes(r *gin.RouterGroup) {
    users := r.Group("/users")
    {
        users.GET("/:nip", h.GetByNIP)
        users.POST("/", h.Create)
    }
}
```

### Outbound (Driven) Adapters

```go
package persistence

import (
    "context"

    "myapp/internal/core/domain"
    "myapp/internal/core/port/outbound"

    "github.com/jackc/pgx/v5"
    "github.com/pkg/errors"
)

type userRepository struct {
    pool *Pool
}

func NewUserRepository(pool *Pool) outbound.UserRepository {
    return &userRepository{pool: pool}
}

func (r *userRepository) GetByNIP(ctx context.Context, nip string) (*domain.User, error) {
    var user domain.User
    err := r.pool.QueryRow(ctx,
        `SELECT nip, name, email, is_active, last_login_at, created_at, updated_at
         FROM users WHERE nip = $1`,
        nip,
    ).Scan(&user.NIP, &user.Name, &user.Email, &user.IsActive,
        &user.LastLoginAt, &user.CreatedAt, &user.UpdatedAt)

    if err == pgx.ErrNoRows {
        return nil, nil
    }
    if err != nil {
        return nil, errors.Wrap(err, "failed to get user")
    }

    return &user, nil
}
```

---

## Provider Pattern

Explicit dependency wiring via Provider struct:

```go
package provider

import (
    "context"
    "fmt"

    "myapp/config"
    "myapp/internal/adapter/outbound/cache"
    "myapp/internal/adapter/outbound/keycloak"
    "myapp/internal/adapter/outbound/persistence"
    "myapp/internal/core/port/inbound"
    "myapp/internal/core/usecase"
    "myapp/pkg/logger"
    "myapp/pkg/metrics"
    "myapp/pkg/tracing"
)

type Provider struct {
    Config         *config.Config
    Logger        *logger.Logger
    Metrics       *metrics.Prometheus
    Tracer        *tracing.TracerProvider
    PersistencePool *persistence.Pool
    CachePool     *persistence.Pool
    Keycloak      *keycloak.Provider
    UserRepo      outbound.UserRepository
    CacheRepo     outbound.CacheRepository
    AuthSvc       inbound.AuthService
    UserSvc       inbound.UserService
}

func NewProvider(ctx context.Context, cfg *config.Config) (*Provider, error) {
    p := &Provider{Config: cfg}

    if err := p.initLogger(); err != nil {
        return nil, fmt.Errorf("logger: %w", err)
    }
    if err := p.initMetrics(); err != nil {
        return nil, fmt.Errorf("metrics: %w", err)
    }
    if err := p.initTracer(ctx); err != nil {
        return nil, fmt.Errorf("tracer: %w", err)
    }
    if err := p.initPersistence(ctx); err != nil {
        return nil, fmt.Errorf("persistence: %w", err)
    }
    if err := p.initCache(ctx); err != nil {
        return nil, fmt.Errorf("cache: %w", err)
    }
    if err := p.initKeycloak(ctx); err != nil {
        return nil, fmt.Errorf("keycloak: %w", err)
    }

    p.UserRepo = persistence.NewUserRepository(p.PersistencePool)
    p.CacheRepo = cache.NewPostgresCacheRepository(p.CachePool)

    p.AuthSvc = usecase.NewAuthService(p.Keycloak, p.UserRepo)
    p.UserSvc = usecase.NewUserService(p.UserRepo, p.CacheRepo)

    return p, nil
}

func (p *Provider) Close(ctx context.Context) error {
    if p.PersistencePool != nil {
        p.PersistencePool.Close()
    }
    if p.CachePool != nil {
        p.CachePool.Close()
    }
    if p.Tracer != nil {
        p.Tracer.Shutdown(ctx)
    }
    return nil
}
```

---

## Directory Structure

```
.
├── cmd/
│   ├── server/           # HTTP server entry point
│   │   └── run.go
│   ├── migrate/          # Migration commands
│   │   └── run.go
│   ├── seed/             # Seeding commands
│   │   └── run.go
│   └── admin/            # Admin CLI commands
│       └── run.go
├── config/
│   ├── config.go         # Config loading
│   ├── model.go          # Config structs
│   └── loader.go         # OpenBao/env loader
├── internal/
│   ├── core/
│   │   ├── domain/      # Domain entities, events
│   │   ├── port/        # Inbound/outbound interfaces
│   │   └── usecase/     # Business logic
│   ├── adapter/
│   │   ├── inbound/     # HTTP handlers
│   │   └── outbound/    # DB, cache, keycloak adapters
│   └── provider/         # Dependency provider
├── middlewares/          # HTTP middleware
├── pkg/
│   ├── logger/          # Structured logging
│   ├── metrics/         # Prometheus metrics
│   ├── response/        # HTTP response utilities
│   └── tracing/         # OpenTelemetry tracing
├── db/migrations/        # Database migrations
└── deployments/          # Docker, CI/CD configs
```
