go-project-template

module
v1.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 24, 2026 License: MIT

README

Go Project Template

A production-ready Go project template following Clean Architecture and Domain-Driven Design principles, optimized for building scalable applications with PostgreSQL or MySQL.

Features

  • Modular Domain Architecture - Domain-based code organization for scalability
  • Clean Architecture - Separation of concerns with domain, repository, use case, and presentation layers
  • Standardized Error Handling - Domain errors with proper HTTP status code mapping
  • Dependency Injection Container - Centralized component wiring with lazy initialization and clean resource management
  • Multiple Database Support - PostgreSQL and MySQL with dedicated repository implementations using database/sql
  • Database Migrations - Separate migrations for PostgreSQL and MySQL using golang-migrate
  • UUIDv7 Primary Keys - Time-ordered, sortable UUIDs for globally unique identifiers
  • Transaction Management - TxManager interface for handling database transactions
  • Transactional Outbox Pattern - Event-driven architecture with guaranteed delivery
  • HTTP Server - Standard library HTTP server with middleware for logging and panic recovery
  • Worker Process - Background worker for processing outbox events
  • CLI Interface - urfave/cli for running server, migrations, and worker
  • Health Checks - Kubernetes-compatible readiness and liveness endpoints
  • Structured Logging - JSON logs using slog
  • Configuration - Environment variable based configuration with go-env
  • Input Validation - Advanced validation with jellydator/validation library including password strength, email format, and custom rules
  • Password Hashing - Secure password hashing with Argon2id via go-pwdhash
  • Docker Support - Multi-stage Dockerfile for minimal container size
  • CI/CD - GitHub Actions workflow for linting and testing
  • Comprehensive Makefile - Easy development and deployment commands

Project Structure

go-project-template/
├── cmd/
│   └── app/                    # Application entry point
│       └── main.go
├── internal/
│   ├── app/                    # Dependency injection container
│   │   ├── di.go
│   │   ├── di_test.go
│   │   └── README.md
│   ├── config/                 # Configuration management
│   │   └── config.go
│   ├── database/               # Database connection and transaction management
│   │   ├── database.go
│   │   └── txmanager.go
│   ├── errors/                 # Standardized domain errors
│   │   └── errors.go
│   ├── http/                   # HTTP server and shared infrastructure
│   │   ├── middleware.go
│   │   ├── response.go
│   │   └── server.go
│   ├── httputil/               # HTTP utility functions
│   │   └── response.go         # JSON responses and error mapping
│   ├── outbox/                 # Outbox domain module
│   │   ├── domain/             # Outbox entities
│   │   │   └── outbox_event.go
│   │   └── repository/         # Outbox data access
│   │       ├── mysql_outbox_repository.go
│   │       └── postgresql_outbox_repository.go
│   ├── user/                   # User domain module
│   │   ├── domain/             # User entities and domain errors
│   │   │   └── user.go
│   │   ├── http/               # User HTTP handlers
│   │   │   ├── dto/            # Request/response DTOs
│   │   │   │   ├── request.go
│   │   │   │   ├── response.go
│   │   │   │   └── mapper.go
│   │   │   └── user_handler.go
│   │   ├── repository/         # User data access
│   │   │   ├── mysql_user_repository.go
│   │   │   └── postgresql_user_repository.go
│   │   └── usecase/            # User business logic
│   │       └── user_usecase.go
│   ├── validation/             # Custom validation rules
│   │   ├── rules.go
│   │   └── rules_test.go
│   └── worker/                 # Background workers
│       └── event_worker.go
├── migrations/
│   ├── mysql/                  # MySQL migrations
│   └── postgresql/             # PostgreSQL migrations
├── .github/
│   └── workflows/
│       └── ci.yml
├── Dockerfile
├── Makefile
├── go.mod
└── go.sum
Domain Module Structure

The project follows a modular domain architecture where each business domain is organized in its own directory with clear separation of concerns:

  • domain/ - Contains entities, value objects, domain types, and domain-specific errors
  • usecase/ - Defines UseCase interfaces and implements business logic and orchestration
  • repository/ - Handles data persistence and retrieval, transforms infrastructure errors to domain errors
  • http/ - Contains HTTP handlers and DTOs (Data Transfer Objects)
    • dto/ - Request/response DTOs and mappers (API contracts)
Shared Utilities
  • app/ - Dependency injection container for assembling application components
  • errors/ - Standardized domain errors for expressing business intent
  • httputil/ - Shared HTTP utility functions including error mapping and JSON responses
  • config/ - Application-wide configuration
  • database/ - Database connection and transaction management
  • worker/ - Background processing infrastructure

This structure makes it easy to add new domains (e.g., internal/product/, internal/order/) without affecting existing modules.

Prerequisites

  • Go 1.25 or higher
  • PostgreSQL 12+ or MySQL 8.0+
  • Docker (optional)
  • Make (optional, for convenience commands)

Quick Start

1. Clone the repository
git clone https://github.com/allisson/go-project-template.git
cd go-project-template
2. Customize the module path

After cloning, you need to update the import paths to match your project:

Option 1: Using find and sed (Linux/macOS)

# Replace with your actual module path
NEW_MODULE="github.com/yourname/yourproject"

# Update go.mod
sed -i "s|github.com/allisson/go-project-template|$NEW_MODULE|g" go.mod

# Update all Go files
find . -type f -name "*.go" -exec sed -i "s|github.com/allisson/go-project-template|$NEW_MODULE|g" {} +

Option 2: Using PowerShell (Windows)

# Replace with your actual module path
$NEW_MODULE = "github.com/yourname/yourproject"

# Update go.mod
(Get-Content go.mod) -replace 'github.com/allisson/go-project-template', $NEW_MODULE | Set-Content go.mod

# Update all Go files
Get-ChildItem -Recurse -Filter *.go | ForEach-Object {
    (Get-Content $_.FullName) -replace 'github.com/allisson/go-project-template', $NEW_MODULE | Set-Content $_.FullName
}

Option 3: Manually

  1. Update the module name in go.mod
  2. Search and replace github.com/allisson/go-project-template with your module path in all .go files

After updating, verify the changes and tidy dependencies:

go mod tidy

Important: Also update the .golangci.yml file to match your new module path:

formatters:
  settings:
    goimports:
      local-prefixes:
        - github.com/yourname/yourproject  # Update this line

This ensures the linter correctly groups your local imports.

UUIDv7 Primary Keys

The project uses UUIDv7 for all primary keys instead of auto-incrementing integers. UUIDv7 provides several advantages:

Benefits:

  • Time-ordered: UUIDs include timestamp information, maintaining temporal ordering
  • Globally unique: No collision risk across distributed systems or databases
  • Database friendly: Better index performance than random UUIDs (v4) due to sequential nature
  • Scalability: No need for centralized ID generation or coordination
  • Merge-friendly: Databases can be merged without ID conflicts

Implementation:

All ID fields use uuid.UUID type from github.com/google/uuid:

import "github.com/google/uuid"

type User struct {
    ID        uuid.UUID `db:"id" json:"id"`
    Name      string    `db:"name"`
    Email     string    `db:"email"`
    CreatedAt time.Time `db:"created_at"`
    UpdatedAt time.Time `db:"updated_at"`
}

IDs are generated in the application code using uuid.NewV7():

user := &domain.User{
    ID:       uuid.Must(uuid.NewV7()),
    Name:     input.Name,
    Email:    input.Email,
    Password: hashedPassword,
}

Database Storage:

  • PostgreSQL: UUID type (native support)
  • MySQL: BINARY(16) type (16-byte storage)

Migration Example (PostgreSQL):

CREATE TABLE users (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Migration Example (MySQL):

CREATE TABLE users (
    id BINARY(16) PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. Install dependencies
go mod download
4. Configure environment variables

The application automatically loads environment variables from a .env file. Create a .env file in your project root (or any parent directory):

# Database configuration
DB_DRIVER=postgres  # or mysql
DB_CONNECTION_STRING=postgres://user:password@localhost:5432/mydb?sslmode=disable
DB_MAX_OPEN_CONNECTIONS=25
DB_MAX_IDLE_CONNECTIONS=5
DB_CONN_MAX_LIFETIME=5

# Server configuration
SERVER_HOST=0.0.0.0
SERVER_PORT=8080

# Logging
LOG_LEVEL=info

# Worker configuration
WORKER_INTERVAL=5
WORKER_BATCH_SIZE=10
WORKER_MAX_RETRIES=3
WORKER_RETRY_INTERVAL=1

Note: The application searches for the .env file recursively from the current working directory up to the root directory. This allows you to run the application from any subdirectory and it will still find your .env file.

Alternatively, you can export environment variables directly without a .env file.

5. Start a database (using Docker)

PostgreSQL:

make dev-postgres

MySQL:

make dev-mysql
6. Run database migrations
make run-migrate
7. Start the HTTP server
make run-server

The server will be available at http://localhost:8080

8. Start the worker (in another terminal)
make run-worker

Usage

HTTP Endpoints
Health Check
curl http://localhost:8080/health
Readiness Check
curl http://localhost:8080/ready
Register User
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com",
    "password": "SecurePass123!"
  }'

Password Requirements:

  • Minimum 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one number
  • At least one special character

Validation Errors:

If validation fails, you'll receive a 422 Unprocessable Entity response with details:

{
  "error": "invalid_input",
  "message": "email: must be a valid email address; password: password must contain at least one uppercase letter."
}
CLI Commands

The binary supports three commands via urfave/cli:

Start HTTP Server
./bin/app server
Run Database Migrations
./bin/app migrate
Run Event Worker
./bin/app worker

Error Handling

The project implements a standardized error handling system that expresses business intent rather than exposing infrastructure details.

Domain Error Architecture

Standard Domain Errors (internal/errors/errors.go)

The project defines standard domain errors that can be used across all modules:

var (
    ErrNotFound      = errors.New("not found")          // 404 Not Found
    ErrConflict      = errors.New("conflict")           // 409 Conflict
    ErrInvalidInput  = errors.New("invalid input")      // 422 Unprocessable Entity
    ErrUnauthorized  = errors.New("unauthorized")       // 401 Unauthorized
    ErrForbidden     = errors.New("forbidden")          // 403 Forbidden
)

Domain-Specific Errors (internal/user/domain/user.go)

Each domain defines its own specific errors that wrap the standard errors:

var (
    ErrUserNotFound      = errors.Wrap(errors.ErrNotFound, "user not found")
    ErrUserAlreadyExists = errors.Wrap(errors.ErrConflict, "user already exists")
    ErrInvalidEmail      = errors.Wrap(errors.ErrInvalidInput, "invalid email format")
)
Error Flow Through Layers

1. Repository Layer - Transforms infrastructure errors to domain errors:

func (r *PostgreSQLUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
    querier := database.GetTx(ctx, r.db)
    query := `SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1`
    
    var user domain.User
    err := querier.QueryRowContext(ctx, query, id).Scan(
        &user.ID, &user.Name, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt,
    )
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, domain.ErrUserNotFound  // Infrastructure → Domain
        }
        return nil, apperrors.Wrap(err, "failed to get user by id")
    }
    return &user, nil
}

2. Use Case Layer - Returns domain errors directly:

func (uc *UserUseCase) RegisterUser(ctx context.Context, input RegisterUserInput) (*domain.User, error) {
    if strings.TrimSpace(input.Email) == "" {
        return nil, domain.ErrEmailRequired  // Domain error
    }
    
    if err := uc.userRepo.Create(ctx, user); err != nil {
        return nil, err  // Pass through domain errors
    }
    return user, nil
}

3. HTTP Handler Layer - Maps domain errors to HTTP responses:

func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
    user, err := h.userUseCase.RegisterUser(r.Context(), input)
    if err != nil {
        httputil.HandleError(w, err, h.logger)  // Auto-maps to HTTP status
        return
    }
    httputil.MakeJSONResponse(w, http.StatusCreated, response)
}
HTTP Error Responses

The httputil.HandleError function automatically maps domain errors to appropriate HTTP status codes:

Domain Error HTTP Status Error Code Example Response
ErrNotFound 404 not_found {"error":"not_found","message":"The requested resource was not found"}
ErrConflict 409 conflict {"error":"conflict","message":"A conflict occurred with existing data"}
ErrInvalidInput 422 invalid_input {"error":"invalid_input","message":"invalid email format"}
ErrUnauthorized 401 unauthorized {"error":"unauthorized","message":"Authentication is required"}
ErrForbidden 403 forbidden {"error":"forbidden","message":"You don't have permission"}
Unknown 500 internal_error {"error":"internal_error","message":"An internal error occurred"}
Benefits
  1. No Infrastructure Leaks - Database errors are never exposed to API clients
  2. Business Intent - Errors express domain concepts (ErrUserNotFound vs sql.ErrNoRows)
  3. Consistent HTTP Mapping - Same domain error always maps to same HTTP status
  4. Type-Safe - Use errors.Is() to check for specific error types
  5. Structured Responses - All errors return consistent JSON format
  6. Centralized Logging - All errors are logged with full context before responding
Adding Errors to New Domains

When creating a new domain, define domain-specific errors:

// internal/product/domain/product.go
var (
    ErrProductNotFound    = errors.Wrap(errors.ErrNotFound, "product not found")
    ErrInsufficientStock  = errors.Wrap(errors.ErrConflict, "insufficient stock")
    ErrInvalidPrice       = errors.Wrap(errors.ErrInvalidInput, "invalid price")
)

Then use httputil.HandleError() in your HTTP handlers for automatic mapping.

Development

Build the application
make build
Run tests
make test
Run tests with coverage
make test-coverage
Run linter
make lint
Clean build artifacts
make clean

Docker

Build Docker image
make docker-build
Run server in Docker
make docker-run-server
Run worker in Docker
make docker-run-worker
Run migrations in Docker
make docker-run-migrate

Architecture

Database Repository Pattern

The project uses separate repository implementations for MySQL and PostgreSQL, leveraging Go's standard database/sql package. This approach provides:

  • Database-specific optimizations - Each implementation is tailored to the specific database's features and syntax
  • Type safety - Direct use of database/sql types without abstraction layers
  • Clarity - Explicit SQL queries make it clear what operations are being performed
  • No external dependencies - Uses only the standard library for database operations

Repository Interface (defined in use case layer):

// internal/user/usecase/user_usecase.go
type UserRepository interface {
    Create(ctx context.Context, user *domain.User) error
    GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error)
    GetByEmail(ctx context.Context, email string) (*domain.User, error)
}

MySQL Implementation (internal/user/repository/mysql_user_repository.go):

func (r *MySQLUserRepository) Create(ctx context.Context, user *domain.User) error {
    querier := database.GetTx(ctx, r.db)
    
    query := `INSERT INTO users (id, name, email, password, created_at, updated_at) 
              VALUES (?, ?, ?, ?, NOW(), NOW())`
    
    // Convert UUID to bytes for MySQL BINARY(16)
    uuidBytes, err := user.ID.MarshalBinary()
    if err != nil {
        return apperrors.Wrap(err, "failed to marshal UUID")
    }
    
    _, err = querier.ExecContext(ctx, query, uuidBytes, user.Name, user.Email, user.Password)
    if err != nil {
        if isMySQLUniqueViolation(err) {
            return domain.ErrUserAlreadyExists
        }
        return apperrors.Wrap(err, "failed to create user")
    }
    return nil
}

PostgreSQL Implementation (internal/user/repository/postgresql_user_repository.go):

func (r *PostgreSQLUserRepository) Create(ctx context.Context, user *domain.User) error {
    querier := database.GetTx(ctx, r.db)
    
    query := `INSERT INTO users (id, name, email, password, created_at, updated_at) 
              VALUES ($1, $2, $3, $4, NOW(), NOW())`
    
    _, err := querier.ExecContext(ctx, query, user.ID, user.Name, user.Email, user.Password)
    if err != nil {
        if isPostgreSQLUniqueViolation(err) {
            return domain.ErrUserAlreadyExists
        }
        return apperrors.Wrap(err, "failed to create user")
    }
    return nil
}

Key Differences:

Feature MySQL PostgreSQL
UUID Storage BINARY(16) - requires marshaling/unmarshaling Native UUID type
Placeholders ? for all parameters $1, $2, $3... numbered parameters
Unique Errors Check for "1062" or "duplicate entry" Check for "duplicate key" or "unique constraint"

Transaction Support:

The database.GetTx() helper function retrieves the transaction from context if available, otherwise returns the DB connection:

// internal/database/txmanager.go
type Querier interface {
    ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
    QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
    QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

func GetTx(ctx context.Context, db *sql.DB) Querier {
    if tx, ok := ctx.Value(txKey{}).(*sql.Tx); ok {
        return tx
    }
    return db
}

This pattern ensures repositories work seamlessly within transactions managed by the use case layer.

Dependency Injection Container

The project uses a custom dependency injection (DI) container located in internal/app/ to manage all application components. This provides:

  • Centralized component wiring - All dependencies are assembled in one place
  • Lazy initialization - Components are only created when first accessed
  • Singleton pattern - Each component is initialized once and reused
  • Clean resource management - Unified shutdown for all resources
  • Thread-safe - Safe for concurrent access across goroutines

Example usage:

// Create container with configuration
container := app.NewContainer(cfg)

// Get HTTP server (automatically initializes all dependencies)
server, err := container.HTTPServer()
if err != nil {
    return fmt.Errorf("failed to initialize HTTP server: %w", err)
}

// Clean shutdown
defer container.Shutdown(ctx)

The container manages the entire dependency graph:

Container
├── Infrastructure (Database, Logger)
├── Repositories (User, Outbox)
├── Use Cases (User)
└── Presentation (HTTP Server, Worker)

For more details on the DI container, see internal/app/README.md.

Modular Domain Architecture

The project follows a modular domain-driven structure where each business domain is self-contained:

User Domain (internal/user/)

  • domain/ - User entity and types
  • usecase/ - UseCase interface and user business logic implementation
  • repository/ - User data persistence
  • http/ - User HTTP endpoints and handlers
    • dto/ - Request/response DTOs and mappers

Outbox Domain (internal/outbox/)

  • domain/ - OutboxEvent entity and status types
  • repository/ - Event persistence and retrieval

Shared Infrastructure

  • app/ - Dependency injection container for component assembly
  • config/ - Application configuration
  • database/ - Database connection and transaction management
  • errors/ - Standardized domain errors (ErrNotFound, ErrConflict, etc.)
  • http/ - HTTP server, middleware, and shared utilities
  • httputil/ - Reusable HTTP utilities (JSON responses, error handling, status code mapping)
  • worker/ - Background event processing
Benefits of This Structure
  1. Scalability - Easy to add new domains without affecting existing code
  2. Encapsulation - Each domain is self-contained with clear boundaries
  3. Team Collaboration - Teams can work on different domains independently
  4. Maintainability - Related code is co-located, making it easier to understand and modify
  5. Dependency Management - Centralized DI container simplifies component wiring and testing
Adding New Domains

To add a new domain (e.g., product):

1. Create the domain structure
internal/product/
├── domain/
│   └── product.go              # Domain entity + domain errors
├── usecase/
│   └── product_usecase.go      # UseCase interface + business logic
├── repository/
│   ├── mysql_product_repository.go      # MySQL data access
│   └── postgresql_product_repository.go # PostgreSQL data access
└── http/
    ├── dto/
    │   ├── request.go          # API request DTOs
    │   ├── response.go         # API response DTOs
    │   └── mapper.go           # DTO-to-domain mappers
    └── product_handler.go      # HTTP handlers (uses httputil.HandleError)

Define domain errors in your entity file:

// internal/product/domain/product.go
package domain

import (
    "time"
    apperrors "github.com/yourname/yourproject/internal/errors"
    "github.com/google/uuid"
)

type Product struct {
    ID          uuid.UUID
    Name        string
    Price       float64
    Stock       int
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

// Domain-specific errors
var (
    ErrProductNotFound    = apperrors.Wrap(apperrors.ErrNotFound, "product not found")
    ErrInsufficientStock  = apperrors.Wrap(apperrors.ErrConflict, "insufficient stock")
    ErrInvalidPrice       = apperrors.Wrap(apperrors.ErrInvalidInput, "invalid price")
)
2. Register in DI container

Add the new domain to the dependency injection container (internal/app/di.go):

// Add fields to Container struct
type Container struct {
    // ... existing fields
    productRepo        *productRepository.ProductRepository
    productUseCase     productUsecase.UseCase  // Interface, not concrete type
    productRepoInit    sync.Once
    productUseCaseInit sync.Once
}

// Add getter methods
func (c *Container) ProductRepository() (*productRepository.ProductRepository, error) {
    var err error
    c.productRepoInit.Do(func() {
        c.productRepo, err = c.initProductRepository()
        if err != nil {
            c.initErrors["productRepo"] = err
        }
    })
    // ... error handling
    return c.productRepo, nil
}

func (c *Container) ProductUseCase() (productUsecase.UseCase, error) {
    var err error
    c.productUseCaseInit.Do(func() {
        c.productUseCase, err = c.initProductUseCase()
        if err != nil {
            c.initErrors["productUseCase"] = err
        }
    })
    // ... error handling
    return c.productUseCase, nil
}

// Add initialization methods
func (c *Container) initProductRepository() (productUsecase.ProductRepository, error) {
    db, err := c.DB()
    if err != nil {
        return nil, fmt.Errorf("failed to get database: %w", err)
    }
    
    // Select the appropriate repository based on the database driver
    switch c.config.DBDriver {
    case "mysql":
        return productRepository.NewMySQLProductRepository(db), nil
    case "postgres":
        return productRepository.NewPostgreSQLProductRepository(db), nil
    default:
        return nil, fmt.Errorf("unsupported database driver: %s", c.config.DBDriver)
    }
}

func (c *Container) initProductUseCase() (productUsecase.UseCase, error) {
    txManager, err := c.TxManager()
    if err != nil {
        return nil, fmt.Errorf("failed to get tx manager: %w", err)
    }
    
    productRepo, err := c.ProductRepository()
    if err != nil {
        return nil, fmt.Errorf("failed to get product repository: %w", err)
    }
    
    return productUsecase.NewProductUseCase(txManager, productRepo)
}
3. Wire handlers in HTTP server

Update internal/http/server.go to register product routes:

productHandler := productHttp.NewProductHandler(container.ProductUseCase(), logger)
mux.HandleFunc("/api/products", productHandler.HandleProducts)

Tips:

  • Define a UseCase interface in your usecase package to enable dependency inversion
  • Define domain-specific errors in your domain package by wrapping standard errors
  • Repository layer should transform infrastructure errors (like sql.ErrNoRows) to domain errors
  • Use case layer should return domain errors directly without additional wrapping
  • Use httputil.HandleError() in HTTP handlers for automatic error-to-HTTP status mapping
  • Use the shared httputil.MakeJSONResponse function for consistent JSON responses
  • Keep domain models free of JSON tags - use DTOs for API serialization
  • Implement validation in your request DTOs
  • Create mapper functions to convert between DTOs and domain models
  • Register all components in the DI container for proper lifecycle management
  • HTTP handlers should depend on the UseCase interface, not concrete implementations
Clean Architecture Layers
  1. Domain Layer - Contains business entities, domain errors, and rules (e.g., internal/user/domain)
  2. Repository Layer - Data access implementations using database/sql; transforms infrastructure errors to domain errors (e.g., internal/user/repository)
  3. Use Case Layer - UseCase interfaces and application business logic; returns domain errors (e.g., internal/user/usecase)
  4. Presentation Layer - HTTP handlers that map domain errors to HTTP responses (e.g., internal/user/http)
  5. Utility Layer - Shared utilities including error handling and mapping (e.g., internal/httputil, internal/errors)

Dependency Inversion Principle: The presentation layer (HTTP handlers) and infrastructure (DI container) depend on UseCase interfaces defined in the usecase layer, not on concrete implementations. This enables better testability and decoupling.

Error Flow: Errors flow from repository → use case → handler, where they are transformed from infrastructure concerns to domain concepts, and finally to appropriate HTTP responses.

Data Transfer Objects (DTOs)

The project enforces clear boundaries between internal domain models and external API contracts using DTOs:

Domain Models (internal/user/domain/user.go)

  • Pure internal representation of business entities
  • No JSON tags - completely decoupled from API serialization
  • Focus on business rules and domain logic

DTOs (internal/user/http/dto/)

  • request.go - API request structures with validation
  • response.go - API response structures
  • mapper.go - Conversion functions between DTOs and domain models

Example:

// Request DTO
type RegisterUserRequest struct {
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"password"`
}

func (r *RegisterUserRequest) Validate() error {
    if r.Name == "" {
        return errors.New("name is required")
    }
    // ... validation logic
}

// Response DTO
type UserResponse struct {
    ID        uuid.UUID `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// Mapper functions
func ToRegisterUserInput(req RegisterUserRequest) usecase.RegisterUserInput {
    return usecase.RegisterUserInput{
        Name:     req.Name,
        Email:    req.Email,
        Password: req.Password,
    }
}

func ToUserResponse(user *domain.User) UserResponse {
    return UserResponse{
        ID:        user.ID,
        Name:      user.Name,
        Email:     user.Email,
        CreatedAt: user.CreatedAt,
        UpdatedAt: user.UpdatedAt,
    }
}

Benefits:

  1. Separation of Concerns - Domain models evolve independently from API contracts
  2. Security - Sensitive fields (like passwords) are never exposed in API responses
  3. Flexibility - Different API views of the same domain model (e.g., summary vs detailed)
  4. Versioning - Easy to maintain multiple API versions with different DTOs
  5. Validation - Request validation happens at the DTO level before reaching domain logic
Input Validation

The project uses the jellydator/validation library for comprehensive input validation at both the DTO and use case layers.

Custom Validation Rules (internal/validation/rules.go)

The project provides reusable validation rules:

// Password strength validation
PasswordStrength{
    MinLength:      8,
    RequireUpper:   true,
    RequireLower:   true,
    RequireNumber:  true,
    RequireSpecial: true,
}

// Email format validation
Email

// No leading/trailing whitespace
NoWhitespace

// Not blank after trimming
NotBlank

DTO Validation Example:

func (r *RegisterUserRequest) Validate() error {
    err := validation.ValidateStruct(r,
        validation.Field(&r.Name,
            validation.Required.Error("name is required"),
            appValidation.NotBlank,
            validation.Length(1, 255).Error("name must be between 1 and 255 characters"),
        ),
        validation.Field(&r.Email,
            validation.Required.Error("email is required"),
            appValidation.NotBlank,
            appValidation.Email,
            validation.Length(5, 255).Error("email must be between 5 and 255 characters"),
        ),
        validation.Field(&r.Password,
            validation.Required.Error("password is required"),
            validation.Length(8, 128).Error("password must be between 8 and 128 characters"),
            appValidation.PasswordStrength{
                MinLength:      8,
                RequireUpper:   true,
                RequireLower:   true,
                RequireNumber:  true,
                RequireSpecial: true,
            },
        ),
    )
    return appValidation.WrapValidationError(err)
}

Validation Layers:

  1. DTO Layer - Validates API request structure and basic constraints
  2. Use Case Layer - Validates business logic rules and constraints
  3. Domain Layer - Defines domain-specific error types

Error Responses:

Validation errors are automatically wrapped as ErrInvalidInput and return 422 Unprocessable Entity:

{
  "error": "invalid_input",
  "message": "password: password must contain at least one uppercase letter."
}

Benefits:

  • Declarative - Validation rules are clear and concise
  • Reusable - Custom rules can be shared across the application
  • Type-Safe - Compile-time validation of struct fields
  • Extensible - Easy to add custom validation rules
  • Consistent - Same validation logic at DTO and use case layers
  • User-Friendly - Detailed error messages help API clients fix issues
Transaction Management

The template implements a TxManager interface for handling database transactions:

type TxManager interface {
    WithTx(ctx context.Context, fn func(ctx context.Context) error) error
}

Transactions are automatically injected into the context and used by repositories.

HTTP Utilities

The httputil package provides shared HTTP utilities used across all domain modules:

MakeJSONResponse - Standardized JSON response formatting:

import "github.com/allisson/go-project-template/internal/httputil"

func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
    product, err := h.productUseCase.GetProduct(r.Context(), productID)
    if err != nil {
        httputil.HandleError(w, err, h.logger)
        return
    }
    
    httputil.MakeJSONResponse(w, http.StatusOK, product)
}

HandleError - Automatic domain error to HTTP status code mapping:

func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
    user, err := h.userUseCase.RegisterUser(r.Context(), input)
    if err != nil {
        // Automatically maps domain errors to appropriate HTTP status codes
        // ErrNotFound → 404, ErrConflict → 409, ErrInvalidInput → 422, etc.
        httputil.HandleError(w, err, h.logger)
        return
    }
    httputil.MakeJSONResponse(w, http.StatusCreated, response)
}

HandleValidationError - For JSON decode and validation errors:

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
    httputil.HandleValidationError(w, err, h.logger)  // Returns 400 Bad Request
    return
}

These utilities ensure consistent response formatting and error handling across all HTTP endpoints.

Transactional Outbox Pattern

User registration demonstrates the transactional outbox pattern:

  1. User is created in the database
  2. user.created event is stored in the outbox table (same transaction)
  3. Worker picks up pending events and processes them
  4. Events are marked as processed or failed

This guarantees that events are never lost and provides at-least-once delivery.

Configuration

All configuration is done via environment variables. The application automatically loads a .env file if present (searching recursively from the current directory up to the root).

Environment Variables
Variable Description Default
SERVER_HOST HTTP server host 0.0.0.0
SERVER_PORT HTTP server port 8080
DB_DRIVER Database driver (postgres/mysql) postgres
DB_CONNECTION_STRING Database connection string postgres://user:password@localhost:5432/mydb?sslmode=disable
DB_MAX_OPEN_CONNECTIONS Max open connections 25
DB_MAX_IDLE_CONNECTIONS Max idle connections 5
DB_CONN_MAX_LIFETIME Connection max lifetime 5
LOG_LEVEL Log level (debug/info/warn/error) info
WORKER_INTERVAL Worker poll interval 5
WORKER_BATCH_SIZE Events to process per batch 10
WORKER_MAX_RETRIES Max retry attempts 3
WORKER_RETRY_INTERVAL Retry interval 1

Database Migrations

Migrations are located in migrations/postgresql and migrations/mysql directories.

Creating new migrations
  1. Create new .up.sql and .down.sql files with sequential numbering
  2. Follow the naming convention: 000003_description.up.sql
Running migrations manually

Use the golang-migrate CLI:

migrate -path migrations/postgresql -database "postgres://user:password@localhost:5432/mydb?sslmode=disable" up

Testing

The project includes a CI workflow that runs tests with PostgreSQL.

Running tests locally
go test -v -race ./...
With coverage
go test -v -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

Dependencies

Core Libraries
Database Drivers

License

MIT License - see LICENSE file for details

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Acknowledgments

This template uses the following excellent Go libraries:

  • github.com/allisson/go-env
  • github.com/allisson/go-pwdhash
  • github.com/jellydator/validation
  • github.com/urfave/cli
  • github.com/golang-migrate/migrate

Directories

Path Synopsis
cmd
app command
Package main provides the entry point for the application with CLI commands.
Package main provides the entry point for the application with CLI commands.
internal
app
Package app provides dependency injection container for assembling application components.
Package app provides dependency injection container for assembling application components.
config
Package config provides application configuration management through environment variables.
Package config provides application configuration management through environment variables.
database
Package database provides database connection management and configuration.
Package database provides database connection management and configuration.
errors
Package errors provides standardized domain errors that express business intent rather than infrastructure details.
Package errors provides standardized domain errors that express business intent rather than infrastructure details.
http
Package http provides HTTP server implementation and request handlers.
Package http provides HTTP server implementation and request handlers.
httputil
Package httputil provides HTTP utility functions for request and response handling.
Package httputil provides HTTP utility functions for request and response handling.
outbox/domain
Package domain defines the core outbox domain entities and types.
Package domain defines the core outbox domain entities and types.
outbox/repository
Package repository provides data persistence implementations for outbox entities.
Package repository provides data persistence implementations for outbox entities.
user/domain
Package domain defines the core user domain entities and types.
Package domain defines the core user domain entities and types.
user/http
Package http provides HTTP handlers for user-related operations.
Package http provides HTTP handlers for user-related operations.
user/http/dto
Package dto provides data transfer objects for the user HTTP layer.
Package dto provides data transfer objects for the user HTTP layer.
user/repository
Package repository provides data persistence implementations for user entities.
Package repository provides data persistence implementations for user entities.
user/usecase
Package usecase implements the user business logic and orchestrates user domain operations.
Package usecase implements the user business logic and orchestrates user domain operations.
validation
Package validation provides custom validation rules for the application.
Package validation provides custom validation rules for the application.
worker
Package worker provides background workers for processing asynchronous tasks.
Package worker provides background workers for processing asynchronous tasks.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL