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
- Dependency Injection Container - Centralized component wiring with lazy initialization and clean resource management
- Multiple Database Support - PostgreSQL and MySQL via unified repository layer
- Database Migrations - Separate migrations for PostgreSQL and MySQL using golang-migrate
- 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
- 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
│ ├── http/ # HTTP server and shared infrastructure
│ │ ├── middleware.go
│ │ ├── response.go
│ │ └── server.go
│ ├── httputil/ # HTTP utility functions
│ │ └── response.go
│ ├── outbox/ # Outbox domain module
│ │ ├── domain/ # Outbox entities
│ │ │ └── outbox_event.go
│ │ └── repository/ # Outbox data access
│ │ └── outbox_repository.go
│ ├── user/ # User domain module
│ │ ├── domain/ # User entities
│ │ │ └── user.go
│ │ ├── http/ # User HTTP handlers
│ │ │ ├── dto/ # Request/response DTOs
│ │ │ │ ├── request.go
│ │ │ │ ├── response.go
│ │ │ │ └── mapper.go
│ │ │ └── user_handler.go
│ │ ├── repository/ # User data access
│ │ │ └── user_repository.go
│ │ └── usecase/ # User business logic
│ │ └── user_usecase.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, and domain types (pure internal representation)
usecase/ - Defines UseCase interfaces and implements business logic and orchestration
repository/ - Handles data persistence and retrieval
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
httputil/ - Shared HTTP utility functions used across all domain modules (e.g., MakeJSONResponse)
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
- Update the module name in
go.mod
- 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.
3. Install dependencies
go mod download
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": "securepassword123"
}'
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
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
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
http/ - HTTP server, middleware, and shared utilities
httputil/ - Reusable HTTP utilities (JSON responses, error handling)
worker/ - Background event processing
Benefits of This Structure
- Scalability - Easy to add new domains without affecting existing code
- Encapsulation - Each domain is self-contained with clear boundaries
- Team Collaboration - Teams can work on different domains independently
- Maintainability - Related code is co-located, making it easier to understand and modify
- 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 (no JSON tags)
├── usecase/
│ └── product_usecase.go # UseCase interface + business logic
├── repository/
│ └── product_repository.go # 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
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() (*productRepository.ProductRepository, error) {
db, err := c.DB()
if err != nil {
return nil, fmt.Errorf("failed to get database: %w", err)
}
return productRepository.NewProductRepository(db, c.config.DBDriver), nil
}
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
- Use the shared
httputil.MakeJSONResponse function in your HTTP handlers 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
- Domain Layer - Contains business entities and rules (e.g.,
internal/user/domain)
- Repository Layer - Data access implementations using sqlutil (e.g.,
internal/user/repository)
- Use Case Layer - UseCase interfaces and application business logic (e.g.,
internal/user/usecase)
- Presentation Layer - HTTP handlers and server (e.g.,
internal/user/http)
- Utility Layer - Shared utilities and helpers (e.g.,
internal/httputil)
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.
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 int64 `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:
- Separation of Concerns - Domain models evolve independently from API contracts
- Security - Sensitive fields (like passwords) are never exposed in API responses
- Flexibility - Different API views of the same domain model (e.g., summary vs detailed)
- Versioning - Easy to maintain multiple API versions with different DTOs
- Validation - Request validation happens at the DTO level before reaching domain logic
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.MakeJSONResponse(w, http.StatusNotFound, map[string]string{
"error": "product not found",
})
return
}
httputil.MakeJSONResponse(w, http.StatusOK, product)
}
This ensures consistent response formatting across all HTTP endpoints and eliminates code duplication.
Transactional Outbox Pattern
User registration demonstrates the transactional outbox pattern:
- User is created in the database
user.created event is stored in the outbox table (same transaction)
- Worker picks up pending events and processes them
- 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
- Create new
.up.sql and .down.sql files with sequential numbering
- 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/allisson/sqlutil
- github.com/urfave/cli
- github.com/golang-migrate/migrate