opencode-workflow/skills/go-backend-dev/SKILL.md

25 KiB

name description
go-backend-dev Backend Agent uses this skill to implement Golang backend. Based on implementation plan and API spec, use Domain-Driven + go-zero style architecture and TDD process to produce production-ready code. Trigger: After Task Breakdown complete (Stage 9).

/go-backend-dev — Golang Backend Implementation

Backend Agent uses this skill to implement Golang backend.

Responsibilities

  1. Establish project structure based on implementation plan (Domain-Driven + go-zero style)
  2. Implement features using TDD process (Red-Green-Refactor)
  3. Deliver incrementally by vertical slices (end-to-end, not layer-by-layer)
  4. Implement Domain / Usecase / Logic / Repository layers
  5. Write unit tests and integration tests

Input

  • Implementation plan (./plans/{feature}.md)
  • API spec (docs/api/{date}-{feature}.yaml)
  • DB Schema (docs/db/{date}-{feature}.sql)

Output

  • Golang code structure
  • Test code (unit tests >= 80%, business logic >= 90%)
  • Protobuf definitions (if gRPC needed)

Flow

Read implementation plan + API spec + DB Schema
    ↓
Identify vertical slices (each slice = end-to-end feature)
    ↓
Execute TDD loop for each slice:
    ├── RED:   Write test → Test fails
    ├── GREEN: Write minimal code → Test passes
    └── REFACTOR: Refactor → Test still passes
    ↓
Build order within slice:
    domain (entity/value object/interface)
    → pkg/domain/usecase (interface)
    → pkg/domain/repository (interface)
    → pkg/usecase (implementation)
    → pkg/mock (mock)
    → internal/logic (handler logic)
    → pkg/repository (infrastructure implementation)
    ↓
All slices complete → Run integration tests
    ↓
Confirm deliverables checklist

Step Details

1. Read Input

Read three documents simultaneously:

  • Implementation plan: Understand vertical slice breakdown and priority
  • API spec: Understand endpoints, request/response structures
  • DB Schema: Understand table structures and relationships

2. Identify Vertical Slices

Not horizontal slicing (layer by layer), but vertical slicing (end-to-end):

✅ Correct way (vertical):
  Slice 1: User registration (domain.entity + domain.usecase interface + usecase implementation + logic + repository)
  Slice 2: User login (same as above)
  Slice 3: User list (same as above)

❌ Wrong way (horizontal):
  Stage 1: All domain entities
  Stage 2: All usecases
  Stage 3: All logic handlers

3. TDD Loop (Each Slice)

For each slice, follow Red-Green-Refactor:

RED:    Write a test → Test fails
GREEN:  Write minimal code to make test pass → Test passes
REFACTOR: Refactor code → Test still passes

Build order within slice (inside-out):

  1. pkg/domain/entity/ — Define Entity and Value Object
  2. pkg/domain/member/ — Define value objects and enums (with tests)
  3. pkg/domain/usecase/ — Define Use Case interface
  4. pkg/domain/repository/ — Define Repository interface
  5. pkg/usecase/ — Implement business logic (write tests first)
  6. pkg/mock/ — Generate mocks
  7. internal/logic/ — Handler logic
  8. pkg/repository/ — Infrastructure implementation (with DB tests)

4. Testing

After each slice completes, ensure:

  • Unit tests pass (pkg/usecase/*_test.go)
  • Value object tests pass (pkg/domain/member/*_test.go)
  • Repository tests pass (pkg/repository/*_test.go)
  • Integration tests pass (critical paths)
  • Test coverage meets requirements

5. Completion Verification

Finally confirm all deliverables are complete.

Project Structure

project-root/
├── build/
│   └── Dockerfile                        # Build image
│
├── etc/
│   └── {service}.example.yaml            # Example config file
│
├── generate/
│   └── protobuf/
│       └── {service}.proto               # Protobuf definitions (if gRPC)
│
├── internal/                              # Application layer (not exposed externally)
│   ├── config/
│   │   └── config.go                      # Application config
│   ├── logic/
│   │   └── {module}/
│   │       ├── create_{entity}_logic.go   # One logic file per use case
│   │       ├── get_{entity}_logic.go
│   │       ├── update_{entity}_logic.go
│   │       └── ...
│   ├── server/
│   │   └── {module}/
│   │       └── {module}_server.go          # Server definition (HTTP/gRPC)
│   └── svc/
│       └── service_context.go             # Dependency injection container
│
├── pkg/                                   # Domain layer (can be exposed externally)
│   ├── domain/
│   │   ├── config/
│   │   │   └── config.go                  # Domain config
│   │   ├── entity/
│   │   │   ├── {entity}.go                # Entity definition
│   │   │   ├── {entity}_uid_table.go      # UID mapping table
│   │   │   └── auto_id.go                 # Auto ID generation
│   │   ├── {module}/
│   │   │   ├── {value_object}.go          # Value objects and enums
│   │   │   └── {value_object}_test.go      # Value object tests
│   │   ├── repository/
│   │   │   ├── {entity}.go                # Repository interface
│   │   │   └── ...
│   │   ├── usecase/
│   │   │   ├── {module}.go                # Use Case interface
│   │   │   └── ...
│   │   ├── errors.go                      # Domain sentinel errors
│   │   ├── const.go                        # Domain constants
│   │   └── redis.go                        # Redis domain definitions
│   ├── mock/
│   │   ├── repository/
│   │   │   ├── {entity}.go                # Repository mock
│   │   │   └── ...
│   │   └── usecase/
│   │       └── {module}.go                # Use Case mock
│   ├── repository/
│   │   ├── {entity}.go                     # Repository implementation
│   │   ├── {entity}_test.go                # Repository tests
│   │   ├── {entity}_uid.go                 # UID Repository implementation
│   │   ├── {entity}_uid_test.go
│   │   ├── error.go                        # Repository error definitions
│   │   └── start_{db}_container_test.go     # testcontainers startup
│   └── usecase/
│       ├── {module}.go                     # Use Case implementation
│       ├── {operation}.go                   # Specific operation
│       ├── {operation}_test.go              # Use Case tests
│       └── {utils}.go                       # Utility functions
│
├── {service}.go                            # Application entry point
├── Makefile
├── go.mod
├── go.sum
├── docker-compose.yml
└── readme.md

Dependency Direction Rules

pkg/domain/         ← No external dependencies (innermost, pure definitions)
    ↑
pkg/domain/usecase/ ← Use Case interface (only interface definitions)
pkg/domain/repository/ ← Repository interface (only interface definitions)
    ↑
pkg/usecase/        ← Depends on domain interfaces (business logic implementation)
pkg/mock/           ← Depends on domain interfaces (test mocks)
    ↑
internal/logic/     ← Depends on usecase implementations (handler logic)
internal/server/    ← Depends on logic (HTTP/gRPC server)
internal/svc/      ← Depends on all (DI container, assemble dependencies)
    ↑
pkg/repository/     ← Depends on domain interfaces (infrastructure implementation)
            ┌─────────────────────────────────┐
            │    pkg/domain/                  │  ← Pure definitions, no dependencies
            │    ├── entity/                  │
            │    ├── {module}/ (value objects)│
            │    ├── repository/ (interfaces) │
            │    ├── usecase/ (interfaces)    │
            │    ├── errors.go                │
            │    └── const.go                 │
            ├─────────────────────────────────┤
            │    pkg/usecase/                 │  ← Depends on domain interfaces
            │    pkg/mock/                    │  ← Depends on domain interfaces
            ├─────────────────────────────────┤
            │    internal/logic/              │  ← Depends on usecase
            │    internal/server/             │
            │    internal/config/             │
            │    internal/svc/                │  ← DI container
            ├─────────────────────────────────┤
            │    pkg/repository/              │  ← Depends on domain interfaces
            └─────────────────────────────────┘

Architecture Principles

pkg/domain/ — Pure Domain Definitions

pkg/domain/ is the core, containing only interfaces and definitions, no implementations:

// pkg/domain/entity/user.go — Entity definition
package entity

type User struct {
    ID        string    `json:"id"`
    Email     string    `json:"email"`
    Name      string    `json:"name"`
    Status    string    `json:"status"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}
// pkg/domain/member/status.go — Value object (with tests)
package member

type Status string

const (
    StatusActive   Status = "active"
    StatusInactive Status = "inactive"
)

func (s Status) IsValid() bool {
    switch s {
    case StatusActive, StatusInactive:
        return true
    }
    return false
}

func NewStatus(s string) (Status, error) {
    status := Status(s)
    if !status.IsValid() {
        return "", fmt.Errorf("invalid status: %s", s)
    }
    return status, nil
}
// pkg/domain/member/status_test.go — Value object tests
package member

func TestStatus_IsValid(t *testing.T) {
    assert.True(t, StatusActive.IsValid())
    assert.True(t, StatusInactive.IsValid())
    assert.False(t, Status("unknown").IsValid())
}

func TestNewStatus(t *testing.T) {
    status, err := NewStatus("active")
    assert.NoError(t, err)
    assert.Equal(t, StatusActive, status)

    _, err = NewStatus("unknown")
    assert.Error(t, err)
}
// pkg/domain/repository/user.go — Repository interface
package repository

type UserRepository interface {
    GetByID(ctx context.Context, id string) (*entity.User, error)
    GetByEmail(ctx context.Context, email string) (*entity.User, error)
    Create(ctx context.Context, user *entity.User) error
    Update(ctx context.Context, user *entity.User) error
    Delete(ctx context.Context, id string) error
}
// pkg/domain/usecase/user.go — Use Case interface
package usecase

type UserUsecase interface {
    CreateUser(ctx context.Context, input CreateUserInput) (*entity.User, error)
    GetUser(ctx context.Context, id string) (*entity.User, error)
    UpdateUser(ctx context.Context, id string, input UpdateUserInput) (*entity.User, error)
}
// pkg/domain/errors.go — Domain sentinel errors
package domain

import "errors"

var (
    ErrUserNotFound   = errors.New("user not found")
    ErrInvalidInput   = errors.New("invalid input")
    ErrDuplicateEmail = errors.New("email already exists")
)

pkg/usecase/ — Business Logic Implementation

One functional domain per file, test file in same directory:

// pkg/usecase/account.go — Use Case entry point
package usecase

type AccountUsecase struct {
    userRepo    repository.UserRepository
    accountRepo repository.AccountRepository
    redis       *redis.Client
}

func NewAccountUsecase(
    userRepo repository.UserRepository,
    accountRepo repository.AccountRepository,
    redis *redis.Client,
) *AccountUsecase {
    return &AccountUsecase{
        userRepo:    userRepo,
        accountRepo: accountRepo,
        redis:       redis,
    }
}
// pkg/usecase/create_user.go — Single operation
package usecase

func (uc *AccountUsecase) CreateUser(ctx context.Context, input CreateUserInput) (*entity.User, error) {
    if err := input.Validate(); err != nil {
        return nil, fmt.Errorf("validate input: %w", err)
    }

    existing, _ := uc.userRepo.GetByEmail(ctx, input.Email)
    if existing != nil {
        return nil, domain.ErrDuplicateEmail
    }

    user, err := entity.NewUser(input.Email, input.Password, input.Name)
    if err != nil {
        return nil, fmt.Errorf("create user: %w", err)
    }

    if err := uc.userRepo.Create(ctx, user); err != nil {
        return nil, fmt.Errorf("save user: %w", err)
    }

    return user, nil
}
// pkg/usecase/create_user_test.go — Test in same directory
package usecase

func TestAccountUsecase_CreateUser_Success(t *testing.T) {
    mockUserRepo := new(mock.UserRepository)
    uc := NewAccountUsecase(mockUserRepo, nil, nil)

    mockUserRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(nil, nil)
    mockUserRepo.On("Create", mock.Anything, mock.AnythingOfType("*entity.User")).Return(nil)

    user, err := uc.CreateUser(context.Background(), input)

    assert.NoError(t, err)
    assert.NotNil(t, user)
    mockUserRepo.AssertExpectations(t)
}

pkg/mock/ — Auto-Generated Mocks

// pkg/mock/repository/user.go — Generated by mockery
//go:generate mockery --name=UserRepository --output=../../mock/repository --outpkg=mock_repository

package mock_repository

import (
    "github.com/stretchr/testify/mock"
    "your-project/pkg/domain/repository"
)

type UserRepository struct {
    mock.Mock
}

func (m *UserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
    args := m.Called(ctx, id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*entity.User), args.Error(1)
}

internal/logic/ — Handler Logic

One logic file per use case (go-zero style):

// internal/logic/account/create_user_logic.go
package account

type CreateUserLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewCreateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserLogic {
    return &CreateUserLogic{
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

func (l *CreateUserLogic) CreateUser(req *types.CreateUserReq) (*types.UserResp, error) {
    user, err := l.svcCtx.UserUsecase.CreateUser(l.ctx, usecase.CreateUserInput{
        Email:    req.Email,
        Password: req.Password,
        Name:     req.Name,
    })
    if err != nil {
        return nil, err
    }

    return &types.UserResp{
        ID:    user.ID,
        Email: user.Email,
        Name:  user.Name,
    }, nil
}

internal/svc/ — Dependency Injection Container

// internal/svc/service_context.go
package svc

type ServiceContext struct {
    Config           config.Config
    UserUsecase      usecase.UserUsecase
    AccountUsecase   usecase.AccountUsecase
}

func NewServiceContext(c config.Config) *ServiceContext {
    db := mongo.NewClient(c.Mongo.URI)
    redisClient := redis.NewClient(c.Redis)

    userRepo := repository.NewUserRepository(db)
    accountRepo := repository.NewAccountRepository(db)

    return &ServiceContext{
        Config:         c,
        UserUsecase:    usecase.NewUserUsecase(userRepo, redisClient),
        AccountUsecase: usecase.NewAccountUsecase(userRepo, accountRepo, redisClient),
    }
}

pkg/repository/ — Infrastructure Implementation

// pkg/repository/user.go
package repository

type userRepository struct {
    db *mongo.Database
}

func NewUserRepository(db *mongo.Database) domain.Repository.UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
    var user entity.User
    err := r.db.Collection("users").FindOne(ctx, bson.M{"_id": id}).Decode(&user)
    if err != nil {
        if err == mongo.ErrNoDocuments {
            return nil, domain.ErrUserNotFound
        }
        return nil, fmt.Errorf("find user by id: %w", err)
    }
    return &user, nil
}
// pkg/repository/user_test.go
package repository

func TestUserRepository_GetByID_Success(t *testing.T) {
    db := startMongoContainer(t)
    defer db.Client().Disconnect(context.Background())

    repo := NewUserRepository(db)
    // ...
}

Coding Standards

File Naming

Value objects and enums: pkg/domain/{module}/{name}.go         + _test.go
Entity:                 pkg/domain/entity/{name}.go
Repository interface:   pkg/domain/repository/{name}.go
Usecase interface:      pkg/domain/usecase/{module}.go
Usecase implementation: pkg/usecase/{operation}.go             + _test.go
Usecase utilities:      pkg/usecase/{module}_utils.go           + _test.go
Repository impl:        pkg/repository/{name}.go                + _test.go
Mock:                   pkg/mock/repository/{name}.go
                        pkg/mock/usecase/{module}.go
Handler logic:          internal/logic/{module}/{operation}_logic.go
Server:                 internal/server/{module}/{module}_server.go
Service Context:        internal/svc/service_context.go
Protobuf definitions:   generate/protobuf/{module}.proto
Config files:           etc/{service}.yaml
Dockerfile:             build/Dockerfile

Naming Conventions

// Package: lowercase, semantically clear
package usecase    // not usecases
package repository // not repositories
package entity     // not entities

// Entity struct: PascalCase, no suffix
type User struct { ... }     // not UserModel, UserEntity

// Value Object: base type alias + methods
type Status string            // not StatusEnum

// Interface: defined in pkg/domain/, semantic naming
type UserRepository interface { ... }  // not UserRepo or UserRepositoryI

// Use Case struct: {Module}Usecase
type AccountUsecase struct { ... }

// Use Case methods: verb prefix
func (uc *AccountUsecase) CreateUser(ctx context.Context, ...) (*entity.User, error)
func (uc *AccountUsecase) GetUser(ctx context.Context, id string) (*entity.User, error)

// Logic struct: {Operation}Logic
type CreateUserLogic struct { ... }

// Error: Err prefix
var ErrUserNotFound = errors.New("user not found")

// Constant: PascalCase (exported) or camelCase (internal)
const MaxRetryCount = 3
const defaultPageSize = 20

Error Handling

// Sentinel errors — pkg/domain/errors.go
var (
    ErrUserNotFound   = errors.New("user not found")
    ErrInvalidInput   = errors.New("invalid input")
    ErrDuplicateEmail = errors.New("email already exists")
)

// Error wrapping — always use %w
if err != nil {
    return fmt.Errorf("create user: %w", err)
}

// Error checking — always use errors.Is
if errors.Is(err, domain.ErrUserNotFound) {
    // handle not found
}

// Repository errors — pkg/repository/error.go
var (
    ErrMongoConnection = errors.New("mongo connection failed")
    ErrRedisConnection = errors.New("redis connection failed")
)

Interface Design

// Interface defined in pkg/domain/ (consumer side)
// Implementation defined in pkg/ (provider side)

// Accept interfaces, return structs
func NewUserUsecase(repo repository.UserRepository, redis *redis.Client) *UserUsecase {
    return &UserUsecase{repo: repo, redis: redis}
}

// Keep interfaces small (1-3 methods)
type UserRepository interface {
    GetByID(ctx context.Context, id string) (*entity.User, error)
    Create(ctx context.Context, user *entity.User) error
}

TDD Standards

This skill integrates with tdd skill, following shared TDD principles.

Test Pyramid

         /\
        /  \
       / E2E \          <- Few critical flows
      /--------\
     /Integration\      <- DB + Redis (testcontainers)
    /--------------\
   /   Unit Tests    \   <- Most, 80%+ coverage
  /--------------------\

Test Location

Test files in same directory as source:

pkg/domain/member/status_test.go         ← Value object tests
pkg/usecase/create_user_test.go          ← Use Case tests
pkg/repository/user_test.go              ← Repository tests
pkg/repository/start_mongo_container_test.go ← testcontainers startup

Vertical Slice TDD

Each slice follows this order:

Slice: User registration
  1. RED:   Write TestStatus_IsValid (value object)
  2. GREEN: Write Status.IsValid()
  3. RED:   Write TestAccountUsecase_CreateUser_Success
  4. GREEN: Write domain/entity, domain/usecase interface, pkg/usecase implementation, mock
  5. RED:   Write TestAccountUsecase_CreateUser_DuplicateEmail
  6. GREEN: Add duplicate check
  7. RED:   Write TestUserRepository_Create (DB test)
  8. GREEN: Write pkg/repository/user.go
  9. RED:   Write TestCreateUserLogic (handler test)
  10. GREEN: Write internal/logic/account/create_user_logic.go
  11. REFACTOR: Clean up everything

Mock Strategy

// Use mockery to auto-generate mocks
// Add go:generate directive in interface file
//go:generate mockery --name=UserRepository --output=../../mock/repository --outpkg=mock_repository

// Unit tests use mock
func TestAccountUsecase_CreateUser_Success(t *testing.T) {
    mockRepo := new(mock_repository.UserRepository)
    uc := usecase.NewAccountUsecase(mockRepo, nil, nil)

    mockRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(nil, nil)
    mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*entity.User")).Return(nil)

    user, err := uc.CreateUser(context.Background(), input)

    assert.NoError(t, err)
    assert.NotNil(t, user)
}

testcontainers Strategy

// pkg/repository/start_mongo_container_test.go
func startMongoContainer(t *testing.T) *mongo.Database {
    ctx := context.Background()
    req := testcontainers.ContainerRequest{
        Image:        "mongo:7",
        ExposedPorts: []string{"27017/tcp"},
        WaitingFor:   wait.ForListeningPort("27017/tcp"),
    }

    mongoC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    require.NoError(t, err)

    t.Cleanup(func() {
        mongoC.Terminate(ctx)
    })

    // ... return connected database
}

Coverage Requirements

  • Value objects (pkg/domain/member/): >= 90%
  • Use Case (pkg/usecase/): >= 90%
  • Repository (pkg/repository/): >= 80%
  • Logic (internal/logic/): >= 80% (mainly integration tests)
  • Critical paths: Integration tests required

Vertical Slice Template

File list for each vertical slice:

Slice: {operation}_{entity}

New/modified files:
├── pkg/domain/entity/{entity}.go              ← Entity definition
├── pkg/domain/member/{value_object}.go         ← Value object (if needed)
├── pkg/domain/member/{value_object}_test.go     ← Value object tests
├── pkg/domain/repository/{entity}.go           ← Repository interface
├── pkg/domain/usecase/{module}.go              ← Use Case interface
├── pkg/usecase/{operation}.go                  ← Use Case implementation
├── pkg/usecase/{operation}_test.go             ← Use Case tests
├── pkg/mock/repository/{entity}.go             ← Repository mock
├── pkg/repository/{entity}.go                  ← Repository implementation
├── pkg/repository/{entity}_test.go             ← Repository tests
├── internal/logic/{module}/{operation}_logic.go ← Handler logic
└── internal/svc/service_context.go              ← Update DI

Completion Checklist

After Each Slice

  • Value object tests pass
  • Use Case tests pass
  • Repository tests pass (with DB)
  • Error handling complete
  • Dependency direction correct (domain has no external dependencies)

After All Complete

  • Project structure follows Domain-Driven + go-zero style
  • pkg/domain/ contains all Entity, Value Object, interface definitions
  • pkg/usecase/ contains all business logic implementations
  • pkg/repository/ contains all infrastructure implementations
  • internal/logic/ contains all Handler logic
  • internal/svc/ contains complete dependency injection setup
  • Unit tests >= 80% coverage
  • Business logic >= 90% coverage
  • Integration tests pass (critical paths)
  • Error handling consistent and uses %w wrapping
  • Prerequisite: prd-to-plan (implementation plan), be-api-design (API spec), dba-schema (DB Schema)
  • 辅助: tdd (TDD Red-Green-Refactor process), design-an-interface (interface design)
  • Follow-up: qa (QA testing)

Rollback Mechanism

QA failed (Stage 10)
    ↓
Orchestrator re-assigns fix task
    ↓
Backend Agent fixes bug + adds regression test
    ↓
Re-enter QA (Stage 10)

Code Review rejected (Stage 10)
    ↓
Handle PR feedback
    ↓
Re-enter QA (Stage 10) for verification

Implementation plan not feasible
    ↓
Return to Task Breakdown (Stage 8) for re-decomposition