golang-architecture

Structure Go applications with standard project layout. Interfaces defined by consumers. Dependencies flow inward. Internal packages enforced by compiler.

j4flmao/agent-skills2 installsMITSynced Aug 26

Tech stack

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI

Agent Skills format with YAML frontmatter. Claude Code reads it as-is.

---
name: "golang-architecture"
description: "Structure Go applications with standard project layout. Interfaces defined by consumers. Dependencies flow inward. Internal packages enforced by compiler."
license: "MIT"
---

# Golang Architecture

## Purpose
Structure Go applications with standard project layout. Interfaces defined by consumers. Dependencies flow inward. Internal packages enforced by compiler.

## Agent Protocol

### Trigger
Exact user phrases: "Go project structure", "Golang architecture", "Go package layout", "Go clean arch", "Go folder structure", "Go module design", "Go interface design", "Go project layout".

### Input Context
Before activating, verify:
- go.mod exists at project root.
- The module name is known.

### Output Artifact
No file output. Produces folder structure and code examples as text.

### Response Format
Folder structure:
```
{project}/
  cmd/{service}/main.go
  internal/
    domain/
    application/
    infrastructure/
    config/
  pkg/
  api/
```

Code: show relevant package and types only. No imports lines.

No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.

### Completion Criteria
- [ ] cmd/ contains only entry points (parse flags, build deps, start server).
- [ ] internal/ contains all application code (compiler-enforced).
- [ ] Domain has zero imports from infrastructure.
- [ ] Interfaces defined by consumers (domain/application), not implementers (infrastructure).
- [ ] Package names are short, lowercase, no underscores.
- [ ] No init() functions used.
- [ ] context.Context is first parameter in all I/O functions.

### Max Response Length
Folder structure: unlimited. Code: 15 lines per example.

## Architecture Decision Trees

### Project Layout: Standard vs Flat vs Hexagonal

| Criterion | Standard (cmd/internal/pkg) | Flat | Hexagonal (ports/adapters) |
|-----------|----------------------------|------|---------------------------|
| Package count | 10+ packages | 1-5 packages | 15+ packages |
| Dependency clarity | Internal enforces boundaries | Manual discipline | Explicit port/adapter |
| Team size | 3+ developers | 1-2 developers | 5+ developers |
| Testing | Unit per package, integration per infra | Package-level | Port-mockable |
| Build time | Slightly slower (many packages) | Fastest | Slower |

Decision: Multi-developer project with clear boundaries → Standard. Small tool/CLI → Flat. Large system with multiple adapters → Hexagonal.

### Interface Definition Placement

| Approach | Consumer | Implementer | Best For |
|----------|----------|-------------|----------|
| Consumer-side | Domain defines repo interface | Infrastructure implements | Clean Architecture |
| Implementer-side | Infrastructure exposes interface | Inferface next to impl | Simple CRUD, libraries |
| Shared | Separate `domain` package | Separate `impl` package | Large microservices |

Decision: Business logic depends on interface → Consumer-side. Library/external-facade → Implementer-side.

## Workflow

### Step 1: Create Standard Layout
```
cmd/
  server/
    main.go                    -- Entry point. Parse flags, build deps, start server. No logic.
internal/
  domain/
    entity.go                  -- Domain entities
    repository.go              -- Repository interfaces (ports)
    service.go                 -- Domain services
  application/
    usecase.go                 -- Use case interfaces + implementations
    dto.go                     -- Data transfer objects
  infrastructure/
    postgres/
      repository.go            -- Repository implementations
    http/
      handler.go               -- HTTP handlers
  config/
    config.go                  -- Configuration
pkg/                           -- Shared libraries (importable by external modules)
api/                           -- API definitions (OpenAPI, protobuf)
migrations/
```

### Step 2: Define Interfaces at Consumer Side
```go
// internal/domain/repository.go -- Interface defined by domain
type UserRepository interface {
  FindByID(ctx context.Context, id uuid.UUID) (*User, error)
  Save(ctx context.Context, user *User) error
  FindByEmail(ctx context.Context, email string) (*User, error)
  List(ctx context.Context, offset, limit int) ([]User, int, error)
}

// internal/domain/service.go -- Domain service
type UserService struct {
  repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
  return &UserService{repo: repo}
}

func (s *UserService) Register(ctx context.Context, email, name string) (*User, error) {
  existing, _ := s.repo.FindByEmail(ctx, email)
  if existing != nil {
    return nil, fmt.Errorf("register user: %w", ErrEmailAlreadyExists)
  }
  user := User{ID: uuid.New(), Email: email, Name: name, Active: true}
  if err := s.repo.Save(ctx, &user); err != nil {
    return nil, fmt.Errorf("register user: %w", err)
  }
  return &user, nil
}

// internal/infrastructure/postgres/repository.go -- Implements domain interface
type PostgresUserRepository struct {
  db *sql.DB
}

func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
  return &PostgresUserRepository{db: db}
}

func (r *PostgresUserRepository) FindByID(ctx context.Context, id uuid.UUID) (*User, error) {
  row := r.db.QueryRowContext(ctx, "SELECT id, email, name, active, created_at FROM users WHERE id = $1", id)
  user := &User{}
  err := row.Scan(&user.ID, &user.Email, &user.Name, &user.Active, &user.CreatedAt)
  if errors.Is(err, sql.ErrNoRows) {
    return nil, fmt.Errorf("find user by id: %w", ErrUserNotFound)
  }
  return user, err
}
```

### Step 3: Wire Dependencies in main.go
```go
func main() {
  cfg := config.Load()
  db := connectDB(cfg.DatabaseURL)
  defer db.Close()

  userRepo := postgres.NewUserRepository(db)
  userService := domain.NewUserService(userRepo)
  createUserUC := application.NewCreateUserUseCase(userService)
  handler := http.NewUserHandler(createUserUC)

  server := &http.Server{
    Addr:    ":" + cfg.Port,
    Handler: handler,
  }

  // Graceful shutdown
  ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  defer stop()
  go func() {
    if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
      log.Fatalf("server error: %v", err)
    }
  }()
  <-ctx.Done()
  shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  defer cancel()
  server.Shutdown(shutdownCtx)
}
```

### Step 4: Package Naming Rules
- Short, lowercase, no underscores: user, order, payment.
- Single-word names preferred. If multi-word: userrepo not user_repository.
- No utility packages named utils/ or common/. Find a specific name.
- internal/ packages cannot be imported by external modules (enforced by Go compiler).
- Package name matches directory name — no exceptions.

### Step 5: Error Handling Architecture
```go
// internal/domain/errors.go -- sentinel errors
var (
  ErrUserNotFound      = errors.New("user not found")
  ErrEmailAlreadyExists = errors.New("email already exists")
  ErrValidation         = errors.New("validation error")
)

// internal/application -- wrap with context
func (uc *CreateUserUseCase) Execute(ctx context.Context, dto CreateUserDTO) (*UserDTO, error) {
  if err := dto.Validate(); err != nil {
    return nil, fmt.Errorf("create user: %w", domain.ErrValidation)
  }
  user, err := uc.userService.Register(ctx, dto.Email, dto.Name)
  if err != nil {
    return nil, fmt.Errorf("create user: %w", err)
  }
  return toDTO(user), nil
}

// internal/infrastructure/http -- map to HTTP
func errorResponse(w http.ResponseWriter, err error) {
  switch {
  case errors.Is(err, domain.ErrUserNotFound):
    writeJSON(w, http.StatusNotFound, ErrorResponse{Code: "NOT_FOUND", Message: err.Error()})
  case errors.Is(err, domain.ErrEmailAlreadyExists):
    writeJSON(w, http.StatusConflict, ErrorResponse{Code: "CONFLICT", Message: err.Error()})
  case errors.Is(err, domain.ErrValidation):
    writeJSON(w, http.StatusBadRequest, ErrorResponse{Code: "VALIDATION_ERROR", Message: err.Error()})
  default:
    log.Printf("unhandled error: %v", err)
    writeJSON(w, http.StatusInternalServerError, ErrorResponse{Code: "INTERNAL", Message: "An error occurred"})
  }
}
```

## Implementation Patterns

### Pattern: Config with envconfig

```go
// internal/config/config.go
type Config struct {
  Port        string `envconfig:"PORT" default:"8080"`
  DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
  LogLevel    string `envconfig:"LOG_LEVEL" default:"info"`
  JWTSecret   string `envconfig:"JWT_SECRET" required:"true"`
  RedisURL    string `envconfig:"REDIS_URL"`
}

func Load() (*Config, error) {
  var cfg Config
  if err := envconfig.Process("", &cfg); err != nil {
    return nil, fmt.Errorf("load config: %w", err)
  }
  return &cfg, nil
}
```

### Pattern: Health Check Endpoint

```go
// internal/infrastructure/http/health.go
type HealthHandler struct {
	db *sql.DB
}

func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if err := h.db.PingContext(r.Context()); err != nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"status": "unhealthy", "error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"status": "healthy"})
}
```

### Pattern: Repository with Transaction Support

```go
// internal/infrastructure/postgres/transaction.go
type TransactionKey struct{}
type Transaction struct{ *sql.Tx }

func WithTransaction(ctx context.Context, db *sql.DB, fn func(ctx context.Context) error) error {
	tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
	if err != nil {
		return fmt.Errorf("begin tx: %w", err)
	}
	ctx = context.WithValue(ctx, TransactionKey{}, &Transaction{tx})
	if err := fn(ctx); err != nil {
		if rbErr := tx.Rollback(); rbErr != nil {
			return fmt.Errorf("rollback: %v (orig: %w)", rbErr, err)
		}
		return err
	}
	return tx.Commit()
}

func GetQuerier(ctx context.Context) Querier {
	if tx, ok := ctx.Value(TransactionKey{}).(*Transaction); ok {
		return tx
	}
	return getDB(ctx) // fallback to direct DB
}

// Usage
func (s *UserService) CreateUser(ctx context.Context, user *User) error {
	return postgres.WithTransaction(ctx, s.db, func(ctx context.Context) error {
		q := postgres.GetQuerier(ctx)
		if err := q.InsertUser(ctx, user); err != nil {
			return fmt.Errorf("insert user: %w", err)
		}
		return q.InsertAuditLog(ctx, user.ID, "created")
	})
}
```

### Pattern: Structured Logging Middleware

```go
// internal/infrastructure/http/middleware.go
type responseWriter struct {
	http.ResponseWriter
	statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
	rw.statusCode = code
	rw.ResponseWriter.WriteHeader(code)
}

func LoggingMiddleware(log *slog.Logger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			start := time.Now()
			rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
			next.ServeHTTP(rw, r)
			log.InfoContext(r.Context(), "HTTP request",
				"method", r.Method,
				"path", r.URL.Path,
				"status", rw.statusCode,
				"duration", time.Since(start).String(),
				"ip", r.RemoteAddr,
			)
		})
	}
}
```

## Production Considerations

### Graceful Shutdown Checklist
- Close database connections
- Flush pending logs
- Complete in-flight requests (shutdownCtx timeout)
- Close message queue consumers
- Release file locks

### Performance
- Use `pgx` over `database/sql` for PostgreSQL (faster, connection pooling built-in)
- Prefer `sync.Pool` for frequently allocated objects
- Profile with `pprof` — import `net/http/pprof` behind a build tag
- Connection pool settings: `SetMaxOpenConns(25)`, `SetMaxIdleConns(10)`, `SetConnMaxLifetime(5*time.Minute)`

## Anti-Patterns

| Anti-Pattern | Why | Fix |
|-------------|-----|-----|
| `init()` functions | Unclear execution order, hard to test | Explicit initialization in `main()` |
| `utils`/`common` packages | Dependency magnet, unclear purpose | Specific package names |
| Package names != dir name | Confusing imports | Match package name to directory |
| Interface on implementer side | Extra indirection, violates DI principle | Interface at consumer side |
| `context.Background` in libs | Never cancelled | Accept `context.Context` from caller |
| Global variables | Hidden dependencies, race conditions | Pass via struct fields or parameters |

## Security Considerations
- Validate all inputs at HTTP boundary before passing to domain
- Use `bcrypt` for passwords, never SHA/MD5
- SQL injection: always parameterized queries, never string concatenation
- JWT: use `golang-jwt/jwt/v5`, validate `alg` header to avoid alg confusion
- CORS: use `rs/cors` middleware with explicit origins
- Rate limiting: use `ulule/limiter` or middleware
- Secrets: environment variables or vault, never committed to repo

## Testing Strategies

### Unit Tests
```go
func TestUserService_Register_DuplicateEmail(t *testing.T) {
  mockRepo := new(MockUserRepository)
  mockRepo.On("FindByEmail", mock.Anything, "test@test.com").
    Return(&User{Email: "test@test.com"}, nil)
  svc := domain.NewUserService(mockRepo)
  _, err := svc.Register(context.Background(), "test@test.com", "Test")
  assert.ErrorIs(t, err, domain.ErrEmailAlreadyExists)
}
```

### Integration Tests
Use `testcontainers-go` for PostgreSQL. Each test creates its own DB. Use `txdb` for transaction-based test isolation. Run with `go test -tags=integration ./...`

### Architecture Tests
```go
func TestDomainDoesNotImportInfrastructure(t *testing.T) {
  domainPkg := "github.com/org/project/internal/domain"
  infraPkg := "github.com/org/project/internal/infrastructure"
  imports, err := pkgimports.List(domainPkg)
  require.NoError(t, err)
  assert.False(t, imports.Contains(infraPkg), "domain must not import infrastructure")
}
```

## Rules
- internal/ is the default location for ALL application code. pkg/ is for libraries designed for external consumption only.
- Interfaces are defined by the consumer (domain/application), not by the implementer (infrastructure). This is the most important Go rule.
- Accept interfaces, return concrete types.
- context.Context is the FIRST parameter in every function that does I/O (database, HTTP, file system).
- No naked error strings. Always wrap: fmt.Errorf("context: %w", err).
- No init() functions. Use explicit initialization in main().
- Package names are part of the import path. A package named "userrepo" is imported as "project/internal/infrastructure/userrepo".
- Wire dependencies in main.go (or a dedicated wire.go for Wire DI). Never use `sync.Once` to lazily initialize.

## References
  - references/go-concurrency.md — Go Concurrency
  - references/go-graceful-shutdown.md — Graceful Shutdown Patterns in Go
  - references/go-hexagonal.md — Hexagonal Architecture in Go
  - references/go-testing.md — Go Testing
  - references/interface-design.md — Go Interface Design
  - references/project-layout.md — Go Project Layout
## Handoff
No artifact produced.
Next skill: golang-patterns — concurrency, HTTP servers, error handling.
Carry forward: package structure, interface definitions, DI wiring approach.
## Implementation Patterns

### Factory Pattern for Module Creation
`
function createModule<T>(config: ModuleConfig): T {
  const dependencies = initializeDependencies(config);
  const module = new Module(dependencies);
  module.hooks.onInit();
  return module as T;
}
`

### Builder Pattern for Complex Configuration
`
class ConfigBuilder {
  private config: AppConfig = new AppConfig();
  withDatabase(url: string): ConfigBuilder { ... }
  withCache(ttl: number): ConfigBuilder { ... }
  withLogging(level: string): ConfigBuilder { ... }
  build(): AppConfig { return this.config; }
}
`

## Production Considerations

### Deployment Checklist
- [ ] Production build with optimizations enabled
- [ ] Environment variables configured per environment
- [ ] Health check endpoint responds correctly
- [ ] Error tracking and monitoring integrated
- [ ] Logging level configured (not debug in production)
- [ ] Resource limits configured
- [ ] Database migrations applied
- [ ] Static assets built and served from CDN or cache
- [ ] Feature flags toggled appropriately
- [ ] Rollback plan documented and tested

### Monitoring and Alerting
| Metric | Threshold | Severity | Action |
|--------|-----------|----------|--------|
| Error rate | > 1% | Critical | Rollback or fix |
| p95 latency | > 500ms | Warning | Profile and optimize |
| Uptime | < 99.9% | Critical | Investigate infrastructure |
| Memory usage | > 80% | Warning | Check for leaks |
| CPU usage | > 80% | Warning | Scale up or optimize |

## Rules
- Prefer composition over inheritance
- Favor immutable data structures
- Use dependency injection for testability
- Keep functions pure when possible — no side effects
- Fail fast with clear error messages
- Don't repeat yourself (DRY) — extract shared logic
- Keep it simple (KISS) — avoid unnecessary complexity
- You aren't gonna need it (YAGNI) — build what's required
- Separate concerns — single responsibility per module
- Code to interfaces, not implementations
- Write self-documenting code — clear names over comments
- Prefer standard library over third-party dependencies
- Handle errors explicitly — no silent failures
- Validate inputs at boundaries
- Log at appropriate levels (debug, info, warn, error)

More General & Other skills

← All General & Other skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY