Claude Code Plugins

Community-maintained marketplace

Feedback

go-maintainable-code

@raunlo/ChecklistApplication
0
0

Write clean, maintainable Go code following Clean Architecture, dependency injection, and ChecklistApplication patterns. Use when writing new Go code, refactoring, or implementing features.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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:

// Define interface in core/repository
package repository
type IChecklistService interface {
    DeleteChecklistById(ctx context.Context, id uint) domain.Error
}

// Implement in core/service
package service
type checklistService struct {
    repository repository.IChecklistRepository  // Interface, not concrete
}

// Wire provides concrete implementation
// internal/deployment/wire.go

3. Dependency Injection via Wire

ALWAYS use Wire for dependencies:

// Add to internal/deployment/wire.go
func InitializeApp() (*App, error) {
    wire.Build(
        // ... existing providers ...
        NewMyService,           // Add your constructor
        wire.Bind(new(IMyService), new(*myService)),
    )
    return nil, nil
}

Then run:

./generate.sh  # Regenerates wire_gen.go

Code Quality Standards

Error Handling

Use domain.Error (custom error type):

// ✅ Good
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
}

// ❌ Bad - using standard error
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:

// In service layer
userId, err := domain.GetUserIdFromContext(ctx)
if err != nil {
    return err
}

// Guard rail checks
if err := s.checklistOwnershipChecker.HasAccessToChecklist(ctx, checklistId); err != nil {
    return error.NewChecklistNotFoundError(checklistId)
}

Extract client ID (for SSE):

// In controller
clientId := serverutils.GetClientIdFromContext(ctx)

Transaction Handling

Use connection.RunInTransaction:

runQueryFunction := func(tx pool.TransactionWrapper) (ResultType, error) {
    // Execute queries using tx, not connection
    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) {
    // Arrange
    mockRepo := new(mockRepository)
    mockRepo.On("Method", mock.Anything, expectedArgs).Return(expectedResult, nil)

    svc := &myService{repository: mockRepo}

    // Act
    result, err := svc.Method(context.Background(), args)

    // Assert
    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:

  1. Update openapi/api_v1.yaml with new operation
  2. Run ./generate.sh to generate server interfaces
  3. Implement generated interface in controller
  4. NEVER edit *_gen.go files manually

Example:

# openapi/api_v1.yaml
paths:
  /api/v1/checklists/{checklistId}/archive:
    post:
      operationId: archiveChecklist
      # ... rest of spec
// internal/server/v1/checklist/controller.go
// Implements generated ServerInterface
func (c *controller) ArchiveChecklist(ctx context.Context, req ArchiveChecklistRequestObject) (ArchiveChecklistResponseObject, error) {
    // Implementation
}

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
    }

    // Publish SSE event
    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:

// Items use NEXT_ITEM_ID/PREV_ITEM_ID
// Use recursive CTE view: CHECKLIST_ITEMS_ORDERED_VIEW
// Phantom items: IS_PHANTOM = true, filtered in queries

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:

// Public interface
type IMyService interface {
    DoSomething(ctx context.Context) error
}

// Private implementation
type myService struct {
    repo repository.IMyRepository
}

// Public constructor for Wire
func NewMyService(repo repository.IMyRepository) IMyService {
    return &myService{repo: repo}
}

Anti-Patterns to Avoid

❌ Don't Do This

// ❌ Importing concrete types across layers
import "com.raunlo.checklist/internal/repository"

// ❌ Business logic in controllers
func (c *controller) Delete(ctx context.Context, req Request) Response {
    // Validating, processing here - NO!
}

// ❌ SQL in service layer
func (s *service) Find(ctx context.Context) {
    rows, _ := db.Query("SELECT ...") // NO!
}

// ❌ Not using guard rails
func (s *service) Delete(ctx context.Context, id uint) {
    return s.repo.Delete(ctx, id) // Missing access check!
}

// ❌ Hardcoded dependencies
type service struct {
    repo *postgresRepo  // Should be interface
}

// ❌ Ignoring errors
s.repo.Delete(ctx, id)  // No error handling

// ❌ Empty error messages
return domain.NewError("", 500)

✅ Do This Instead

// ✅ Interface imports only
import "com.raunlo.checklist/internal/core/repository"

// ✅ Thin controllers
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()
}

// ✅ SQL in repository layer
func (r *repo) Find(ctx context.Context) ([]Entity, domain.Error) {
    rows, err := r.connection.Query(ctx, query)
    // ...
}

// ✅ Guard rail checks
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)
}

// ✅ Interface dependencies
type service struct {
    repo repository.IMyRepository  // Interface
}

// ✅ Proper error handling
if err := s.repo.Delete(ctx, id); err != nil {
    return domain.Wrap(err, "failed to delete checklist", 500)
}

// ✅ Descriptive errors
return domain.NewError("Checklist is not empty", 400)

Checklist for New Code

Before submitting code, verify:

  • Follows Clean Architecture (correct layer separation)
  • Uses interfaces for dependencies
  • Added to Wire configuration if new service/repo
  • Ran ./generate.sh if OpenAPI changed
  • Proper error handling (domain.Error)
  • Guard rail checks for authorization
  • SSE notifications for mutations (if applicable)
  • Unit tests with mocks (testify)
  • No magic numbers or strings
  • Context passed through all layers
  • Ran go test ./... and all pass
  • Ran go build ./... successfully
  • No TODO comments without issue number

See code-review-checklist.md for complete review guide.

Quick Reference

Common commands:

./generate.sh           # OpenAPI + Wire code generation
go test ./...          # Run all tests
go build ./...         # Build all packages
go test ./internal/core/service -v -run TestMyTest  # Run specific test

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