Go cloud platform with hexagonal architecture. Frontend: Next.js 14 + TailwindCSS.
CRITICAL: When developing a new feature, always create a new branch and commit there often.
git checkout -b feature/<feature-name>
git commit -m "feat: description" # Commit frequently!Handlers → Services → Ports ← Repositories
↓
Domain
- Handlers (
internal/handlers/) import Services only - Services (
internal/core/services/) import Ports and Domain only - Repositories (
internal/repositories/) implement Ports
- ❌ Put business logic in handlers
- ❌ Import repositories directly in handlers
- ❌ Use global variables (
var DB *sql.DB) - ❌ Panic in production code - return errors
- ❌ Silent failures (
_ = someFunc()) - ❌ Magic numbers - use constants
- ❌ Skip
context.Contextas first parameter - ❌ Create circular dependencies between packages
- ❌ Skip tests for new services
- ❌ Test with real external dependencies in unit tests
- ❌ Commit directly to
mainbranch - ❌ Large, infrequent commits
- ❌ Commit binaries or coverage files
- ✅ Use constructor injection for dependencies
- ✅ Propagate
context.Contextto all blocking calls - ✅ Use
internal/errorspackage for domain errors - ✅ Follow existing patterns in the codebase
- Domain:
internal/core/domain/<name>.go - Ports:
internal/core/ports/<name>.go - Service:
internal/core/services/<name>.go - Repository:
internal/repositories/postgres/<name>.go - Handler:
internal/handlers/<name>_handler.go - Wire up in
cmd/api/main.go
- ✅ Table-driven tests
- ✅ Mock repositories in service tests
- ✅ Use
testify/mockfor mocks
make run # Start services
make test # Run all tests
make build # Build binaries
make swagger # Generate API docs- Handlers:
*_handler.go - Tests:
*_test.go - Domain: singular (
instance.go,user.go)
| Element | Style | Example |
|---|---|---|
| Types/Structs | PascalCase | InstanceService, LaunchRequest |
| Interfaces | PascalCase | InstanceRepository, ComputeBackend |
| Functions | PascalCase (exported), camelCase (private) | LaunchInstance, parsePort |
| Constants | SCREAMING_SNAKE (status), PascalCase (limits) | StatusRunning, MaxPortsPerInstance |
| Variables | camelCase | instanceRepo, vpcID |
// Always include json tags with omitempty for optional fields
type Instance struct {
ID uuid.UUID `json:"id"`
VpcID *uuid.UUID `json:"vpc_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
}// Service layer - use internal/errors
if user == nil {
return nil, errors.New(errors.NotFound, "user not found")
}
return nil, errors.Wrap(errors.Internal, "failed to create", err)
// Handler layer - use httputil
httputil.Error(c, err) // Maps to HTTP status automatically
httputil.Success(c, http.StatusOK, data)// @Summary Short description
// @Tags resourceName
// @Security APIKeyAuth
// @Router /path [method]
func (h *Handler) Method(c *gin.Context) {
var req Request
if err := c.ShouldBindJSON(&req); err != nil {
httputil.Error(c, errors.New(errors.InvalidInput, "invalid request"))
return
}
result, err := h.svc.Method(c.Request.Context(), req)
if err != nil {
httputil.Error(c, err)
return
}
httputil.Success(c, http.StatusOK, result)
}// Use params struct for 3+ dependencies
type ServiceParams struct {
Repo ports.Repository
EventSvc ports.EventService
Logger *slog.Logger
}
func NewService(params ServiceParams) *Service {
return &Service{
repo: params.Repo,
eventSvc: params.EventSvc,
logger: params.Logger,
}
}// Repository interfaces - CRUD operations with context
type Repository interface {
Create(ctx context.Context, entity *domain.Entity) error
GetByID(ctx context.Context, id uuid.UUID) (*domain.Entity, error)
Update(ctx context.Context, entity *domain.Entity) error
Delete(ctx context.Context, id uuid.UUID) error
}
// Service interfaces - business operations
type Service interface {
CreateEntity(ctx context.Context, name string) (*domain.Entity, error)
ListEntities(ctx context.Context) ([]*domain.Entity, error)
}