A production-ready Go template repository implementing Hexagonal Architecture (Ports & Adapters) with feature-sliced structure. This template provides a solid foundation for building scalable REST APIs, gRPC services, CLI applications, and background workers.
🎯 This is a GitHub Template Repository!
Click the "Use this template" button above to create your own project based on this architecture.
For setup instructions, see TEMPLATE_SETUP.md
- Features
- Architecture
- Project Structure
- Getting Started
- API Documentation
- Development
- Testing
- Deployment
- Contributing
- License
- ✅ Hexagonal Architecture - Clean separation between domain logic and infrastructure
- ✅ Feature-Sliced Structure - Vertical organization by domain feature
- ✅ Dependency Injection - Compile-time DI with Google Wire
- ✅ SOLID Principles - Maintainable and testable code
- ✅ REST API - HTTP/JSON API with Gin framework
- 🚧 gRPC - High-performance RPC (planned)
- 🚧 CLI - Command-line interface with Cobra (planned)
- 🚧 Workers - Background job processing (planned)
- ✅ PostgreSQL - Primary database with GORM v2
- 🚧 MongoDB - Document store (planned)
- 🚧 Redis - Caching and pub/sub (planned)
- ✅ Structured Logging - JSON logging with zerolog
- ✅ Health Checks - Kubernetes-ready liveness/readiness endpoints
- 🚧 Metrics - Prometheus metrics (planned)
- 🚧 Tracing - OpenTelemetry distributed tracing (planned)
- ✅ Taskfile - Simple task runner for common operations
- ✅ Docker Compose - Local development environment
- ✅ Hot Reload - Fast development iteration
- ✅ Database Migrations - Version-controlled schema changes
- ✅ Code Generation - Mocks and Wire providers
- ✅ Unit Tests - 83.8% average coverage
- ✅ Integration Tests - Testcontainers with real PostgreSQL
- ✅ Mocking - Type-safe mocks with Mockery v3
- 🚧 E2E Tests - Full system tests (planned)
This template implements Hexagonal Architecture (Ports & Adapters) with three main layers:
┌─────────────────────────────────────────────────────────┐
│ External World │
│ (HTTP, gRPC, CLI, Databases, Message Queues, etc.) │
└────────────────────┬────────────────────────────────────┘
│
┌───────────▼──────────┐
│ Adapters (Input) │
│ - HTTP Handlers │
│ - gRPC Servers │
│ - CLI Commands │
└───────────┬──────────┘
│
┌───────────▼──────────┐
│ Ports (Input) │
│ - Service Interface │
└───────────┬──────────┘
│
┌────────────────▼────────────────┐
│ Domain Core │
│ - Business Logic │
│ - Domain Entities │
│ - Domain Errors │
│ - Validation Rules │
└────────────────┬────────────────┘
│
┌───────────▼──────────┐
│ Ports (Output) │
│ - Repository Iface │
└───────────┬──────────┘
│
┌───────────▼──────────┐
│ Adapters (Output) │
│ - PostgreSQL │
│ - MongoDB │
│ - Redis │
└──────────────────────┘
- Dependencies point inward - Domain has zero external dependencies
- Interfaces at boundaries - Ports define contracts, adapters implement them
- Testability - Each layer can be tested in isolation
- Flexibility - Easy to swap implementations (e.g., switch databases)
.
├── cmd/ # Application entry points
│ └── api/ # REST API server
│ ├── main.go # Server bootstrap
│ ├── wire.go # Wire injector definition
│ └── integration_test.go # Integration tests
├── internal/ # Private application code
│ ├── config/ # Configuration management
│ │ ├── config.go
│ │ └── config_test.go
│ ├── infrastructure/ # Infrastructure concerns
│ │ ├── database/ # Database connections
│ │ │ └── postgres.go
│ │ ├── health/ # Health check system
│ │ │ ├── health.go
│ │ │ └── health_test.go
│ │ └── logger/ # Logging infrastructure
│ │ ├── logger.go
│ │ └── logger_test.go
│ ├── user/ # User feature (example domain)
│ │ ├── domain/ # Domain layer (business logic)
│ │ │ ├── user.go # User entity with validation
│ │ │ ├── user_test.go
│ │ │ └── errors.go # Domain-specific errors
│ │ ├── ports/ # Ports (interfaces)
│ │ │ ├── repository.go # Repository interface
│ │ │ ├── service.go # Service interface
│ │ │ └── mocks/ # Generated mocks
│ │ ├── service/ # Domain service implementation
│ │ │ ├── user_service.go
│ │ │ └── user_service_test.go
│ │ └── adapters/ # Infrastructure adapters
│ │ ├── postgres/ # PostgreSQL adapter
│ │ │ ├── models.go # GORM models
│ │ │ ├── mappers.go # Domain ↔ DB mappers
│ │ │ ├── repository.go
│ │ │ └── repository_test.go
│ │ └── http/ # HTTP adapter
│ │ ├── dto.go # Request/Response DTOs
│ │ ├── handlers.go # HTTP handlers
│ │ └── routes.go # Route registration
│ └── wire/ # Wire providers
│ └── providers.go
├── migrations/ # Database migrations
│ ├── 000001_create_users_table.up.sql
│ └── 000001_create_users_table.down.sql
├── docs/ # Documentation
│ └── plans/ # Design and implementation plans
├── config.yaml # Application configuration
├── docker-compose.yml # Local infrastructure
├── Taskfile.yml # Task automation
├── .gitignore
├── .mockery.yml # Mockery configuration
├── go.mod
└── README.md
Each feature (e.g., user, post, product) follows this structure:
internal/
└── {feature}/
├── domain/ # Business logic, entities, validation
├── ports/ # Interface definitions
├── service/ # Service implementation (uses repository port)
└── adapters/ # Infrastructure implementations
├── postgres/ # Database adapter
├── http/ # HTTP adapter
├── grpc/ # gRPC adapter (if needed)
└── mongodb/ # Alternative database (if needed)
- Go 1.25+ - Download (Latest: 1.25.4)
- Docker & Docker Compose - Download
- Task (optional) - Install
- golang-migrate - Install
- Clone the template
git clone https://github.com/yourusername/go-scaffolding.git my-project
cd my-project- Update module name
# Replace 'github.com/yourusername/go-scaffolding' with your module name
find . -type f -name '*.go' -exec sed -i 's|github.com/yourusername/go-scaffolding|github.com/yourorg/yourproject|g' {} +
go mod edit -module github.com/yourorg/yourproject
go mod tidy- Start infrastructure
# Using Task (recommended)
task docker:up
# Or using docker compose directly
docker compose up -d postgres- Run database migrations
task migrate:up
# Or manually
migrate -path ./migrations -database "postgresql://postgres:postgres@localhost:5432/app?sslmode=disable" up- Run the API server
# Using Task
task run:api
# Or build and run
task build:api
./bin/api
# Or run directly
go run ./cmd/api- Verify it's running
# Check health
curl http://localhost:8080/health/live
# Create a user
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","name":"Test User"}'
# List users
curl http://localhost:8080/usersLiveness check - Always returns 200 if the server is running
curl http://localhost:8080/health/liveResponse:
{
"status": "healthy",
"checks": {
"liveness": {"status": "healthy"}
}
}Readiness check - Returns 200 if ready to serve traffic, 503 if not ready
curl http://localhost:8080/health/readyResponse:
{
"status": "healthy",
"checks": {
"database": {"status": "healthy"}
}
}Create a new user
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"name": "John Doe"
}'Response (201 Created):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@example.com",
"name": "John Doe",
"created_at": "2025-11-22T10:00:00Z",
"updated_at": "2025-11-22T10:00:00Z"
}Errors:
400 Bad Request- Invalid email format or missing required fields409 Conflict- Email already exists
Get a user by ID
curl http://localhost:8080/users/550e8400-e29b-41d4-a716-446655440000Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@example.com",
"name": "John Doe",
"created_at": "2025-11-22T10:00:00Z",
"updated_at": "2025-11-22T10:00:00Z"
}Errors:
404 Not Found- User not found
Get a user by email address
curl http://localhost:8080/users/email/john@example.comResponse (200 OK): Same as GET /users/:id
List users with pagination
curl "http://localhost:8080/users?limit=10&offset=0"Response (200 OK):
{
"users": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@example.com",
"name": "John Doe",
"created_at": "2025-11-22T10:00:00Z",
"updated_at": "2025-11-22T10:00:00Z"
}
],
"limit": 10,
"offset": 0
}Query Parameters:
limit- Number of users to return (default: 10, max: 100)offset- Number of users to skip (default: 0)
Results are ordered by created_at DESC (newest first).
Errors:
400 Bad Request- Limit exceeds 100
Update a user's name
curl -X PUT http://localhost:8080/users/550e8400-e29b-41d4-a716-446655440000 \
-H "Content-Type: application/json" \
-d '{"name": "John Updated"}'Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@example.com",
"name": "John Updated",
"created_at": "2025-11-22T10:00:00Z",
"updated_at": "2025-11-22T10:05:00Z"
}Note: Email cannot be updated for data integrity reasons.
Errors:
400 Bad Request- Invalid name404 Not Found- User not found
Delete a user (soft delete)
curl -X DELETE http://localhost:8080/users/550e8400-e29b-41d4-a716-446655440000Response (204 No Content): Empty body
Errors:
404 Not Found- User not found
View all available tasks:
task --listCommon tasks:
# Development
task run:api # Run API server
task docker:up # Start infrastructure
task docker:down # Stop infrastructure
# Testing
task test # Run unit tests
task test:integration # Run integration tests (requires Docker)
task test:coverage # Generate coverage report
# Database
task migrate:up # Run migrations
task migrate:down # Rollback last migration
task migrate:create # Create new migration
# Code Generation
task mock:generate # Generate mocks with Mockery
task wire:generate # Generate Wire dependency injection code
# Build
task build:api # Build API binaryExample: Adding a Post feature
- Create domain layer
mkdir -p internal/post/domainCreate internal/post/domain/post.go:
package domain
import (
"errors"
"time"
)
type Post struct {
ID string
Title string
Content string
AuthorID string
CreatedAt time.Time
UpdatedAt time.Time
}
func NewPost(title, content, authorID string) (*Post, error) {
if title == "" {
return nil, errors.New("title cannot be empty")
}
// Add validation...
return &Post{
Title: title,
Content: content,
AuthorID: authorID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}, nil
}- Define ports
Create internal/post/ports/repository.go:
package ports
import (
"context"
"github.com/yourorg/yourproject/internal/post/domain"
)
type PostRepository interface {
Create(ctx context.Context, post *domain.Post) error
GetByID(ctx context.Context, id string) (*domain.Post, error)
// Add other methods...
}- Implement service
Create internal/post/service/post_service.go:
package service
import (
"context"
"github.com/yourorg/yourproject/internal/post/domain"
"github.com/yourorg/yourproject/internal/post/ports"
)
type PostService struct {
repo ports.PostRepository
}
func NewPostService(repo ports.PostRepository) *PostService {
return &PostService{repo: repo}
}
func (s *PostService) CreatePost(ctx context.Context, title, content, authorID string) (*domain.Post, error) {
post, err := domain.NewPost(title, content, authorID)
if err != nil {
return nil, err
}
if err := s.repo.Create(ctx, post); err != nil {
return nil, err
}
return post, nil
}-
Implement adapters
- Create PostgreSQL adapter in
internal/post/adapters/postgres/ - Create HTTP adapter in
internal/post/adapters/http/
- Create PostgreSQL adapter in
-
Wire it up
Update internal/wire/providers.go to include Post providers.
- Write tests
- Unit tests for domain and service
- Integration tests for adapters
Configuration is managed via config.yaml and environment variables.
Environment variables override config file values using the pattern: SECTION_KEY
Examples:
# Override database host
export POSTGRES_HOST=localhost
# Override log level
export APP_LOG_LEVEL=debug
# Override HTTP port
export APP_HTTP_PORT=3000Run all unit tests:
task testRun tests for a specific package:
go test ./internal/user/domain/... -vRun tests with coverage:
task test:coverage
# Open coverage.html in browserIntegration tests use testcontainers to spin up real PostgreSQL containers.
# Requires Docker
task test:integration
# Or directly
go test ./cmd/api/... -run TestIntegration -vCurrent coverage: 83.8%
| Package | Coverage |
|---|---|
| config | 90% |
| logger | 69% |
| health | 100% |
| user/domain | 97% |
| user/service | 62% |
| user/adapters/postgres | 81% |
Example unit test with mocks:
func TestUserService_CreateUser(t *testing.T) {
// Arrange
mockRepo := new(mocks.MockUserRepository)
service := service.NewUserService(mockRepo)
mockRepo.On("GetByEmail", mock.Anything, "test@example.com").
Return(nil, domain.ErrUserNotFound)
mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*domain.User")).
Return(nil)
// Act
user, err := service.CreateUser(context.Background(), "test@example.com", "Test User")
// Assert
assert.NoError(t, err)
assert.Equal(t, "test@example.com", user.Email)
mockRepo.AssertExpectations(t)
}Build the Docker image:
docker build -t myapp:latest .Run the container:
docker run -p 8080:8080 \
-e POSTGRES_HOST=postgres \
-e POSTGRES_PORT=5432 \
myapp:latestExample Kubernetes manifests are in deployments/k8s/ (to be added).
Health check configuration:
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5Contributions are welcome! Please read the contributing guidelines first.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
- Ensure all tests pass (
task test:all) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
We follow Conventional Commits:
feat:- New featurefix:- Bug fixdocs:- Documentation changestest:- Test additions or changesrefactor:- Code refactoringchore:- Maintenance tasks
This project is licensed under the MIT License - see the LICENSE file for details.
- Hexagonal Architecture by Alistair Cockburn
- Clean Architecture by Robert C. Martin
- Feature-Sliced Design
Made with ❤️ by developers, for developers