ring/dev-team/docs/standards/golang.md
Jefferson Rodrigues 675a4e3029
feat(standards): add TOC to all standards files and improve skill execution
- Add Table of Contents to devops.md, frontend.md, golang.md, sre.md, typescript.md
- Update CLAUDE.md with Four-File Update Rule and TOC maintenance guidelines
- Add checklist for adding/removing sections in standards files
- Improve dev-refactor skill to use Skill tool for dev-cycle handoff
- Add anti-rationalization patterns for gate execution shortcuts
- Add Execution Report sections to dev-cycle, dev-feedback-loop, dev-refactor
- Extract gap tracking rationalizations to shared-anti-rationalization.md

Generated-by: Claude
AI-Model: claude-opus-4-5-20251101
2025-12-17 12:47:03 -03:00

54 KiB
Raw Blame History

Go Standards

⚠️ MAINTENANCE: This file is indexed in dev-team/skills/shared-patterns/standards-coverage-table.md. When adding/removing ## sections, update the coverage table AND agent files per THREE-FILE UPDATE RULE in CLAUDE.md.

This file defines the specific standards for Go development at Lerian Studio.

Reference: Always consult docs/PROJECT_RULES.md for common project standards.


Table of Contents

# Section Description
1 Version Go version requirements
2 Core Dependency: lib-commons Required foundation library
3 Frameworks & Libraries Required packages and versions
4 Configuration Loading Environment variable handling
5 Telemetry & Observability OpenTelemetry integration
6 Bootstrap Pattern Application initialization
7 Data Transformation ToEntity/FromEntity patterns
8 Error Codes Convention Service-prefixed error codes
9 Error Handling Error wrapping and checking
10 Function Design Single responsibility principle
11 Pagination Patterns Cursor and page-based pagination
12 Testing Patterns Table-driven tests, edge cases
13 Logging Standards Structured logging with lib-commons
14 Linting golangci-lint configuration
15 Architecture Patterns Hexagonal architecture
16 Directory Structure Project layout
17 Concurrency Patterns Goroutines, channels, errgroup
18 DDD Patterns Entity, Value Object, Aggregate
19 RabbitMQ Worker Pattern Async message processing

Meta-sections (not checked by agents):


Version

  • Minimum: Go 1.24
  • Recommended: Latest stable release

Core Dependency: lib-commons (MANDATORY)

All Lerian Studio Go projects MUST use lib-commons/v2 as the foundation library. This ensures consistency across all services.

Required Import (lib-commons v2)

import (
    libCommons "github.com/LerianStudio/lib-commons/v2/commons"
    libZap "github.com/LerianStudio/lib-commons/v2/commons/zap"           // Logger initialization (config/bootstrap only)
    libLog "github.com/LerianStudio/lib-commons/v2/commons/log"           // Logger interface (services, routes, consumers)
    libOpentelemetry "github.com/LerianStudio/lib-commons/v2/commons/opentelemetry"
    libServer "github.com/LerianStudio/lib-commons/v2/commons/server"
    libHTTP "github.com/LerianStudio/lib-commons/v2/commons/net/http"
    libPostgres "github.com/LerianStudio/lib-commons/v2/commons/postgres"
    libMongo "github.com/LerianStudio/lib-commons/v2/commons/mongo"
    libRedis "github.com/LerianStudio/lib-commons/v2/commons/redis"
)

Note: v2 uses lib prefix aliases (e.g., libCommons, libZap, libLog) to distinguish lib-commons packages from standard library and other imports.

What lib-commons Provides

Package Purpose Where Used
commons Core utilities, config loading, tracking context Everywhere
commons/zap Logger initialization/configuration Config/bootstrap files only
commons/log Logger interface (log.Logger) for logging operations Services, routes, consumers, handlers
commons/postgres PostgreSQL connection management, pagination Bootstrap, repositories
commons/mongo MongoDB connection management Bootstrap, repositories
commons/redis Redis connection management Bootstrap, repositories
commons/opentelemetry OpenTelemetry initialization and helpers Bootstrap, middleware
commons/net/http HTTP utilities, telemetry middleware, pagination Routes, handlers
commons/server Server lifecycle with graceful shutdown Bootstrap

Frameworks & Libraries

Required Versions (Minimum)

Library Minimum Version Purpose
lib-commons v2.0.0 Core infrastructure
fiber/v2 v2.52.0 HTTP framework
pgx/v5 v5.7.0 PostgreSQL driver
go.opentelemetry.io/otel v1.38.0 Telemetry
zap v1.27.0 Logging implementation (internal to lib-commons)
testify v1.10.0 Testing
mockery v2.50.0 Mock generation
mongo-driver v1.17.0 MongoDB driver
go-redis/v9 v9.7.0 Redis client
validator/v10 v10.26.0 Input validation

HTTP Framework

Library Use Case
Fiber v2 Primary choice - High-performance APIs
gRPC-Go Service-to-service communication

Database

Library Use Case
pgx/v5 PostgreSQL (recommended)
sqlc Type-safe SQL queries
GORM ORM (when needed)
go-redis/v9 Redis client
mongo-go-driver MongoDB

Testing

Library Use Case
testify Assertions, mocks
GoMock Interface mocking (check if lib-commons already has a mock)
SQLMock Database mocking
testcontainers-go Integration tests

Configuration Loading (MANDATORY)

All services MUST use libCommons.SetConfigFromEnvVars for configuration loading.

1. Define Configuration Struct

// bootstrap/config.go
package bootstrap

const ApplicationName = "your-service-name"

// Config is the top level configuration struct for the entire application.
type Config struct {
    // Application
    EnvName       string `env:"ENV_NAME"`
    LogLevel      string `env:"LOG_LEVEL"`
    ServerAddress string `env:"SERVER_ADDRESS"`

    // Database - Primary
    PrimaryDBHost     string `env:"DB_HOST"`
    PrimaryDBUser     string `env:"DB_USER"`
    PrimaryDBPassword string `env:"DB_PASSWORD"`
    PrimaryDBName     string `env:"DB_NAME"`
    PrimaryDBPort     string `env:"DB_PORT"`
    PrimaryDBSSLMode  string `env:"DB_SSLMODE"`

    // Database - Replica (for read scaling)
    ReplicaDBHost     string `env:"DB_REPLICA_HOST"`
    ReplicaDBUser     string `env:"DB_REPLICA_USER"`
    ReplicaDBPassword string `env:"DB_REPLICA_PASSWORD"`
    ReplicaDBName     string `env:"DB_REPLICA_NAME"`
    ReplicaDBPort     string `env:"DB_REPLICA_PORT"`
    ReplicaDBSSLMode  string `env:"DB_REPLICA_SSLMODE"`

    // Database - Connection Pool
    MaxOpenConnections int `env:"DB_MAX_OPEN_CONNS"`
    MaxIdleConnections int `env:"DB_MAX_IDLE_CONNS"`

    // MongoDB (if needed)
    MongoDBHost       string `env:"MONGO_HOST"`
    MongoDBName       string `env:"MONGO_NAME"`
    MongoDBUser       string `env:"MONGO_USER"`
    MongoDBPassword   string `env:"MONGO_PASSWORD"`
    MongoDBPort       string `env:"MONGO_PORT"`
    MongoDBParameters string `env:"MONGO_PARAMETERS"`
    MaxPoolSize       int    `env:"MONGO_MAX_POOL_SIZE"`

    // Redis
    RedisHost     string `env:"REDIS_HOST"`
    RedisPassword string `env:"REDIS_PASSWORD"`
    RedisDB       int    `env:"REDIS_DB"`
    RedisPoolSize int    `env:"REDIS_POOL_SIZE"`

    // OpenTelemetry
    OtelServiceName         string `env:"OTEL_RESOURCE_SERVICE_NAME"`
    OtelLibraryName         string `env:"OTEL_LIBRARY_NAME"`
    OtelServiceVersion      string `env:"OTEL_RESOURCE_SERVICE_VERSION"`
    OtelDeploymentEnv       string `env:"OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT"`
    OtelColExporterEndpoint string `env:"OTEL_EXPORTER_OTLP_ENDPOINT"`
    EnableTelemetry         bool   `env:"ENABLE_TELEMETRY"`

    // Auth
    AuthEnabled bool   `env:"PLUGIN_AUTH_ENABLED"`
    AuthHost    string `env:"PLUGIN_AUTH_HOST"`

    // External Services (gRPC)
    ExternalServiceAddress string `env:"EXTERNAL_SERVICE_GRPC_ADDRESS"`
    ExternalServicePort    string `env:"EXTERNAL_SERVICE_GRPC_PORT"`
}

2. Load Configuration

// bootstrap/config.go
func InitServers() *Service {
    cfg := &Config{}

    // Load all environment variables into config struct
    if err := libCommons.SetConfigFromEnvVars(cfg); err != nil {
        panic(err)
    }

    // Validate required fields
    if cfg.PrimaryDBHost == "" || cfg.PrimaryDBName == "" {
        panic("DB_HOST and DB_NAME must be configured")
    }

    // Continue with initialization...
}

Supported Types

Go Type Default Value Example
string "" ServerAddress string \env:"SERVER_ADDRESS"``
bool false EnableTelemetry bool \env:"ENABLE_TELEMETRY"``
int, int8, int16, int32, int64 0 MaxPoolSize int \env:"MONGO_MAX_POOL_SIZE"``

Environment Variable Naming Convention

Category Prefix Example
Application None ENV_NAME, LOG_LEVEL, SERVER_ADDRESS
PostgreSQL DB_ DB_HOST, DB_USER, DB_PASSWORD
PostgreSQL Replica DB_REPLICA_ DB_REPLICA_HOST, DB_REPLICA_USER
MongoDB MONGO_ MONGO_HOST, MONGO_NAME
Redis REDIS_ REDIS_HOST, REDIS_PASSWORD
OpenTelemetry OTEL_ OTEL_RESOURCE_SERVICE_NAME
Auth Plugin PLUGIN_AUTH_ PLUGIN_AUTH_ENABLED, PLUGIN_AUTH_HOST
gRPC Services {SERVICE}_GRPC_ TRANSACTION_GRPC_ADDRESS

What NOT to Do

// FORBIDDEN: Manual os.Getenv calls scattered across code
host := os.Getenv("DB_HOST")  // DON'T do this

// FORBIDDEN: Configuration outside bootstrap
func NewService() *Service {
    dbHost := os.Getenv("DB_HOST")  // DON'T do this
}

// CORRECT: All configuration in Config struct, loaded once in bootstrap
type Config struct {
    PrimaryDBHost string `env:"DB_HOST"`  // Centralized
}

// Load with: libCommons.SetConfigFromEnvVars(&cfg)

Telemetry & Observability (MANDATORY)

All services MUST integrate OpenTelemetry using lib-commons.

Complete Telemetry Flow

┌─────────────────────────────────────────────────────────────────┐
│ 1. BOOTSTRAP (config.go)                                        │
│    telemetry := libOpentelemetry.InitializeTelemetry(&config)   │
│    → Creates OpenTelemetry provider once at startup             │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. ROUTER (routes.go)                                           │
│    tlMid := libHTTP.NewTelemetryMiddleware(tl)                  │
│    f.Use(tlMid.WithTelemetry(tl))      ← Injects into context   │
│    ...routes...                                                  │
│    f.Use(tlMid.EndTracingSpans)        ← Closes root spans      │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. ANY LAYER (handlers, services, repositories)                 │
│    logger, tracer, _, _ := libCommons.NewTrackingFromContext(ctx)│
│    ctx, span := tracer.Start(ctx, "operation_name")             │
│    defer span.End()                                              │
│    logger.Infof("Processing...")   ← Logger from same context   │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. SERVER LIFECYCLE (fiber.server.go)                           │
│    libServer.NewServerManager(nil, &s.telemetry, s.logger)      │
│        .WithHTTPServer(s.app, s.serverAddress)                  │
│        .StartWithGracefulShutdown()                             │
│    → Handles signal trapping + telemetry flush + clean shutdown │
└─────────────────────────────────────────────────────────────────┘

1. Bootstrap Initialization

// bootstrap/config.go
func InitServers() *Service {
    cfg := &Config{}
    if err := libCommons.SetConfigFromEnvVars(cfg); err != nil {
        panic(err)
    }

    // Initialize logger FIRST (zap package for initialization in bootstrap)
    logger := libZap.InitializeLogger()

    // Initialize telemetry with config
    telemetry := libOpentelemetry.InitializeTelemetry(&libOpentelemetry.TelemetryConfig{
        LibraryName:               cfg.OtelLibraryName,
        ServiceName:               cfg.OtelServiceName,
        ServiceVersion:            cfg.OtelServiceVersion,
        DeploymentEnv:             cfg.OtelDeploymentEnv,
        CollectorExporterEndpoint: cfg.OtelColExporterEndpoint,
        EnableTelemetry:           cfg.EnableTelemetry,
        Logger:                    logger,
    })

    // Pass telemetry to router...
}

2. Router Middleware Setup

// adapters/http/in/routes.go
func NewRouter(lg libLog.Logger, tl *libOpentelemetry.Telemetry, ...) *fiber.App {
    f := fiber.New(fiber.Config{
        DisableStartupMessage: true,
        ErrorHandler: func(ctx *fiber.Ctx, err error) error {
            return libHTTP.HandleFiberError(ctx, err)
        },
    })

    // Create telemetry middleware
    tlMid := libHTTP.NewTelemetryMiddleware(tl)

    // MUST be first middleware - injects tracer+logger into context
    f.Use(tlMid.WithTelemetry(tl))
    f.Use(cors.New())
    f.Use(libHTTP.WithHTTPLogging(libHTTP.WithCustomLogger(lg)))

    // ... define routes ...

    // Version endpoint
    f.Get("/version", libHTTP.Version)

    // MUST be last middleware - closes root spans
    f.Use(tlMid.EndTracingSpans)

    return f
}

3. Recovering Logger & Tracer (Any Layer)

// ANY file in ANY layer (handler, service, repository)
func (s *Service) ProcessEntity(ctx context.Context, id string) error {
    // Single call recovers BOTH logger AND tracer from context
    logger, tracer, _, _ := libCommons.NewTrackingFromContext(ctx)

    // Create child span for this operation
    ctx, span := tracer.Start(ctx, "service.process_entity")
    defer span.End()

    // Logger is automatically correlated with trace
    logger.Infof("Processing entity: %s", id)

    // Pass ctx to downstream calls - trace propagates automatically
    return s.repo.Update(ctx, id)
}

4. Error Handling with Spans

// For technical errors (unexpected failures)
if err != nil {
    libOpentelemetry.HandleSpanError(&span, "Failed to connect database", err)
    logger.Errorf("Database error: %v", err)
    return nil, err
}

// For business errors (expected validation failures)
if err != nil {
    libOpentelemetry.HandleSpanBusinessErrorEvent(&span, "Validation failed", err)
    logger.Warnf("Validation error: %v", err)
    return nil, err
}

5. Server Lifecycle with Graceful Shutdown

// bootstrap/fiber.server.go
type Server struct {
    app           *fiber.App
    serverAddress string
    logger        libLog.Logger
    telemetry     libOpentelemetry.Telemetry
}

func (s *Server) Run(l *libCommons.Launcher) error {
    libServer.NewServerManager(nil, &s.telemetry, s.logger).
        WithHTTPServer(s.app, s.serverAddress).
        StartWithGracefulShutdown()  // Handles: SIGINT/SIGTERM, telemetry flush, connections close
    return nil
}

Required Environment Variables

Variable Description Example
OTEL_RESOURCE_SERVICE_NAME Service name in traces service-name
OTEL_LIBRARY_NAME Library identifier service-name
OTEL_RESOURCE_SERVICE_VERSION Service version 1.0.0
OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT Environment production
OTEL_EXPORTER_OTLP_ENDPOINT Collector endpoint http://otel-collector:4317
ENABLE_TELEMETRY Enable/disable true

Bootstrap Pattern (MANDATORY)

All services MUST follow the bootstrap pattern for initialization.

Directory Structure

/internal
  /bootstrap
    config.go          # Config struct + InitServers()
    fiber.server.go    # HTTP server with graceful shutdown
    grpc.server.go     # gRPC server (if needed)
    service.go         # Service struct wrapping servers

Complete Bootstrap Example

// bootstrap/config.go
package bootstrap

const ApplicationName = "your-service"

type Config struct {
    // ... config fields with env tags
}

func InitServers() *Service {
    // 1. Load configuration
    cfg := &Config{}
    if err := libCommons.SetConfigFromEnvVars(cfg); err != nil {
        panic(err)
    }

    // 2. Initialize logger (zap package for initialization in bootstrap)
    logger := libZap.InitializeLogger()

    // 3. Initialize telemetry
    telemetry := libOpentelemetry.InitializeTelemetry(&libOpentelemetry.TelemetryConfig{...})

    // 4. Initialize database connections
    postgresConnection := &libPostgres.PostgresConnection{...}
    mongoConnection := &libMongo.MongoConnection{...}
    redisConnection := &libRedis.RedisConnection{...}

    // 5. Initialize repositories (adapters)
    userRepo := postgresadapter.NewUserRepository(postgresConnection)
    cacheRepo := redisadapter.NewCacheRepository(redisConnection)

    // 6. Initialize use cases (services)
    commandUseCase := &command.UseCase{
        UserRepo:  userRepo,
        CacheRepo: cacheRepo,
    }
    queryUseCase := &query.UseCase{
        UserRepo: userRepo,
    }

    // 7. Initialize handlers
    userHandler := &httpin.UserHandler{
        Command: commandUseCase,
        Query:   queryUseCase,
    }

    // 8. Initialize router with middleware
    httpApp := httpin.NewRouter(logger, telemetry, userHandler)

    // 9. Create server
    serverAPI := NewServer(cfg, httpApp, logger, telemetry)

    return &Service{
        Server: serverAPI,
        Logger: logger,
    }
}

Data Transformation: ToEntity/FromEntity (MANDATORY)

All database models MUST implement transformation methods to/from domain entities.

Pattern

// internal/adapters/postgres/user/user.postgresql.go

// UserPostgreSQLModel is the database representation
type UserPostgreSQLModel struct {
    ID        string         `db:"id"`
    Email     string         `db:"email"`
    Name      string         `db:"name"`
    Status    string         `db:"status"`
    CreatedAt time.Time      `db:"created_at"`
    UpdatedAt time.Time      `db:"updated_at"`
    DeletedAt sql.NullTime   `db:"deleted_at"`
}

// ToEntity converts database model to domain entity
func (m *UserPostgreSQLModel) ToEntity() *domain.User {
    var deletedAt *time.Time
    if m.DeletedAt.Valid {
        deletedAt = &m.DeletedAt.Time
    }

    return &domain.User{
        ID:        domain.UserID(m.ID),
        Email:     domain.Email(m.Email),
        Name:      m.Name,
        Status:    domain.UserStatus(m.Status),
        CreatedAt: m.CreatedAt,
        UpdatedAt: m.UpdatedAt,
        DeletedAt: deletedAt,
    }
}

// FromEntity converts domain entity to database model
func (m *UserPostgreSQLModel) FromEntity(u *domain.User) {
    m.ID = string(u.ID)
    m.Email = string(u.Email)
    m.Name = u.Name
    m.Status = string(u.Status)
    m.CreatedAt = u.CreatedAt
    m.UpdatedAt = u.UpdatedAt
    if u.DeletedAt != nil {
        m.DeletedAt = sql.NullTime{Time: *u.DeletedAt, Valid: true}
    }
}

Why This Matters

  • Layer isolation: Domain doesn't know about database concerns
  • Testability: Domain entities can be tested without database
  • Flexibility: Database schema can change without affecting domain
  • Type safety: Explicit conversions prevent accidental mixing

Error Codes Convention (MANDATORY)

Each service MUST define error codes with a service-specific prefix.

Service Prefixes

Service Prefix Example
Midaz MDZ MDZ-0001
Plugin-Fees FEE FEE-0001
Plugin-Auth AUT AUT-0001
Platform PLT PLT-0001

Error Code Structure

// pkg/constant/errors.go
package constant

const (
    ErrCodeInvalidInput     = "PLT-0001"
    ErrCodeNotFound         = "PLT-0002"
    ErrCodeUnauthorized     = "PLT-0003"
    ErrCodeForbidden        = "PLT-0004"
    ErrCodeConflict         = "PLT-0005"
    ErrCodeInternalError    = "PLT-0006"
    ErrCodeValidationFailed = "PLT-0007"
)

// Error definitions with messages
var (
    ErrInvalidInput = &BusinessError{
        Code:    ErrCodeInvalidInput,
        Message: "Invalid input provided",
    }
    ErrNotFound = &BusinessError{
        Code:    ErrCodeNotFound,
        Message: "Resource not found",
    }
)

Business Error Type

// pkg/errors.go
type BusinessError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    Details any    `json:"details,omitempty"`
}

func (e *BusinessError) Error() string {
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

func ValidateBusinessError(err *BusinessError, entityType string, args ...any) error {
    // Format error with entity context
    return &BusinessError{
        Code:    err.Code,
        Message: fmt.Sprintf(err.Message, args...),
        Details: map[string]string{"entity": entityType},
    }
}

Error Handling

Rules

// ALWAYS check errors
if err != nil {
    return fmt.Errorf("context: %w", err)
}

// ALWAYS wrap errors with context
if err != nil {
    return fmt.Errorf("failed to create user %s: %w", userID, err)
}

// Use custom error types for domain errors
var ErrUserNotFound = errors.New("user not found")

// Check specific errors with errors.Is
if errors.Is(err, ErrUserNotFound) {
    return nil, status.Error(codes.NotFound, "user not found")
}

Forbidden

// NEVER use panic for business logic
panic(err) // FORBIDDEN

// NEVER ignore errors
result, _ := doSomething() // FORBIDDEN

// NEVER return nil error without checking
return nil, nil // SUSPICIOUS - check if error is possible

Function Design (MANDATORY)

Single Responsibility Principle (SRP): Each function MUST have exactly ONE responsibility.

Rules

Rule Description
One responsibility per function A function should do ONE thing and do it well
Max 20-30 lines If longer, break into smaller functions
One level of abstraction Don't mix high-level and low-level operations
Descriptive names Function name should describe its single responsibility

Examples

// ❌ BAD - Multiple responsibilities
func ProcessOrder(order Order) error {
    // Validate order
    if order.Items == nil {
        return errors.New("no items")
    }
    // Calculate total
    total := 0.0
    for _, item := range order.Items {
        total += item.Price * float64(item.Quantity)
    }
    // Apply discount
    if order.CouponCode != "" {
        total = total * 0.9
    }
    // Save to database
    db.Save(&order)
    // Send email
    sendEmail(order.CustomerEmail, "Order confirmed")
    return nil
}

// ✅ GOOD - Single responsibility per function
func ProcessOrder(order Order) error {
    if err := validateOrder(order); err != nil {
        return err
    }
    total := calculateTotal(order.Items)
    total = applyDiscount(total, order.CouponCode)
    if err := saveOrder(order, total); err != nil {
        return err
    }
    return notifyCustomer(order.CustomerEmail)
}

func validateOrder(order Order) error {
    if order.Items == nil || len(order.Items) == 0 {
        return errors.New("order must have items")
    }
    return nil
}

func calculateTotal(items []Item) float64 {
    total := 0.0
    for _, item := range items {
        total += item.Price * float64(item.Quantity)
    }
    return total
}

func applyDiscount(total float64, couponCode string) float64 {
    if couponCode != "" {
        return total * 0.9
    }
    return total
}

Signs a Function Has Multiple Responsibilities

Sign Action
Multiple // section comments Split at comment boundaries
"and" in function name Split into separate functions
More than 3 parameters Consider parameter object or splitting
Nested conditionals > 2 levels Extract inner logic to functions
Function does validation AND processing Separate validation function

Pagination Patterns

Lerian Studio supports multiple pagination patterns. This section provides implementation details for each pattern.

Note

: The pagination strategy should be decided during the TRD (Technical Requirements Document) phase, not during implementation. See the pre-dev-trd-creation skill for the decision workflow. If no TRD exists, consult with the user before implementing.

Quick Reference

Pattern Best For Query Params Response Fields
Cursor-Based High-volume data, real-time cursor, limit, sort_order next_cursor, prev_cursor
Page-Based Low-volume data page, limit, sort_order page, limit
Page-Based + Total UI needs "Page X of Y" page, limit, sort_order page, limit, total

Decision Guide (Reference Only)

Is this a high-volume entity (>10k records typical)?
├── YES → Use Cursor-Based Pagination
└── NO  → Use Page-Based Pagination

Does the user need to jump to arbitrary pages?
├── YES → Use Page-Based Pagination
└── NO  → Cursor-Based is fine

Does the UI need to show total count (e.g., "Page 1 of 10")?
├── YES → Use Page-Based with Total Count
└── NO  → Standard Page-Based is sufficient

Pattern 1: Cursor-Based Pagination (PREFERRED for high-volume)

Use for: Transactions, Operations, Balances, Audit logs, Events

Query Parameters:

Parameter Type Default Description
cursor string (none) Base64-encoded cursor from previous response
limit int 10 Items per page (max: 100)
sort_order string "asc" Sort direction: "asc" or "desc"
start_date datetime (calculated) Filter start date
end_date datetime now Filter end date

Response Structure:

{
  "items": [...],
  "limit": 10,
  "next_cursor": "eyJpZCI6IjEyMzQ1Njc4Li4uIiwicG9pbnRzX25leHQiOnRydWV9",
  "prev_cursor": "eyJpZCI6IjEyMzQ1Njc4Li4uIiwicG9pbnRzX25leHQiOmZhbHNlfQ=="
}

Handler Implementation:

func (h *Handler) GetAllTransactions(c *fiber.Ctx) error {
    ctx := c.UserContext()
    logger, tracer, _, _ := libCommons.NewTrackingFromContext(ctx)

    ctx, span := tracer.Start(ctx, "handler.get_all_transactions")
    defer span.End()

    // Parse and validate query parameters
    headerParams, err := libHTTP.ValidateParameters(c.Queries())
    if err != nil {
        libOpentelemetry.HandleSpanBusinessErrorEvent(&span, "Invalid parameters", err)
        return libHTTP.WithError(c, err)
    }

    // Build pagination request (cursor-based)
    pagination := libPostgres.Pagination{
        Limit:     headerParams.Limit,
        SortOrder: headerParams.SortOrder,
        StartDate: headerParams.StartDate,
        EndDate:   headerParams.EndDate,
    }

    // Query with cursor pagination
    items, cursor, err := h.Query.GetAllTransactions(ctx, orgID, ledgerID, *headerParams)
    if err != nil {
        libOpentelemetry.HandleSpanBusinessErrorEvent(&span, "Query failed", err)
        return libHTTP.WithError(c, err)
    }

    // Set response with cursor
    pagination.SetItems(items)
    pagination.SetCursor(cursor.Next, cursor.Prev)

    return libHTTP.OK(c, pagination)
}

Repository Implementation:

func (r *Repository) FindAll(ctx context.Context, filter libHTTP.Pagination) ([]Entity, libHTTP.CursorPagination, error) {
    logger, tracer, _, _ := libCommons.NewTrackingFromContext(ctx)

    ctx, span := tracer.Start(ctx, "postgres.find_all")
    defer span.End()

    // Decode cursor if provided
    var decodedCursor libHTTP.Cursor
    isFirstPage := true

    if filter.Cursor != "" {
        isFirstPage = false
        decodedCursor, _ = libHTTP.DecodeCursor(filter.Cursor)
    }

    // Build query with cursor pagination
    query := squirrel.Select("*").From("table_name")
    query, orderUsed := libHTTP.ApplyCursorPagination(
        query,
        decodedCursor,
        strings.ToUpper(filter.SortOrder),
        filter.Limit,
    )

    // Execute query...
    rows, err := query.RunWith(db).QueryContext(ctx)
    // ... scan rows into items ...

    // Check if there are more items
    hasPagination := len(items) > filter.Limit

    // Paginate records (trim to limit, handle direction)
    items = libHTTP.PaginateRecords(
        isFirstPage,
        hasPagination,
        decodedCursor.PointsNext || isFirstPage,
        items,
        filter.Limit,
        orderUsed,
    )

    // Calculate cursors for response
    var firstID, lastID string
    if len(items) > 0 {
        firstID = items[0].ID
        lastID = items[len(items)-1].ID
    }

    cursor, _ := libHTTP.CalculateCursor(
        isFirstPage,
        hasPagination,
        decodedCursor.PointsNext || isFirstPage,
        firstID,
        lastID,
    )

    return items, cursor, nil
}

Pattern 2: Page-Based (Offset) Pagination

Use for: Organizations, Ledgers, Assets, Portfolios, Accounts

Query Parameters:

Parameter Type Default Description
page int 1 Page number (1-indexed)
limit int 10 Items per page (max: 100)
sort_order string "asc" Sort direction
start_date datetime (calculated) Filter start date
end_date datetime now Filter end date

Response Structure:

{
  "items": [...],
  "page": 1,
  "limit": 10
}

Handler Implementation:

func (h *Handler) GetAllOrganizations(c *fiber.Ctx) error {
    ctx := c.UserContext()
    logger, tracer, _, _ := libCommons.NewTrackingFromContext(ctx)

    ctx, span := tracer.Start(ctx, "handler.get_all_organizations")
    defer span.End()

    headerParams, err := libHTTP.ValidateParameters(c.Queries())
    if err != nil {
        return libHTTP.WithError(c, err)
    }

    // Build page-based pagination
    pagination := libPostgres.Pagination{
        Limit:     headerParams.Limit,
        Page:      headerParams.Page,
        SortOrder: headerParams.SortOrder,
        StartDate: headerParams.StartDate,
        EndDate:   headerParams.EndDate,
    }

    // Query with offset pagination (uses ToOffsetPagination())
    items, err := h.Query.GetAllOrganizations(ctx, headerParams.ToOffsetPagination())
    if err != nil {
        return libHTTP.WithError(c, err)
    }

    pagination.SetItems(items)

    return libHTTP.OK(c, pagination)
}

Repository Implementation:

func (r *Repository) FindAll(ctx context.Context, pagination http.Pagination) ([]Entity, error) {
    offset := (pagination.Page - 1) * pagination.Limit

    query := squirrel.Select("*").
        From("table_name").
        OrderBy("id " + pagination.SortOrder).
        Limit(uint64(pagination.Limit)).
        Offset(uint64(offset))

    // Execute query...
    return items, nil
}

Pattern 3: Page-Based with Total Count

Use when: Client needs total count for pagination UI (showing "Page 1 of 10")

Response Structure:

{
  "items": [...],
  "page": 1,
  "limit": 10,
  "total": 100
}

Note: Adds a COUNT query overhead. Only use if total is required.


Shared Utilities from lib-commons

Utility Package Purpose
Pagination struct lib-commons/commons/postgres Unified response structure
Cursor struct lib-commons/commons/net/http Cursor encoding
DecodeCursor lib-commons/commons/net/http Parse cursor from request
ApplyCursorPagination lib-commons/commons/net/http Add cursor to SQL query
PaginateRecords lib-commons/commons/net/http Trim results, handle direction
CalculateCursor lib-commons/commons/net/http Generate next/prev cursors

Environment Variables

Variable Default Description
MAX_PAGINATION_LIMIT 100 Maximum allowed limit per request
MAX_PAGINATION_MONTH_DATE_RANGE 1 Default date range in months

Testing Patterns

Table-Driven Tests (MANDATORY)

func TestCreateUser(t *testing.T) {
    tests := []struct {
        name    string
        input   CreateUserInput
        want    *User
        wantErr error
    }{
        {
            name:  "valid user",
            input: CreateUserInput{Name: "John", Email: "john@example.com"},
            want:  &User{Name: "John", Email: "john@example.com"},
        },
        {
            name:    "invalid email",
            input:   CreateUserInput{Name: "John", Email: "invalid"},
            wantErr: ErrInvalidEmail,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := CreateUser(tt.input)

            if tt.wantErr != nil {
                require.ErrorIs(t, err, tt.wantErr)
                return
            }

            require.NoError(t, err)
            assert.Equal(t, tt.want.Name, got.Name)
        })
    }
}

Test Naming Convention

Test{Unit}_{Scenario}_{ExpectedResult}

Examples:
- TestOrderService_CreateOrder_WithValidItems_ReturnsOrder
- TestOrderService_CreateOrder_WithEmptyItems_ReturnsError
- TestMoney_Add_SameCurrency_ReturnsSum

Edge Case Coverage (MANDATORY)

Every acceptance criterion MUST have edge case tests beyond the happy path.

AC Type Required Edge Cases Minimum Count
Input validation nil, empty string, boundary values, invalid format, special chars, max length 3+
CRUD operations not found, duplicate key, concurrent modification, large payload 3+
Business logic zero value, negative numbers, overflow, boundary conditions, invalid state 3+
Error handling context timeout, connection refused, invalid response, retry exhausted 2+
Authentication expired token, invalid signature, missing claims, revoked token 2+

Table-Driven Edge Cases Pattern:

func TestUserService_CreateUser(t *testing.T) {
    tests := []struct {
        name    string
        input   CreateUserInput
        wantErr error
    }{
        // Happy path
        {name: "valid user", input: validInput(), wantErr: nil},
        
        // Edge cases (MANDATORY - minimum 3)
        {name: "nil input", input: CreateUserInput{}, wantErr: ErrInvalidInput},
        {name: "empty email", input: CreateUserInput{Name: "John", Email: ""}, wantErr: ErrEmailRequired},
        {name: "invalid email format", input: CreateUserInput{Name: "John", Email: "invalid"}, wantErr: ErrInvalidEmail},
        {name: "email too long", input: CreateUserInput{Name: "John", Email: strings.Repeat("a", 256) + "@test.com"}, wantErr: ErrEmailTooLong},
        {name: "name with special chars", input: CreateUserInput{Name: "<script>", Email: "test@test.com"}, wantErr: ErrInvalidName},
    }
    // ... test execution
}

Anti-Pattern (FORBIDDEN):

// ❌ WRONG: Only happy path
func TestUserService_CreateUser(t *testing.T) {
    result, err := service.CreateUser(validInput())
    require.NoError(t, err)  // No edge cases = incomplete test
}

Mock Generation

// Using mockery
//go:generate mockery --name=OrderRepository --output=mocks --outpkg=mocks

// Using GoMock
//go:generate mockgen -source=repository.go -destination=mocks/mock_repository.go -package=mocks

Logging Standards

Using lib-commons Logger

// Recover logger from context (PREFERRED)
logger, tracer, _, _ := libCommons.NewTrackingFromContext(ctx)

// Log with context correlation
logger.Infof("Processing entity: %s", entityID)
logger.Warnf("Rate limit approaching: %d/%d", current, limit)
logger.Errorf("Failed to save entity: %v", err)

What NOT to Log

// FORBIDDEN - sensitive data
logger.Info("user login", "password", password)  // NEVER
logger.Info("payment", "card_number", card)      // NEVER
logger.Info("auth", "token", token)              // NEVER
logger.Info("user", "cpf", cpf)                  // NEVER (PII)

Linting

golangci-lint Configuration

# .golangci.yml
linters:
  enable:
    - errcheck      # Check error handling
    - govet         # Go vet
    - staticcheck   # Static analysis
    - gosimple      # Simplify code
    - ineffassign   # Unused assignments
    - unused        # Unused code
    - gofmt         # Formatting
    - goimports     # Import ordering
    - misspell      # Spelling
    - goconst       # Repeated strings
    - gosec         # Security issues
    - nilerr        # Return nil with non-nil error

Format Commands

# Format code
gofmt -w .
goimports -w .

# Run linter
golangci-lint run ./...

Architecture Patterns

Hexagonal Architecture (Ports & Adapters)

/internal
  /bootstrap         # Application initialization
    config.go
    fiber.server.go
  /domain            # Business entities (no dependencies)
    user.go
    errors.go
  /services          # Application/Business logic
    /command         # Write operations
    /query           # Read operations
  /adapters          # Implementations (adapters)
    /http/in         # HTTP handlers + routes
    /grpc/in         # gRPC handlers
    /postgres        # PostgreSQL repositories
    /mongodb         # MongoDB repositories
    /redis           # Redis repositories

Interface-Based Abstractions

// Define interface in the package that USES it (not implements)
// /internal/services/command/usecase.go

type UserRepository interface {
    FindByID(ctx context.Context, id uuid.UUID) (*domain.User, error)
    Save(ctx context.Context, user *domain.User) error
}

type UseCase struct {
    UserRepo UserRepository  // Depend on interface
}

Directory Structure

/cmd
  /app                   # Main application entry
    main.go
/internal
  /bootstrap             # Initialization (config, servers)
    config.go
    fiber.server.go
    service.go
  /domain                # Business entities
  /services              # Business logic
    /command             # Write operations (use cases)
    /query               # Read operations (use cases)
  /adapters              # Infrastructure implementations
    /http/in             # HTTP handlers + routes
    /grpc/in             # gRPC handlers
    /grpc/out            # gRPC clients
    /postgres            # PostgreSQL repositories
    /mongodb             # MongoDB repositories
    /redis               # Redis repositories
/pkg
  /constant              # Constants and error codes
  /mmodel                # Shared models
  /net/http              # HTTP utilities
/api                     # OpenAPI/Swagger specs
/migrations              # Database migrations

Concurrency Patterns

Goroutines with Context

func processItems(ctx context.Context, items []Item) error {
    g, ctx := errgroup.WithContext(ctx)

    for _, item := range items {
        item := item // capture variable
        g.Go(func() error {
            select {
            case <-ctx.Done():
                return ctx.Err()
            default:
                return processItem(ctx, item)
            }
        })
    }

    return g.Wait()
}

Channel Patterns

// Worker pool
func workerPool(ctx context.Context, jobs <-chan Job, results chan<- Result) {
    for {
        select {
        case <-ctx.Done():
            return
        case job, ok := <-jobs:
            if !ok {
                return
            }
            results <- process(job)
        }
    }
}

DDD Patterns (Go Implementation)

DDD patterns are MANDATORY for all Go services.

Entity

type User struct {
    ID        UserID    // Value object for identity
    Email     Email     // Value object
    Name      string
    CreatedAt time.Time
    UpdatedAt time.Time
}

func (u User) Equals(other User) bool {
    return u.ID == other.ID
}

Value Object

type Money struct {
    amount   int64  // cents to avoid float issues
    currency string
}

func NewMoney(amount int64, currency string) (Money, error) {
    if currency == "" {
        return Money{}, errors.New("currency is required")
    }
    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
}

Aggregate Root

type Order struct {
    ID         OrderID
    CustomerID CustomerID
    Items      []OrderItem
    Status     OrderStatus
    events     []DomainEvent
}

func (o *Order) AddItem(product Product, quantity int) error {
    if o.Status != OrderStatusDraft {
        return ErrOrderNotModifiable
    }

    o.Items = append(o.Items, OrderItem{...})
    o.events = append(o.events, OrderItemAdded{...})
    return nil
}

func (o *Order) PullEvents() []DomainEvent {
    events := o.events
    o.events = nil
    return events
}

RabbitMQ Worker Pattern

When the application includes async processing (API+Worker or Worker Only), follow this pattern.

Application Types

Type Characteristics Components
API Only HTTP endpoints, no async processing Handlers, Services, Repositories
API + Worker HTTP endpoints + async message processing All above + Consumers, Producers
Worker Only No HTTP, only message processing Consumers, Services, Repositories

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│  Service Bootstrap                                          │
│  ├── HTTP Server (Fiber)         ← API endpoints           │
│  ├── RabbitMQ Consumer           ← Event-driven workers    │
│  └── Redis Consumer (optional)   ← Scheduled polling       │
└─────────────────────────────────────────────────────────────┘

Core Components

// ConsumerRoutes - Multi-queue consumer manager
type ConsumerRoutes struct {
    conn              *RabbitMQConnection
    routes            map[string]QueueHandlerFunc  // Queue name → Handler
    NumbersOfWorkers  int                          // Workers per queue (default: 5)
    NumbersOfPrefetch int                          // QoS prefetch (default: 10)
    Logger
    Telemetry
}

// Handler function signature
type QueueHandlerFunc func(ctx context.Context, body []byte) error

Worker Configuration

Config Default Purpose
RABBITMQ_NUMBERS_OF_WORKERS 5 Concurrent workers per queue
RABBITMQ_NUMBERS_OF_PREFETCH 10 Messages buffered per worker
RABBITMQ_CONSUMER_USER - Separate credentials for consumer
RABBITMQ_{QUEUE}_QUEUE - Queue name per handler

Formula: Total buffered = Workers × Prefetch (e.g., 5 × 10 = 50 messages)

Handler Registration

// Register handlers per queue
func (mq *MultiQueueConsumer) RegisterRoutes(routes *ConsumerRoutes) {
    routes.Register(os.Getenv("RABBITMQ_BALANCE_CREATE_QUEUE"), mq.handleBalanceCreate)
    routes.Register(os.Getenv("RABBITMQ_TRANSACTION_QUEUE"), mq.handleTransaction)
}

Handler Implementation

func (mq *MultiQueueConsumer) handleBalanceCreate(ctx context.Context, body []byte) error {
    // 1. Deserialize message
    var message QueueMessage
    if err := json.Unmarshal(body, &message); err != nil {
        return fmt.Errorf("unmarshal message: %w", err)
    }

    // 2. Execute business logic
    if err := mq.UseCase.CreateBalance(ctx, message); err != nil {
        return fmt.Errorf("create balance: %w", err)
    }

    // 3. Success → Ack automatically
    return nil
}

Message Acknowledgment

Result Action Effect
return nil msg.Ack(false) Message removed from queue
return err msg.Nack(false, true) Message requeued

Worker Lifecycle

RunConsumers()
├── For each registered queue:
│   ├── EnsureChannel() with exponential backoff
│   ├── Set QoS (prefetch)
│   ├── Start Consume()
│   └── Spawn N worker goroutines
│       └── startWorker(workerID, queue, handler, messages)

startWorker():
├── for msg := range messages:
│   ├── Extract/generate TraceID from headers
│   ├── Create context with HeaderID
│   ├── Start OpenTelemetry span
│   ├── Call handler(ctx, msg.Body)
│   ├── On success: msg.Ack(false)
│   └── On error: log + msg.Nack(false, true)

Exponential Backoff with Jitter

const (
    MaxRetries     = 5
    InitialBackoff = 500 * time.Millisecond
    MaxBackoff     = 10 * time.Second
    BackoffFactor  = 2.0
)

// Full jitter: random delay in [0, baseDelay]
func FullJitter(baseDelay time.Duration) time.Duration {
    jitter := time.Duration(rand.Float64() * float64(baseDelay))
    if jitter > MaxBackoff {
        return MaxBackoff
    }
    return jitter
}

Producer Implementation

func (p *ProducerRepository) Publish(ctx context.Context, exchange, routingKey string, message []byte) error {
    if err := p.EnsureChannel(); err != nil {
        return fmt.Errorf("ensure channel: %w", err)
    }

    headers := amqp.Table{
        "HeaderID": GetRequestID(ctx),
    }
    InjectTraceHeaders(ctx, &headers)

    return p.channel.Publish(
        exchange,
        routingKey,
        false,
        false,
        amqp.Publishing{
            ContentType:  "application/json",
            DeliveryMode: amqp.Persistent,
            Headers:      headers,
            Body:         message,
        },
    )
}

Message Format

type QueueMessage struct {
    OrganizationID uuid.UUID   `json:"organization_id"`
    LedgerID       uuid.UUID   `json:"ledger_id"`
    AuditID        uuid.UUID   `json:"audit_id"`
    Data           []QueueData `json:"data"`
}

type QueueData struct {
    ID    uuid.UUID       `json:"id"`
    Value json.RawMessage `json:"value"`
}

Service Bootstrap (API + Worker)

type Service struct {
    *Server              // HTTP server (Fiber)
    *MultiQueueConsumer  // RabbitMQ consumer
    Logger
}

func (s *Service) Run() {
    launcher := libCommons.NewLauncher(
        libCommons.WithLogger(s.Logger),
        libCommons.RunApp("HTTP Server", s.Server),
        libCommons.RunApp("RabbitMQ Consumer", s.MultiQueueConsumer),
    )
    launcher.Run() // All components run concurrently
}

Directory Structure for Workers

/internal
  /adapters
    /rabbitmq
      consumer.go      # ConsumerRoutes, worker pool
      producer.go      # ProducerRepository
      connection.go    # Connection management
  /bootstrap
    rabbitmq.server.go # MultiQueueConsumer, handler registration
    service.go         # Service orchestration
/pkg
  /utils
    jitter.go          # Backoff utilities

Worker Checklist

  • Handlers are idempotent (safe to process duplicates)
  • Manual Ack enabled (autoAck: false)
  • Error handling returns error (triggers Nack)
  • Context propagation with HeaderID
  • OpenTelemetry spans for tracing
  • Exponential backoff for connection recovery
  • Graceful shutdown respects context cancellation
  • Separate credentials for consumer vs producer

Standards Compliance Output Format

When producing a Standards Compliance report (used by dev-refactor workflow), follow these output formats:

If ALL Categories Are Compliant

## Standards Compliance

### Lerian/Ring Standards Comparison

#### Bootstrap & Initialization
| Category | Current Pattern | Expected Pattern | Status | Evidence |
|----------|----------------|------------------|--------|----------|
| Config Struct | `Config` struct with `env` tags | Single struct with `env` tags | ✅ Compliant | `internal/bootstrap/config.go:15` |
| Config Loading | `libCommons.SetConfigFromEnvVars(&cfg)` | `libCommons.SetConfigFromEnvVars(&cfg)` | ✅ Compliant | `internal/bootstrap/config.go:42` |
| Logger Init | `libZap.InitializeLogger()` | `libZap.InitializeLogger()` (bootstrap only) | ✅ Compliant | `internal/bootstrap/config.go:45` |
| Telemetry Init | `libOpentelemetry.InitializeTelemetry()` | `libOpentelemetry.InitializeTelemetry()` | ✅ Compliant | `internal/bootstrap/config.go:48` |
| ... | ... | ... | ✅ Compliant | ... |

#### Context & Tracking
| Category | Current Pattern | Expected Pattern | Status | Evidence |
|----------|----------------|------------------|--------|----------|
| ... | ... | ... | ✅ Compliant | ... |

#### Infrastructure
| Category | Current Pattern | Expected Pattern | Status | Evidence |
|----------|----------------|------------------|--------|----------|
| ... | ... | ... | ✅ Compliant | ... |

#### Domain Patterns
| Category | Current Pattern | Expected Pattern | Status | Evidence |
|----------|----------------|------------------|--------|----------|
| ... | ... | ... | ✅ Compliant | ... |

### Verdict: ✅ FULLY COMPLIANT

No migration actions required. All categories verified against Lerian/Ring Go Standards.

If ANY Category Is Non-Compliant

## Standards Compliance

### Lerian/Ring Standards Comparison

#### Bootstrap & Initialization
| Category | Current Pattern | Expected Pattern | Status | File/Location |
|----------|----------------|------------------|--------|---------------|
| Config Struct | Scattered `os.Getenv()` calls | Single struct with `env` tags | ⚠️ Non-Compliant | `cmd/api/main.go` |
| Config Loading | Manual env parsing | `libCommons.SetConfigFromEnvVars(&cfg)` | ⚠️ Non-Compliant | `cmd/api/main.go:25` |
| Logger Init | `libZap.InitializeLogger()` | `libZap.InitializeLogger()` (bootstrap only) | ✅ Compliant | `cmd/api/main.go:30` |
| ... | ... | ... | ... | ... |

#### Context & Tracking
| Category | Current Pattern | Expected Pattern | Status | File/Location |
|----------|----------------|------------------|--------|---------------|
| ... | ... | ... | ... | ... |

### Verdict: ⚠️ NON-COMPLIANT (X of Y categories)

### Required Changes for Compliance

1. **Config Struct Migration**
   - Replace: Direct `os.Getenv()` calls scattered across files
   - With: Single `Config` struct with `env` tags in `/internal/bootstrap/config.go`
   - Import: `libCommons "github.com/LerianStudio/lib-commons/v2/commons"`
   - Usage: `libCommons.SetConfigFromEnvVars(&cfg)`
   - Files affected: `cmd/api/main.go`, `internal/service/user.go`

2. **Logger Migration**
   - Replace: Custom logger or `log.Println()`
   - With: lib-commons structured logger
   - Bootstrap import: `libZap "github.com/LerianStudio/lib-commons/v2/commons/zap"` (initialization)
   - Application import: `libLog "github.com/LerianStudio/lib-commons/v2/commons/log"` (interface for logging calls)
   - Bootstrap usage: `logger := libZap.InitializeLogger()` (returns `libLog.Logger` interface)
   - Application usage: Use `libLog.Logger` interface for all logging calls
   - Files affected: [list files]

3. **Telemetry Migration**
   - Replace: No tracing or custom tracing
   - With: OpenTelemetry integration
   - Import: `libOpentelemetry "github.com/LerianStudio/lib-commons/v2/commons/opentelemetry"`
   - Usage: `telemetry := libOpentelemetry.InitializeTelemetry(&libOpentelemetry.TelemetryConfig{...})`
   - Files affected: [list files]

4. **[Next Category] Migration**
   - Replace: ...
   - With: ...
   - Import: ...
   - Usage: ...

CRITICAL: The comparison table is NOT optional. It serves as:

  1. Evidence that each category was actually checked
  2. Documentation for the codebase's compliance status
  3. Audit trail for future refactors

Checklist

Before submitting Go code, verify:

  • Using lib-commons v2 for infrastructure
  • Configuration loaded via SetConfigFromEnvVars
  • Telemetry initialized and middleware configured
  • Logger/tracer recovered from context via NewTrackingFromContext
  • All errors are checked and wrapped with context
  • Error codes use service prefix (e.g., PLT-0001)
  • No panic() outside of main.go or InitServers
  • Tests use table-driven pattern
  • Database models have ToEntity/FromEntity methods
  • Interfaces defined where they're used
  • No global mutable state
  • Context propagated through all calls
  • Sensitive data not logged
  • golangci-lint passes
  • Pagination strategy defined in TRD (or confirmed with user if no TRD)