README
¶
GoCleanstack
THIS IS A WORK IN PROGRESS !
DO NOT USE IN PRODUCTION !
A production-ready Go multi-module workspace demonstrating Clean Architecture principles with Domain-Driven Design and Hexagonal Architecture, governed by a unified CLI project manager.
Go Workspace Architecture
This project uses Go workspaces (go.work) to manage multiple independent modules:
| Module | Path | Purpose |
|---|---|---|
| Root | / |
CLI orchestrator, aggregates sub-applications |
| Common | /internal/common |
Shared platform utilities (logging, config, errors) |
| User | /internal/app/user |
User management application with CRUD operations |
Benefits:
- Independent versioning and testing per module
- Clear boundaries between applications
- Shared utilities without code duplication
- Easy to add new applications (app2, app3, etc.)
Features
- Clean Architecture: Strict separation of concerns with domain, application, and infrastructure layers
- Hexagonal Architecture: Ports and adapters pattern for dependency inversion
- Connect RPC: Modern RPC framework built on protobuf and HTTP/2
- CLI Interface: Cobra-based command-line interface
- Database: PostgreSQL with sqlx and Goose v3 migrations
- Configuration: Viper-based configuration with environment-specific TOML files
- Testing: Full test pyramid (unit, integration, e2e) with testcontainers
- Docker: Production-ready Docker setup with docker-compose
- Dev Tools: justfile, golangci-lint, pre-commit hooks
Architecture
This project follows Hexagonal Architecture principles with clear boundaries, organized as a Go workspace:
/ - Root module (CLI orchestrator)
├── main.go - Entry point, aggregates sub-applications
/internal/common/ - Common module (shared utilities)
├── platform/
│ ├── logging/ - Logger abstraction (zap-based)
│ ├── config/ - Generic configuration loader
│ ├── apperr/ - Application error types
│ ├── clierr/ - CLI error handling
│ └── reqid/ - Request ID utilities
└── transport/
└── connectx/ - Connect RPC interceptors and error handling
/internal/app/user/ - User module (user management application)
├── domain/ - Pure business logic (no external dependencies)
│ ├── entity/ - Domain entities (User, Role) with business rules
│ └── ports/ - Interfaces defining what domain needs
├── service/ - Use cases and business workflows
├── adapters/ - Bridges between domain and infrastructure
├── config/ - App-specific configuration
├── cmd/ - App CLI commands (serve, version)
├── api/ - Connect RPC API layer
│ ├── proto/ - Protobuf definitions
│ ├── gen/ - Generated code
│ ├── handler/ - Request handlers
│ └── server.go - HTTP server
└── infra/ - External concerns and frameworks
└── persistence/ - Database access with DTOs and migrations
Dependency Rules
- Domain has no dependencies on any other layer
- Application depends only on domain (via ports)
- Infrastructure provides implementations of domain ports via adapters
- Infrastructure never imports domain directly (uses DTOs instead)
Project Structure
.
├── go.work # Go workspace definition
├── go.mod # Root module
├── main.go # Entry point (CLI orchestrator)
│
├── internal/
│ ├── common/ # Common module (go.mod)
│ │ ├── platform/ # Cross-cutting utilities
│ │ │ ├── logging/ # Logger abstraction
│ │ │ ├── config/ # Generic config loader
│ │ │ ├── apperr/ # Application errors
│ │ │ ├── clierr/ # CLI error handling
│ │ │ └── reqid/ # Request ID utilities
│ │ └── transport/ # Transport utilities
│ │ └── connectx/ # Connect RPC interceptors
│ │
│ └── app/
│ └── user/ # User module (go.mod)
│ ├── main.go # App entry point (standalone use)
│ ├── cmd/ # App CLI commands
│ │ ├── root.go # Root command with logger init
│ │ ├── serve.go # HTTP server command
│ │ └── version.go # Version command
│ ├── config/ # App-specific configuration
│ │ └── config.go
│ ├── domain/ # Core business logic
│ │ ├── entity/ # Domain entities
│ │ │ ├── user.go # User entity with validation
│ │ │ └── role.go # Role enum with business rules
│ │ └── ports/ # Port interfaces
│ │ └── repository.go
│ ├── service/ # Use cases
│ │ └── user_service.go
│ ├── adapters/ # Domain-to-infra adapters
│ │ └── user_repo_adapter.go
│ ├── api/ # Connect RPC API
│ │ ├── proto/ # Protobuf definitions
│ │ ├── gen/ # Generated code
│ │ ├── handler/ # Request handlers
│ │ ├── interceptor/ # Custom interceptors
│ │ └── server.go
│ ├── infra/ # Infrastructure layer
│ │ └── persistence/ # Database access
│ │ ├── db.go
│ │ ├── models.go
│ │ ├── user_repo.go
│ │ └── migrations/
│ └── tests/ # App-specific tests
│ ├── integration/ # Integration tests
│ ├── e2e/ # End-to-end tests
│ └── testutil/ # Test utilities
│
├── config_default.toml # Default configuration
├── docker-compose.yml # Docker orchestration
├── Dockerfile # Production container
├── justfile # Task runner
└── .golangci.yml # Linter configuration
Prerequisites
- Go 1.25 or higher (optional if the app is built from Docker)
- PostgreSQL 16 or higher
- Docker and docker-compose (for testing and local development)
- Node.js/npm (for local development)
- Python 3 (for pre-commit hooks and local development)
If the installation of Python 3 is system-wide, you must install pre-commit yourself.
Getting Started
1. Install Dependencies
Simply run this command :
./configure
This command can be launch as many times as you want.
2. Configuration
The application uses environment-based configuration with two files:
config_default.toml- Base configuration (always loaded)config_<env>.toml- Environment-specific overrides (merged with default)
Important: Set APP_ENV environment variable to choose configuration (REQUIRED - no default):
export APP_ENV=development # or production
Or use [direnv](https://direnv.net/).
3. Start Database
Using Docker:
just up or docker-compose up -d or manually start PostgreSQL and update the database URL in your config file.
4. Run Migrations
just migrate-up or ./bin/cleanstack migrate up or go run . migrate up
5. Run the Server
just dev or go run . serve
The API will be available at http://localhost:4224.
Development Commands
This project uses just as a task runner. Available commands follow.
Development
just generate-api # Generate code from protobuf definitions
just dev # Run development server (requires APP_ENV env var)
Testing
just test # Run unit tests
just test-int # Run integration tests (requires Docker)
just test-e2e # Run end-to-end tests (requires Docker)
just test-all # Run all tests
just test-cover # Generate test coverage report
Database
just migrate-up # Run database migrations
just migrate-down # Rollback database migrations
Code Quality
just lint # Run linter
just lint-fix # Run linter with auto-fix
Build
just build # Build binary to bin/cleanstack
Docker
just up # Start services with docker-compose
just down # Stop services
just logs # View service logs
Cleanup
just clean # Remove build artifacts and coverage files
Testing
This project implements a comprehensive test pyramid:
Unit Tests
Run unit tests that don't require external dependencies:
just test
# or
go test ./...
Unit tests are co-located with the code they test:
internal/app/user/domain/entity/user_test.go- Domain entity testsinternal/app/user/domain/entity/role_test.go- Role enum testsinternal/app/user/service/user_service_test.go- Service layer testsinternal/common/platform/config/config_test.go- Configuration tests
Integration Tests
Run integration tests that use testcontainers for real PostgreSQL:
just test-int
# or
go test -tags=integration ./tests/integration/...
Integration tests verify:
- Database operations with real PostgreSQL
- Repository implementations
- Data persistence and retrieval
E2E Tests
Run end-to-end tests that test the full application stack:
just test-e2e
# or
go test -tags=e2e ./tests/e2e/...
E2E tests verify:
- Complete API flows
- Request/response handling
- Full dependency wiring
Test Coverage
Generate a test coverage report:
just test-cover
open coverage.html
API Usage
The application exposes a Connect RPC API. You can interact with it using any HTTP client or Connect-compatible client.
Example: Create a User
curl -X POST http://localhost:4224/user.v1.UserService/CreateUser \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "securepassword123",
"role": "ROLE_USER"
}'
Example: List Users
curl -X POST http://localhost:4224/user.v1.UserService/ListUsers \
-H "Content-Type: application/json" \
-d '{"limit": 10, "offset": 0}'
Example: Get User by ID
curl -X POST http://localhost:4224/user.v1.UserService/GetUser \
-H "Content-Type: application/json" \
-d '{"id": 1}'
Docker Deployment
Build and Run
# Build the image
docker build -t go-cleanstack .
# Run with docker-compose
docker-compose up
The docker-compose setup includes:
- Application container (port varies by APP_ENV: 4224/4225/4226)
- PostgreSQL database (port varies by APP_ENV: 5435/5436/5437)
- Health checks and automatic migrations
Environment Variables
Configure the application using environment variables in docker-compose.yml:
environment:
- APP_ENV=production
Code Generation
Protocol Buffer definitions are in internal/app/user/api/proto/user/v1/. To regenerate code:
just generate-api
This uses buf to generate:
- Go structs from protobuf messages
- Connect RPC service interfaces and handlers
- Generated code is output to
internal/app/user/api/gen/
Linting
This project uses golangci-lint with a comprehensive configuration:
just lint # Check for issues
just lint-fix # Auto-fix issues
Pre-commit hooks are configured to run linting automatically.
Contributing
This is a template/skeleton project demonstrating architecture patterns. To use it as a starting point:
- Fork or clone the repository
- Update module names in
go.mod,go.work, and all sub-modulego.modfiles - Update
replacedirectives in allgo.modfiles to match your module path - Update protobuf package names in
internal/app/user/api/proto/ - Implement your domain entities and business logic
- Add corresponding service methods and API endpoints
- To add a new application, create a new module under
internal/app/(see CLAUDE.md for detailed steps)
Architecture Highlights
Hexagonal Architecture Benefits
- Testability: Domain logic can be tested without databases or HTTP
- Flexibility: Easy to swap infrastructure implementations
- Independence: Domain layer is completely isolated from frameworks
- Maintainability: Clear boundaries reduce cognitive load
The Adapter Pattern
The UserRepositoryAdapter demonstrates the adapter pattern:
// Domain defines what it needs (port)
type UserRepository interface {
Create(ctx context.Context, user *entity.User) (*entity.User, error)
GetByID(ctx context.Context, id int64) (*entity.User, error)
List(ctx context.Context, offset, limit int) ([]*entity.User, int64, error)
}
// Infrastructure provides DTOs (no domain import)
type UserRepo struct {}
func (r *UserRepo) Create(ctx context.Context, dto *UserDTO) (*UserDTO, error)
// Adapter bridges domain and infrastructure
type UserRepositoryAdapter struct {
infraRepo *persistence.UserRepo
}
func (a *UserRepositoryAdapter) Create(ctx context.Context, user *entity.User) (*entity.User, error) {
dto := toDTO(user) // Convert entity to DTO
result, err := a.infraRepo.Create(ctx, dto)
return toEntity(result), err // Convert DTO back to entity
}
This ensures infrastructure never imports domain, maintaining clean boundaries.
Resources
Documentation
¶
There is no documentation for this package.