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
- Establish project structure based on implementation plan (Domain-Driven + go-zero style)
- Implement features using TDD process (Red-Green-Refactor)
- Deliver incrementally by vertical slices (end-to-end, not layer-by-layer)
- Implement Domain / Usecase / Logic / Repository layers
- 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):
pkg/domain/entity/— Define Entity and Value Objectpkg/domain/member/— Define value objects and enums (with tests)pkg/domain/usecase/— Define Use Case interfacepkg/domain/repository/— Define Repository interfacepkg/usecase/— Implement business logic (write tests first)pkg/mock/— Generate mocksinternal/logic/— Handler logicpkg/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 definitionspkg/usecase/contains all business logic implementationspkg/repository/contains all infrastructure implementationsinternal/logic/contains all Handler logicinternal/svc/contains complete dependency injection setup- Unit tests >= 80% coverage
- Business logic >= 90% coverage
- Integration tests pass (critical paths)
- Error handling consistent and uses
%wwrapping
Related Skills
- 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