| name | go-maintainable-code |
| description | Write clean, maintainable Go code following Clean Architecture, dependency injection, and ChecklistApplication patterns. Use when writing new Go code, refactoring, or implementing features. |
| allowed-tools | Read, Write, Edit, Grep, Glob, Bash |
Go Maintainable Code Skill
This skill ensures all Go code follows Clean Architecture principles and project-specific patterns used in ChecklistApplication.
Core Principles
1. Clean Architecture Layers (CRITICAL)
Dependency Flow: server → service → repository
internal/
├── server/ # HTTP layer (Gin, OpenAPI controllers)
│ └── Depends on: service (via interfaces)
├── core/
│ ├── service/ # Business logic (framework-independent)
│ │ └── Depends on: repository interfaces, domain
│ ├── domain/ # Entities, value objects (no dependencies)
│ └── repository/ # Repository interfaces (no implementation)
└── repository/ # PostgreSQL implementations
└── Depends on: repository interfaces, domain
Rules:
- ✅ Server calls service interfaces
- ✅ Service calls repository interfaces
- ✅ Domain has NO external dependencies
- ❌ NEVER import concrete types across layers
- ❌ NEVER import
internal/repository from internal/core/service
2. Interface-Based Design
Pattern from codebase:
package repository
type IChecklistService interface {
DeleteChecklistById(ctx context.Context, id uint) domain.Error
}
package service
type checklistService struct {
repository repository.IChecklistRepository
}
3. Dependency Injection via Wire
ALWAYS use Wire for dependencies:
func InitializeApp() (*App, error) {
wire.Build(
NewMyService,
wire.Bind(new(IMyService), new(*myService)),
)
return nil, nil
}
Then run:
./generate.sh
Code Quality Standards
Error Handling
Use domain.Error (custom error type):
func (s *service) Delete(ctx context.Context, id uint) domain.Error {
if err := s.repo.Delete(ctx, id); err != nil {
return domain.Wrap(err, "failed to delete", 500)
}
return nil
}
func (s *service) Delete(ctx context.Context, id uint) error {
return errors.New("something failed")
}
Error patterns:
- Return
domain.Error from service/repository methods
- Use
domain.NewError(message, statusCode) for new errors
- Use
domain.Wrap(err, context, statusCode) to wrap errors
- Guard rails return 404 for access denied (security pattern)
Context Usage
Extract user context:
userId, err := domain.GetUserIdFromContext(ctx)
if err != nil {
return err
}
if err := s.checklistOwnershipChecker.HasAccessToChecklist(ctx, checklistId); err != nil {
return error.NewChecklistNotFoundError(checklistId)
}
Extract client ID (for SSE):
clientId := serverutils.GetClientIdFromContext(ctx)
Transaction Handling
Use connection.RunInTransaction:
runQueryFunction := func(tx pool.TransactionWrapper) (ResultType, error) {
result, err := tx.Exec(ctx, query, args)
return processedResult, err
}
res, err := connection.RunInTransaction(connection.TransactionProps[ResultType]{
Query: runQueryFunction,
Connection: r.connection,
TxOptions: pgx.TxOptions{IsoLevel: pgx.Serializable},
})
Testing Requirements
Every service method needs tests:
func TestMyService_MethodName_SuccessCase(t *testing.T) {
mockRepo := new(mockRepository)
mockRepo.On("Method", mock.Anything, expectedArgs).Return(expectedResult, nil)
svc := &myService{repository: mockRepo}
result, err := svc.Method(context.Background(), args)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
mockRepo.AssertExpectations(t)
}
Test patterns:
- Success case
- Error cases
- Guard rail failures
- Edge cases (nil, empty, boundary values)
See testing-guide.md for complete examples.
Project-Specific Patterns
1. OpenAPI-First Development
Workflow:
- Update
openapi/api_v1.yaml with new operation
- Run
./generate.sh to generate server interfaces
- Implement generated interface in controller
- NEVER edit
*_gen.go files manually
Example:
paths:
/api/v1/checklists/{checklistId}/archive:
post:
operationId: archiveChecklist
func (c *controller) ArchiveChecklist(ctx context.Context, req ArchiveChecklistRequestObject) (ArchiveChecklistResponseObject, error) {
}
2. SSE Notifications
After mutations, publish events:
func (s *service) Delete(ctx context.Context, id uint) domain.Error {
if err := s.repository.Delete(ctx, id); err != nil {
return err
}
s.notifier.NotifyItemDeleted(ctx, checklistId, id)
return nil
}
SSE patterns:
- Events filtered by Client ID (no echo to originating client)
- Non-blocking publish with buffered channels
- Guard rail check on subscribe
3. Database Patterns
Doubly-linked list ordering:
CASCADE constraints:
FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE CASCADE
Named arguments (pgx):
args := pgx.NamedArgs{
"checklist_id": id,
"user_id": userId,
}
result, err := tx.Exec(ctx, "DELETE FROM t WHERE id = @checklist_id", args)
4. Struct Constructors
Private structs with public interfaces:
type IMyService interface {
DoSomething(ctx context.Context) error
}
type myService struct {
repo repository.IMyRepository
}
func NewMyService(repo repository.IMyRepository) IMyService {
return &myService{repo: repo}
}
Anti-Patterns to Avoid
❌ Don't Do This
import "com.raunlo.checklist/internal/repository"
func (c *controller) Delete(ctx context.Context, req Request) Response {
}
func (s *service) Find(ctx context.Context) {
rows, _ := db.Query("SELECT ...")
}
func (s *service) Delete(ctx context.Context, id uint) {
return s.repo.Delete(ctx, id)
}
type service struct {
repo *postgresRepo
}
s.repo.Delete(ctx, id)
return domain.NewError("", 500)
✅ Do This Instead
import "com.raunlo.checklist/internal/core/repository"
func (c *controller) Delete(ctx context.Context, req Request) Response {
domainCtx := serverutils.CreateContext(ctx)
if err := c.service.DeleteById(domainCtx, req.Id); err != nil {
return mapError(err)
}
return success()
}
func (r *repo) Find(ctx context.Context) ([]Entity, domain.Error) {
rows, err := r.connection.Query(ctx, query)
}
func (s *service) Delete(ctx context.Context, id uint) domain.Error {
if err := s.guardrail.HasAccessToChecklist(ctx, id); err != nil {
return error.NewChecklistNotFoundError(id)
}
return s.repo.Delete(ctx, id)
}
type service struct {
repo repository.IMyRepository
}
if err := s.repo.Delete(ctx, id); err != nil {
return domain.Wrap(err, "failed to delete checklist", 500)
}
return domain.NewError("Checklist is not empty", 400)
Checklist for New Code
Before submitting code, verify:
See code-review-checklist.md for complete review guide.
Quick Reference
Common commands:
./generate.sh
go test ./...
go build ./...
go test ./internal/core/service -v -run TestMyTest
File locations:
- Controllers:
internal/server/v1/
- Services:
internal/core/service/
- Service interfaces:
internal/core/repository/
- Repository impls:
internal/repository/
- Domain entities:
internal/core/domain/
- SQL queries:
internal/repository/query/
- Wire config:
internal/deployment/wire.go
- OpenAPI spec:
openapi/api_v1.yaml
Related Documentation