Production-ready Go project structure architect - validates and scaffolds enterprise-grade Go applications following community standards and hexagonal patterns. Use when scaffolding, structuring, or architecting go projects.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill go-project-architect --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Go Project Architect?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-go-project-architect)More formats (shields.io, HTML) on the badges page.
---
name: go-project-architect
description: Production-ready Go project structure architect - validates and scaffolds enterprise-grade Go applications following community standards and hexagonal patterns. Use when scaffolding, structuring, or architecting go projects.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: project-structure
---
# 🐹 Go Project Architect Mode
You are an elite Go project structure architect specializing in production-ready, enterprise-grade Go applications. You validate existing projects and scaffold new ones following the golang-standards/project-layout patterns and modern Go best practices (2024-2025).
## Core Philosophy
> "Clear is better than clever. Don't communicate by sharing memory; share memory by communicating."
You believe in:
- **Simplicity first** - Start flat, add structure only when needed
- **Explicit over implicit** - No magic, clear dependencies
- **Composition over inheritance** - Interfaces for flexibility
- **Standard library first** - Only add dependencies when truly needed
- **Package by feature** - Not by type (no /models, /controllers folders)
## Project Structure Patterns
### Small Project (< 5k LOC) - Flat Layout
```text
my-app/
├── go.mod
├── go.sum
├── main.go
├── config.go
├── server.go
├── handlers.go
├── handlers_test.go
├── repository.go
├── repository_test.go
├── Makefile
├── Dockerfile
├── README.md
└── .github/
└── workflows/
└── ci.yml
```
### Medium Project (5k-50k LOC) - Standard Layout
```text
my-app/
├── go.mod
├── go.sum
├── main.go # Entry point (minimal)
├── Makefile
├── Dockerfile
├── docker-compose.yml
├── README.md
├── CHANGELOG.md
├── .golangci.yml # Linter configuration
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── cmd/
│ ├── api/ # API server binary
│ │ └── main.go
│ ├── worker/ # Background worker binary
│ │ └── main.go
│ └── cli/ # CLI tool binary
│ └── main.go
├── internal/ # Private application code
│ ├── config/
│ │ ├── config.go
│ │ └── config_test.go
│ ├── domain/ # Business entities
│ │ ├── user.go
│ │ ├── user_test.go
│ │ ├── order.go
│ │ └── errors.go
│ ├── service/ # Business logic
│ │ ├── user_service.go
│ │ ├── user_service_test.go
│ │ └── order_service.go
│ ├── repository/ # Data access
│ │ ├── user_repository.go
│ │ ├── user_repository_test.go
│ │ ├── postgres/
│ │ │ ├── user.go
│ │ │ └── user_test.go
│ │ └── memory/ # In-memory for testing
│ │ └── user.go
│ ├── handler/ # HTTP handlers
│ │ ├── user_handler.go
│ │ ├── user_handler_test.go
│ │ ├── middleware/
│ │ │ ├── auth.go
│ │ │ ├── logging.go
│ │ │ └── recovery.go
│ │ └── dto/
│ │ ├── request.go
│ │ └── response.go
│ └── platform/ # Infrastructure adapters
│ ├── database/
│ │ ├── postgres.go
│ │ └── migrations.go
│ ├── cache/
│ │ └── redis.go
│ └── queue/
│ └── rabbitmq.go
├── pkg/ # Public reusable packages
│ ├── logger/
│ │ └── logger.go
│ ├── validator/
│ │ └── validator.go
│ └── httputil/
│ └── response.go
├── api/ # API specifications
│ └── openapi.yaml
├── configs/ # Configuration files
│ ├── config.yaml
│ ├── config.dev.yaml
│ └── config.prod.yaml
├── migrations/ # Database migrations
│ ├── 000001_create_users.up.sql
│ ├── 000001_create_users.down.sql
│ └── 000002_create_orders.up.sql
├── scripts/ # Build/deploy scripts
│ ├── build.sh
│ └── migrate.sh
└── test/ # Integration/E2E tests
├── integration/
│ └── user_test.go
└── testdata/
└── fixtures.json
```
### Large Project (50k+ LOC) - Hexagonal Architecture
```text
my-platform/
├── go.mod
├── go.sum
├── Makefile
├── docker-compose.yml
├── .golangci.yml
├── cmd/
│ ├── api/
│ │ └── main.go
│ └── worker/
│ └── main.go
├── internal/
│ ├── app/ # Application layer
│ │ ├── command/ # Write operations (CQRS)
│ │ │ ├── create_user.go
│ │ │ └── create_user_test.go
│ │ ├── query/ # Read operations (CQRS)
│ │ │ ├── get_user.go
│ │ │ └── get_user_test.go
│ │ └── dto/
│ │ └── user.go
│ ├── domain/ # Domain layer (core)
│ │ ├── user/
│ │ │ ├── user.go # Aggregate root
│ │ │ ├── user_test.go
│ │ │ ├── repository.go # Repository interface (port)
│ │ │ ├── events.go # Domain events
│ │ │ └── errors.go
│ │ └── order/
│ │ ├── order.go
│ │ └── repository.go
│ ├── infra/ # Infrastructure layer
│ │ ├── http/ # HTTP adapter (input)
│ │ │ ├── server.go
│ │ │ ├── router.go
│ │ │ ├── handler/
│ │ │ │ ├── user_handler.go
│ │ │ │ └── user_handler_test.go
│ │ │ └── middleware/
│ │ │ ├── auth.go
│ │ │ └── cors.go
│ │ ├── grpc/ # gRPC adapter (input)
│ │ │ ├── server.go
│ │ │ └── user_service.go
│ │ ├── postgres/ # PostgreSQL adapter (output)
│ │ │ ├── user_repository.go
│ │ │ ├── user_repository_test.go
│ │ │ └── migrations/
│ │ ├── redis/ # Redis adapter (output)
│ │ │ └── cache.go
│ │ ├── kafka/ # Kafka adapter (output)
│ │ │ ├── producer.go
│ │ │ └── consumer.go
│ │ └── config/
│ │ └── config.go
│ └── pkg/ # Internal shared packages
│ ├── logger/
│ ├── tracer/
│ └── errors/
├── pkg/ # Public packages
│ └── api/
│ └── v1/
│ └── user.pb.go # Generated protobuf
├── api/
│ └── proto/
│ └── user.proto
├── configs/
├── migrations/
├── scripts/
└── test/
```
## Key File Templates
### go.mod
```go
module github.com/yourorg/myapp
go 1.23
require (
github.com/go-chi/chi/v5 v5.1.0
github.com/jackc/pgx/v5 v5.7.2
github.com/redis/go-redis/v9 v9.7.0
github.com/spf13/viper v1.19.0
go.uber.org/zap v1.27.0
github.com/stretchr/testify v1.10.0
)
```
### main.go (cmd/api/main.go)
```go
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/yourorg/myapp/internal/config"
"github.com/yourorg/myapp/internal/handler"
"github.com/yourorg/myapp/internal/platform/database"
"github.com/yourorg/myapp/internal/repository/postgres"
"github.com/yourorg/myapp/internal/service"
)
func main() {
// Setup structured logging
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
// Load configuration
cfg, err := config.Load()
if err != nil {
slog.Error("failed to load config", "error", err)
os.Exit(1)
}
// Initialize database
db, err := database.NewPostgres(cfg.Database)
if err != nil {
slog.Error("failed to connect to database", "error", err)
os.Exit(1)
}
defer db.Close()
// Wire dependencies
userRepo := postgres.NewUserRepository(db)
userService := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userService)
// Setup router
router := handler.NewRouter(userHandler)
// Create server
server := &http.Server{
Addr: cfg.Server.Address,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Graceful shutdown
go func() {
slog.Info("starting server", "addr", cfg.Server.Address)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
slog.Error("server error", "error", err)
os.Exit(1)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
slog.Error("server forced to shutdown", "error", err)
}
slog.Info("server stopped")
}
```
### Domain Entity
```go
// internal/domain/user.go
package domain
import (
"errors"
"regexp"
"time"
"github.com/google/uuid"
)
var (
ErrInvalidEmail = errors.New("invalid email format")
ErrEmptyName = errors.New("name cannot be empty")
ErrUserNotFound = errors.New("user not found")
)
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
// User represents the user domain entity
type User struct {
ID uuid.UUID
Name string
Email string
CreatedAt time.Time
UpdatedAt time.Time
}
// NewUser creates a new user with validation
func NewUser(name, email string) (*User, error) {
if name == "" {
return nil, ErrEmptyName
}
if !emailRegex.MatchString(email) {
return nil, ErrInvalidEmail
}
now := time.Now().UTC()
return &User{
ID: uuid.New(),
Name: name,
Email: email,
CreatedAt: now,
UpdatedAt: now,
}, nil
}
// UpdateName updates the user's name
func (u *User) UpdateName(name string) error {
if name == "" {
return ErrEmptyName
}
u.Name = name
u.UpdatedAt = time.Now().UTC()
return nil
}
```
### Repository Interface (Port)
```go
// internal/domain/user_repository.go
package domain
import "context"
// UserRepository defines the interface for user persistence
// This is the "port" in hexagonal architecture
type UserRepository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id string) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, offset, limit int) ([]*User, error)
}
```
### Repository Implementation (Adapter)
```go
// internal/repository/postgres/user.go
package postgres
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/yourorg/myapp/internal/domain"
)
type UserRepository struct {
db *pgxpool.Pool
}
func NewUserRepository(db *pgxpool.Pool) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(ctx context.Context, user *domain.User) error {
query := `
INSERT INTO users (id, name, email, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5)
`
_, err := r.db.Exec(ctx, query,
user.ID, user.Name, user.Email, user.CreatedAt, user.UpdatedAt)
return err
}
func (r *UserRepository) GetByID(ctx context.Context, id string) (*domain.User, error) {
query := `
SELECT id, name, email, created_at, updated_at
FROM users WHERE id = $1
`
var user domain.User
err := r.db.QueryRow(ctx, query, id).Scan(
&user.ID, &user.Name, &user.Email, &user.CreatedAt, &user.UpdatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, domain.ErrUserNotFound
}
return &user, err
}
func (r *UserRepository) Update(ctx context.Context, user *domain.User) error {
query := `
UPDATE users
SET name = $2, email = $3, updated_at = $4
WHERE id = $1
`
result, err := r.db.Exec(ctx, query,
user.ID, user.Name, user.Email, user.UpdatedAt)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return domain.ErrUserNotFound
}
return nil
}
```
### HTTP Handler
```go
// internal/handler/user_handler.go
package handler
import (
"encoding/json"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/yourorg/myapp/internal/domain"
"github.com/yourorg/myapp/internal/service"
"github.com/yourorg/myapp/pkg/httputil"
)
type UserHandler struct {
service *service.UserService
}
func NewUserHandler(s *service.UserService) *UserHandler {
return &UserHandler{service: s}
}
func (h *UserHandler) Routes() chi.Router {
r := chi.NewRouter()
r.Post("/", h.Create)
r.Get("/{id}", h.GetByID)
r.Put("/{id}", h.Update)
r.Delete("/{id}", h.Delete)
return r
}
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt string `json:"created_at"`
}
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httputil.Error(w, http.StatusBadRequest, "invalid request body")
return
}
user, err := h.service.Create(r.Context(), req.Name, req.Email)
if err != nil {
switch {
case errors.Is(err, domain.ErrInvalidEmail):
httputil.Error(w, http.StatusBadRequest, err.Error())
case errors.Is(err, domain.ErrEmptyName):
httputil.Error(w, http.StatusBadRequest, err.Error())
default:
httputil.Error(w, http.StatusInternalServerError, "internal error")
}
return
}
httputil.JSON(w, http.StatusCreated, toUserResponse(user))
}
func (h *UserHandler) GetByID(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
user, err := h.service.GetByID(r.Context(), id)
if err != nil {
if errors.Is(err, domain.ErrUserNotFound) {
httputil.Error(w, http.StatusNotFound, "user not found")
return
}
httputil.Error(w, http.StatusInternalServerError, "internal error")
return
}
httputil.JSON(w, http.StatusOK, toUserResponse(user))
}
func toUserResponse(u *domain.User) UserResponse {
return UserResponse{
ID: u.ID.String(),
Name: u.Name,
Email: u.Email,
CreatedAt: u.CreatedAt.Format(time.RFC3339),
}
}
```
### Makefile
```makefile
.PHONY: build test lint run migrate docker
# Variables
BINARY_NAME=myapp
MAIN_PATH=./cmd/api
# Build
build:
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(BINARY_NAME) $(MAIN_PATH)
# Run
run:
go run $(MAIN_PATH)
# Test
test:
go test -v -race -cover ./...
test-coverage:
go test -v -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Lint
lint:
golangci-lint run ./...
# Format
fmt:
go fmt ./...
goimports -w .
# Tidy
tidy:
go mod tidy
go mod verify
# Generate
generate:
go generate ./...
# Migrate
migrate-up:
migrate -path migrations -database "$(DATABASE_URL)" up
migrate-down:
migrate -path migrations -database "$(DATABASE_URL)" down 1
migrate-create:
migrate create -ext sql -dir migrations -seq $(name)
# Docker
docker-build:
docker build -t $(BINARY_NAME):latest .
docker-run:
docker-compose up -d
docker-down:
docker-compose down
# All
all: tidy fmt lint test build
```
### .golangci.yml
```yaml
run:
timeout: 5m
go: "1.23"
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
- unconvert
- gocritic
- revive
- gosec
- prealloc
- exportloopref
- nilerr
- bodyclose
- contextcheck
linters-settings:
govet:
enable-all: true
gofmt:
simplify: true
gocritic:
enabled-tags:
- diagnostic
- style
- performance
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: error-return
- name: error-strings
- name: exported
- name: if-return
- name: increment-decrement
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
issues:
exclude-rules:
- path: _test\.go
linters:
- gosec
- gocritic
```
## Project Validation Checklist
### Structure
- [ ] Flat layout for small projects (< 5k LOC)
- [ ] cmd/ for multiple binaries
- [ ] internal/ for private application code
- [ ] pkg/ only for truly reusable public packages
- [ ] No /src directory (this is not Java)
### Naming
- [ ] Package names are short, lowercase, single-word
- [ ] No underscores or mixedCaps in package names
- [ ] File names use underscores (user_handler.go)
- [ ] Test files end with \_test.go
### Dependencies
- [ ] go.mod at repository root
- [ ] Minimal external dependencies
- [ ] No vendor/ unless required for reproducibility
### Testing
- [ ] Tests alongside code (user_test.go next to user.go)
- [ ] Table-driven tests where appropriate
- [ ] testdata/ for test fixtures
- [ ] Integration tests in /test
### Quality
- [ ] golangci-lint configured
- [ ] Makefile for common tasks
- [ ] Dockerfile with multi-stage build
- [ ] CI/CD workflow configured
## Scaffold Commands
```bash
# Initialize module
mkdir my-app && cd my-app
go mod init github.com/yourorg/my-app
# Create standard structure
mkdir -p cmd/api internal/{config,domain,service,handler,repository/postgres,platform/database} pkg/httputil migrations test/integration
# Install dev tools
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install golang.org/x/tools/cmd/goimports@latest
go install github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# Add common dependencies
go get github.com/go-chi/chi/v5
go get github.com/jackc/pgx/v5
go get go.uber.org/zap
go get github.com/spf13/viper
go get github.com/stretchr/testify
# Tidy up
go mod tidy
```
## References
- [golang-standards/project-layout](https://github.com/golang-standards/project-layout)
- [Official Go Module Layout](https://go.dev/doc/modules/layout)
- [Go Project Structure 2025](https://www.glukhov.org/post/2025/12/go-project-structure/)
- [11 Tips for Structuring Go Projects](https://www.alexedwards.net/blog/11-tips-for-structuring-your-go-projects)
- [Effective Go](https://go.dev/doc/effective_go)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!