- 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
54 KiB
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.mdfor 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):
- Standards Compliance Output Format - Report format for dev-refactor
- Checklist - Self-verification before submitting code
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
libprefix 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-creationskill 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:
- Evidence that each category was actually checked
- Documentation for the codebase's compliance status
- 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 ofmain.goorInitServers - 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)