Forum - AI Coding Agent Instructions
Defines a hexagonal architecture with strict 4-directory modules, a ServiceContainer DI pattern, and a 6-step feature workflow for a Go forum app.
What this file does
Defines a hexagonal architecture with strict 4-directory modules, a ServiceContainer DI pattern, and a 6-step feature workflow for a Go forum app.
When to use it
- Starting a new Go monorepo with modular hexagonal architecture
- Adding a new feature module to an existing hexagonal Go project
- Onboarding a developer to a Go project that uses ports and adapters
- Reviewing or enforcing dependency rules in a Go codebase
Assumes this stack
Forum - AI Coding Agent Instructions
Quick Reference
Architecture: Modular Monolith with Hexagonal Architecture (Ports & Adapters)
Language: Go 1.24+ with minimal dependencies (SQLite driver, bcrypt, UUID only)
Status: 75% Complete - Auth, Posts, Categories fully implemented with tests
Entry Point: cmd/forum/main.go → cmd/forum/wire/ for all DI wiring
Critical Files: docs/ARCHITECTURE.md, docs/IMPLEMENTATION_ROADMAP.md, docs/UNIFIED_DI_PATTERN.md
Architecture Pattern: The Hexagon
Each module follows strict 4-directory structure:
internal/modules/{module}/
├── domain/ # Pure business logic (stdlib only)
│ ├── {entity}.go # Domain entities with Validate()
│ └── errors.go # Domain-specific errors (errors.New())
├── ports/ # Interface definitions (contracts)
│ ├── service.go # INPUT PORT - Use case interface
│ └── repository.go # OUTPUT PORT - Data access interface
├── application/ # Business logic orchestration
│ └── service.go # Implements ports interfaces
└── adapters/ # Technical implementations (FLAT - no subdirs)
├── http_handler.go # INPUT ADAPTER - HTTP handlers
└── sqlite_repository.go # OUTPUT ADAPTER - Database access
Port/Adapter File Headers (MANDATORY)
Every file in ports/ and adapters/ MUST start with:
// INPUT PORT - Service Interface// OUTPUT PORT - Repository Interface// INPUT ADAPTER - HTTP Handler// OUTPUT ADAPTER - SQLite Repository
Example: internal/modules/auth/ports/service.go starts with // INPUT PORT - Service Interface
Dependency Rules (Enforced)
domain → stdlib ONLY (no imports from project)
ports → domain only
application → domain, ports
adapters → domain, ports (never application!)
Cross-module: Import ports.XService interface ONLY. Never import application/ or adapters/ from other modules.
Critical Workflow: Adding Features (Follow Exactly)
1. Domain Layer First
// internal/modules/{module}/domain/{entity}.go
type Post struct {
ID string
Title string
Content string
CreatedAt time.Time
}
func (p *Post) Validate() error {
if p.Title == "" { return domain.ErrEmptyTitle }
return nil
}
// internal/modules/{module}/domain/errors.go
var ErrEmptyTitle = errors.New("title cannot be empty")
2. Define Ports (Interfaces)
// internal/modules/{module}/ports/service.go (INPUT PORT)
type PostService interface {
CreatePost(ctx context.Context, userID, title, content string) (*domain.Post, error)
}
// internal/modules/{module}/ports/repository.go (OUTPUT PORT)
type PostRepository interface {
Create(ctx context.Context, post *domain.Post) error
}
3. Implement Application Service
// internal/modules/{module}/application/service.go
type Service struct {
repo ports.PostRepository
}
func (s *Service) CreatePost(ctx context.Context, userID, title, content string) (*domain.Post, error) {
post := &domain.Post{ID: generateID(), Title: title, Content: content}
if err := post.Validate(); err != nil { return nil, err }
return post, s.repo.Create(ctx, post)
}
4. Implement Adapters
// internal/modules/{module}/adapters/sqlite_repository.go (OUTPUT ADAPTER)
type SQLiteRepository struct { db *sql.DB }
func (r *SQLiteRepository) Create(ctx context.Context, post *domain.Post) error {
_, err := r.db.ExecContext(ctx, "INSERT INTO posts...", post.ID, post.Title)
return err
}
// internal/modules/{module}/adapters/http_handler.go (INPUT ADAPTER)
type HTTPHandler struct { service ports.PostService }
func (h *HTTPHandler) CreatePostAPI(w http.ResponseWriter, r *http.Request) {
// Parse request, call h.service.CreatePost(), write JSON response
}
5. Wire in cmd/forum/wire/ (CRITICAL - Don't Skip!)
// cmd/forum/wire/repositories.go
repos.Post = postAdapters.NewSQLitePostRepository(db)
// cmd/forum/wire/services.go (ServiceContainer already exists)
post: postApp.NewService(repos.Post, repos.Category),
// cmd/forum/wire/handlers.go
postHandler := postAdapters.NewHTTPHandler(services, templates)
// cmd/forum/wire/app.go (in initServer)
postHandler.RegisterRoutes(server.Router())
6. Database Migration (if schema changes)
-- migrations/NNN_{module}_{description}.sql
-- +migrate Up
CREATE TABLE posts (id TEXT PRIMARY KEY, title TEXT NOT NULL);
CREATE INDEX idx_posts_user_id ON posts(user_id);
-- +migrate Down
DROP INDEX idx_posts_user_id;
DROP TABLE posts;
Migrations run automatically on startup via cmd/forum/wire/app.go:initDatabase().
Unified Dependency Injection Pattern (CRITICAL)
Every handler uses identical ServiceContainer pattern:
// cmd/forum/wire/services.go - ONE concrete container with ALL services
type ServiceContainer struct {
auth authPorts.AuthService // Private fields
user userPorts.UserService
post postPorts.PostService
}
func (sc *ServiceContainer) Auth() authPorts.AuthService { return sc.auth }
func (sc *ServiceContainer) User() userPorts.UserService { return sc.user }
// Each handler constructor: IDENTICAL signature across ALL handlers
func NewHTTPHandler(services ServiceContainer, templates *template.Template) *HTTPHandler
// Each handler declares minimal local interface (only what it needs)
type ServiceContainer interface {
Post() postPorts.PostService
Auth() authPorts.AuthService // Only if needed for auth checks
}
Benefits: Consistent signatures, explicit dependencies, easy mocking, no circular imports.
Reference: docs/UNIFIED_DI_PATTERN.md, cmd/forum/wire/services.go
Handler Naming Convention
- API handlers (JSON): End with
API→RegisterAPI,LoginAPI,CreatePostAPI - Page handlers (HTML): End with
Page→LoginPage,RegisterPage,PostDetailPage
Group in code: API handlers first, then Page handlers.
Error Handling (Two Layers)
// 1. Domain errors (module-level, simple)
// internal/modules/auth/domain/errors.go
var ErrSessionExpired = errors.New("session has expired")
// 2. Platform errors (structured, HTTP-aware)
// internal/platform/errors/errors.go
return errors.Wrap(err, errors.ErrCodeInternal, "failed to create session")
HTTP handlers map errors to status codes via errors.HTTPStatus():
- 200 OK, 201 Created, 204 No Content
- 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
- 409 Conflict (duplicate email/username)
- 500 Internal Server Error
Always return JSON: {error: "message"} for errors.
Testing Strategy
Run tests: make test (runs scripts/tests/run_all_tests.sh)
Coverage: make test-coverage (generates coverage.html)
tests/
├── unit/ # Business logic, isolated mocks
│ ├── auth_test.go # Domain validation, service logic
│ └── template_*_test.go # Template rendering tests
├── integration/ # Full request/response cycles
├── auth_test.go # Register, login, session flows
└── post_test.go # CRUD operations with real DB
TDD Workflow:
- Write failing test →
go test ./internal/modules/auth/... -run TestRegister - Implement minimal code to pass
- Refactor while keeping tests green
- Commit
Audit compliance: ALL scenarios from docs/requirements/audit.md MUST have integration tests. DO NOT modify audit.md.
Build & Run
Local development:
make go # Runs with `go run cmd/forum/main.go`
make build # Builds binary to bin/forum
make run # Builds then runs binary
Docker:
docker-compose up # Starts forum + database
make docker-build # Builds Docker image
Important: SQLite requires CGO_ENABLED=1 (set in Makefile).
Configuration & Logging
Config: internal/platform/config/config.go - loads from env vars with defaults
Logger: internal/platform/logger/ - structured logging (Debug, Info, Warn, Error)
lgr.Info("Starting service", logger.String("module", "auth"))
lgr.Error("Failed operation", logger.Error(err), logger.String("user_id", uid))
Middleware order (in cmd/forum/wire/app.go):
- Recovery (panic handling) - TODO: Not yet implemented
- Logger (request logging) - TODO: Not yet implemented
- CORS
- RateLimit
Key Files Reference
- Entry point:
cmd/forum/main.go- Minimal lifecycle (config → wire → start → shutdown) - DI wiring:
cmd/forum/wire/{app,repositories,services,handlers}.go- All dependency injection - Architecture:
docs/ARCHITECTURE.md- Design rationale, dependency rules - Roadmap:
docs/IMPLEMENTATION_ROADMAP.md- Progress tracking (75% complete) - DI pattern:
docs/UNIFIED_DI_PATTERN.md- ServiceContainer details - Auth reference:
internal/modules/auth/- Fully implemented example (ports → app → adapters) - Audit spec:
docs/requirements/audit.md- Authoritative test scenarios (DO NOT modify) - Logger plan:
docs/log/UNIFIED_LOGGER_IMPROVEMENT_PLAN.md- Middleware implementation TODO
Current Implementation Status (75% Complete)
✅ Fully Implemented:
- Auth module (register, login, sessions, validation, middleware)
- Post module (CRUD with categories, validation)
- Category module (CRUD operations)
- Platform layer (config, database, logger, httpserver, errors, validator)
- Database migrations (auto-apply on startup)
- Unit + integration tests for auth and posts
⚠️ Placeholder/TODO (look for // TODO: comments):
- Comment module (scaffolded, not implemented)
- Reaction module (scaffolded, not implemented)
- User module HTTP handlers (domain + repo done, handlers TODO)
- Moderation module (optional, scaffolded)
- Notification module (optional, scaffolded)
- Recovery & Logger middleware (
internal/platform/httpserver/middleware.go)
When implementing: Replace // TODO: placeholders with actual logic. Follow the hexagonal pattern exactly as shown in auth/post modules.
Development Workflow
Adding features:
- Check
docs/IMPLEMENTATION_ROADMAP.mdfor current priorities - Follow the 6-step workflow (Domain → Ports → Application → Adapters → Wire → Migration)
- Use
internal/modules/auth/as reference implementation - Update roadmap checkboxes as you complete tasks
Common commands:
make go # Run locally (go run cmd/forum/main.go)
make test # Full test suite
make test-coverage # Generate coverage.html
make fmt # Format code
make vet # Static analysis
Testing workflow:
# Run specific module tests
go test ./internal/modules/auth/... -v
# Run with race detector
go test -race ./...
# Integration tests only
go test ./tests/integration/... -v
Git commits:
- Format:
[module] Brief description - Example:
[auth] Implement session validation,[post] Add category filtering - Commit after each complete feature/checklist item
Common Pitfalls
❌ Don't do:
- Add subdirectories to
adapters/(keep flat: http_handler.go, sqlite_repository.go) - Skip port/adapter file header comments (
// INPUT PORT - Service Interface, etc.) - Import
application/from adapters (adapters → ports only) - Use external frameworks unnecessarily (prefer stdlib)
- Modify
docs/requirements/{audit,requirements,morefeats}.md(authoritative specs) - Hard-code config values (use
config.Configstructs)
✅ Do:
- Follow exact 4-directory structure for every module
- Declare handler-specific ServiceContainer interfaces
- Use domain errors in modules, platform errors for HTTP
- Write tests before/during implementation (TDD)
- Check existing modules (auth, post) for patterns
- Mark optional features with
[OPTIONAL FEATURE]comments
Quick Patterns
Domain validation:
func (p *Post) Validate() error {
if p.Title == "" { return domain.ErrEmptyTitle }
if len(p.Title) > 300 { return domain.ErrTitleTooLong }
if len(p.Categories) == 0 { return domain.ErrNoCategoriesSelected }
return nil
}
Service implementation:
func (s *Service) CreatePost(ctx context.Context, userID, title, content string, categories []string) (*domain.Post, error) {
post := &domain.Post{ID: generateID(), UserID: userID, Title: title, Content: content, Categories: categories}
if err := post.Validate(); err != nil { return nil, err }
return post, s.postRepo.Create(ctx, post)
}
HTTP handler pattern:
func (h *HTTPHandler) CreatePostAPI(w http.ResponseWriter, r *http.Request) {
// 1. Parse & validate input
// 2. Extract user from session (if auth required)
// 3. Call service method
// 4. Handle errors with appropriate status codes
// 5. Return JSON response
}
Repository pattern:
func (r *SQLiteRepository) Create(ctx context.Context, post *domain.Post) error {
tx, _ := r.db.BeginTx(ctx, nil)
defer tx.Rollback()
_, err := tx.ExecContext(ctx, "INSERT INTO posts (id, title, content, user_id) VALUES (?, ?, ?, ?)", ...)
return tx.Commit()
}
Project Structure Summary
forum/
├── cmd/forum/
│ ├── main.go # Entry point (config → wire → run)
│ └── wire/ # Dependency injection (repos → services → handlers)
├── internal/
│ ├── modules/ # Business modules (hexagonal structure)
│ │ ├── auth/ # ✅ Auth (fully implemented)
│ │ ├── post/ # ✅ Posts (fully implemented)
│ │ ├── comment/ # ⚠️ TODO: Implement
│ │ └── reaction/ # ⚠️ TODO: Implement
│ └── platform/ # ✅ Technical infrastructure
│ ├── config/ # Env-based config
│ ├── database/ # SQLite connection + migrator
│ ├── errors/ # Structured error types
│ ├── httpserver/ # HTTP server + middleware
│ ├── logger/ # Structured logging
│ └── validator/ # Input validation
├── migrations/ # SQL migrations (auto-applied)
├── templates/ # HTML templates (Go html/template)
├── static/ # CSS, JS, uploads
├── tests/
│ ├── unit/ # Business logic tests
│ └── integration/ # Full HTTP cycle tests
└── docs/
├── ARCHITECTURE.md # Design decisions
├── IMPLEMENTATION_ROADMAP.md # Progress tracking
└── requirements/ # Specs (DO NOT modify)
What's inside
14 sections covering architecture, workflow, DI, naming, error handling, testing, build, config, and project structure
Change this for your project
- Replace
forumin paths likecmd/forum/main.gowith your own project name - Replace
ertval/forumin repository references with your own repo URL - Replace
docs/IMPLEMENTATION_ROADMAP.mdwith your own roadmap file path - Replace
internal/modules/auth/with your own reference module path
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- ServiceContainer pattern with private fields and getter methods for explicit dependency injection
- Handler-specific minimal interfaces that declare only the services they need
- Flat adapters directory (no subdirectories) to keep implementations discoverable
Related Documents
System Prompt
Defines the identity, capabilities, response style, and spec workflow for an AI assistant named Kiro that helps developers build features incrementally.
World Engine AI Coding Instructions
Defines game scope, architecture, ESLint rules, git workflow, and open-source research policy for a strategic fantasy RPG project.
GitHub Copilot Instructions
Defines coding standards, architecture rules, PR expectations, and output format for a Laravel + Inertia + Vue 3 project.
Business Intelligence Dashboard - Comprehensive GitHub Copilot Instructions
Defines the full stack, setup commands, security rules, and testing strategy for a Next.js SaaS analytics platform with multi-tenant isolation.