From skills
Go backend development standards and scaffold tools for MBU ID services. Use this skill for any Go backend work including project initialization, entity generation, repository patterns, usecase implementation, REST/gRPC handlers, event publishers/subscribers, and dependency injection. Apply when scaffolding new services, adding features to existing Go services, or implementing clean architecture patterns. This skill covers the complete workflow from SQL DDL to production-ready handlers.
How this skill is triggered — by the user, by Claude, or both
Slash command
/skills:mbu-golangThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Go backend coding standards, clean architecture patterns, and project structure for MBU microservices.
scripts/common.shscripts/scaffold-entity.shscripts/scaffold-factory.shscripts/scaffold-grpc.shscripts/scaffold-handler.shscripts/scaffold-init.shscripts/scaffold-publisher.shscripts/scaffold-repo.shscripts/scaffold-service.shscripts/scaffold-subscriber.shscripts/scaffold-usecase.shscripts/templates/factory.tplscripts/templates/handler/handler.tplscripts/templates/handler/request_create.tplscripts/templates/handler/request_delete.tplscripts/templates/handler/request_get.tplscripts/templates/handler/request_update.tplscripts/templates/init/chart-deployment.tplscripts/templates/init/chart-ingress.tplscripts/templates/init/chart-service.tplGo backend coding standards, clean architecture patterns, and project structure for MBU microservices.
interface{} unless necessaryservice-warehouse/ # Service root
├── go.mod # Main module
├── main.go # Entry point
├── entity/ # Entity definitions
│ ├── warehouse.go
│ └── delivery_plan.go
├── src/
│ ├── handler.go # Route registration
│ ├── handler/
│ │ ├── warehouse/ # Module handlers
│ │ │ ├── handler.go # Route group + RegisterHandler
│ │ │ ├── request_create.go
│ │ │ ├── request_update.go
│ │ │ ├── request_get.go
│ │ │ └── request_delete.go
│ │ └── delivery_plan/
│ │ └── publish.go
│ ├── event/
│ │ ├── publisher/
│ │ │ └── warehouse.go # RabbitMQ publishers
│ │ └── subscriber/
│ │ └── warehouse.go # RabbitMQ subscribers
│ ├── repository/
│ │ ├── warehouse.go # Data access layer
│ │ └── delivery_plan.go
│ ├── services/
│ │ └── service_ordering.go # gRPC client to external service
│ └── usecase/
│ ├── factory.go # Dependency injection (Factory struct)
│ ├── warehouse.go # Business logic
│ └── delivery_plan.go
└── migrations/ # Database migrations
├── 20260525_create_warehouses.up.sql
└── 20260525_create_warehouses.down.sql
warehouse.go, not warehouses.gohandler/warehouse/, handler/delivery_plan/// ✅ Correct
type Warehouse struct {}
func NewWarehouse() *Warehouse {}
const MaxRetries = 3
// ❌ Wrong
type warehouse struct {} // Should be exported
func new_warehouse() {} // Should be camelCase
const max_retries = 3 // Should be PascalCase
handler, usecase, repositorywarehouse, orderbusiness_partner/, item_category/ for handler module directories (exception: multi-word package names use underscore since Go requires single-word names)All code generation is driven by two naming domains:
| Domain | Source | Example | Used By |
|---|---|---|---|
| ENTITY_NAME | SQL table name (PascalCase) | Warehouse | entity/, repository/ |
| MODULE_NAME | Business feature (PascalCase) | DeliveryPlan | usecase/, handler/, factory.go |
Key distinction:
A module may use multiple entities. Example: DeliveryPlan module uses DeliveryPlan, DeliveryPlanItem, and Item entities.
HTTP Request
↓
Handler (transport layer)
↓
Request Struct (validation + transformation)
↓
Usecase (business logic)
↓
Repository (data access)
↓
Database
Rule 1 — Never skip layers Every request must flow through all layers. No shortcuts.
// ❌ Wrong — Handler bypassing usecase (repo exported, but shouldn't be)
func (h *Handler) Create(ctx *rest.Context) error {
// Repo is unexported — you can't even access it. Always use usecase.
// ...
}
// ✅ Correct — Handler calls usecase, usecase calls repository
func (h *Handler) Create(ctx *rest.Context) error {
warehouse, err := h.uc.Warehouse.FindByID(id)
// ...
}
Rule 2 — Repository only accessible by usecase
Handler and request structs MUST NOT access .Repo directly.
Rule 3 — Return raw errors, never wrap The engine extracts error messages from raw errors. Wrapping loses the message.
// ✅ Correct
return nil, err
// ❌ Wrong — wrapping loses the message
return nil, fmt.Errorf("failed to create: %w", err)
Rule 4 — Publish events async with captured context
// ✅ Correct — capture context before goroutine
ctx := u.ctx
go publisher.WarehouseCreated(ctx, req)
// ❌ Wrong — loses context reference
go publisher.WarehouseCreated(u.ctx, req)
Scripts are in skills/mbu-golang/scripts/:
scaffold-init.sh — Project skeletonscaffold-entity.sh — Entity + repository (chains to scaffold-repo.sh)scaffold-repo.sh — Repository (called by scaffold-entity.sh)scaffold-usecase.sh — Usecasescaffold-factory.sh — Factory wiringscaffold-handler.sh — Handler + request filesscaffold-grpc.sh — gRPC handler + proto boilerplatescaffold-publisher.sh — Event publisherscaffold-subscriber.sh — Event subscriberscaffold-service.sh — gRPC client to external service (src/services/service_{name}.go)See Workflows section for automated task sequences using these tools.
Step 0 — Determine project type
Ask the user:
go.mod exists, proceed to Step 1github.com/mbu-id/service-myapi)For new projects:
mkdir -p ~/Works/Enigma/service-myapi
cd ~/Works/Enigma/service-myapi
go mod init github.com/mbu-id/service-myapi
Step 1 — Initialize project
SCRIPTS=~/Works/Enigma/claude-plugins/skills/mbu-golang/scripts
cd ~/Works/Enigma/service-myapi
# Generate project skeleton
$SCRIPTS/scaffold-init.sh
# Initialize entity module
cd entity && go mod init github.com/mbu-id/service-myapi/entity && cd ..
# Wire entity module (required for subdirectory modules)
go mod edit -require github.com/mbu-id/service-myapi/[email protected]
go mod edit -replace github.com/mbu-id/service-myapi/entity=./entity
Step 2 — Generate entity + repository
# From SQL CREATE TABLE statement
echo "CREATE TABLE warehouses (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
address TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);" | $SCRIPTS/scaffold-entity.sh --from-stdin
# This generates:
# - entity/warehouse.go
# - src/repository/warehouse.go
Step 3 — Generate usecase
$SCRIPTS/scaffold-usecase.sh Warehouse
# Generates: src/usecase/warehouse.go
Step 4 — Wire into factory
$SCRIPTS/scaffold-factory.sh Warehouse
# Updates: src/factory/factory.go
Step 5 — Generate handler
$SCRIPTS/scaffold-handler.sh Warehouse
# Generates:
# - src/handler/warehouse/create.go
# - src/handler/warehouse/update.go
# - src/handler/warehouse/show.go
# - src/handler/warehouse/list.go
# - src/handler/warehouse/delete.go
# Updates: src/handler/handler.go (route registration)
Step 6 — Resolve dependencies
go mod tidy && go build ./...
Use -f flag to overwrite existing files:
$SCRIPTS/scaffold-entity.sh --from-stdin -f
$SCRIPTS/scaffold-usecase.sh Warehouse -f
package entity
import (
"github.com/uptrace/bun"
"github.com/google/uuid"
"time"
)
type Warehouse struct {
bun.BaseModel `bun:"table:warehouse,alias:w"`
ID uuid.UUID `bun:"id,pk,type:uuid,default:uuid_generate_v4()"`
Name string `bun:"name,notnull"`
Address string `bun:"address,null"`
IsActive bool `bun:"is_active,notnull,default:true"`
CreatedAt time.Time `bun:"created_at,notnull"`
UpdatedAt time.Time `bun:"updated_at,notnull"`
}
Bun tags:
pk — Primary keynotnull — Required fieldnull — Nullable fieldtype:uuid — UUID typedefault:uuid_generate_v4() — Auto-generate UUIDalias:w — Table alias for queriesNullable fields:
Use pointer types for nullable fields: *float64, *uuid.UUID, *bool
package repository
import (
"github.com/mbu-id/engine/ds/postgres"
"github.com/mbu-id/service-myapi/entity"
)
type WarehouseRepository struct {
*postgres.BaseRepository[entity.Warehouse]
}
func NewWarehouseRepository() *WarehouseRepository {
return &WarehouseRepository{
BaseRepository: postgres.NewBaseRepository[entity.Warehouse](
postgres.GetDB(), // DB from engine's postgres package
"warehouse", // table name
[]string{"w.name", "w.address"}, // searchable fields
[]string{}, // default relations
false, // soft delete disabled
),
}
}
// Override WithContext for method chaining
func (r *WarehouseRepository) WithContext(ctx context.Context) common.BaseRepositoryInterface[entity.Warehouse] {
return &WarehouseRepository{
BaseRepository: r.BaseRepository.WithCtx(ctx).(*postgres.BaseRepository[entity.Warehouse]),
}
}
// Custom query methods
func (r *WarehouseRepository) FindByName(ctx context.Context, name string) (*entity.Warehouse, error) {
return r.WithContext(ctx).FindOne(func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("name = ?", name)
})
}
package usecase
import (
"context"
"github.com/mbu-id/mbu-service/src/repository"
"github.com/mbu-id/mbu-service/entity"
)
type WarehouseUsecase struct {
repo *repository.WarehouseRepository // unexported — only usecase can access
ctx context.Context
}
func NewWarehouseUsecase() *WarehouseUsecase {
return &WarehouseUsecase{
repo: repository.NewWarehouseRepository(),
ctx: context.Background(),
}
}
func (u *WarehouseUsecase) Create(req *entity.Warehouse) error {
return u.repo.WithContext(u.ctx).Insert(req)
}
func (u *WarehouseUsecase) Show(id string) (*entity.Warehouse, error) {
return u.repo.WithContext(u.ctx).FindByID(id)
}
func (u *WarehouseUsecase) Update(id string, req *entity.Warehouse, fields ...string) (*entity.Warehouse, error) {
existing, err := u.repo.WithContext(u.ctx).FindByID(id)
if err != nil {
return nil, err
}
existing.Name = req.Name
existing.Address = req.Address
if err := u.repo.WithContext(u.ctx).Update(existing, fields...); err != nil {
return nil, err
}
return existing, nil
}
package warehouse
import (
"github.com/mbu-id/engine/transport/rest"
"github.com/mbu-id/mbu-service/src/usecase"
)
type handler struct {
uc *usecase.Factory
}
func CreateHandler(uc *usecase.Factory) *handler {
return &handler{uc: uc}
}
func (h *handler) create(ctx *rest.Context) error {
var req createRequest
if err := ctx.Bind(req.with(ctx, h.uc)); err != nil {
return ctx.Respond(nil, err)
}
result, err := req.execute()
return ctx.Respond(result, err)
}
Request Validation Flow:
ctx.Bind() automatically handles validation via the validate.Request interface:
req.Validate() if the struct implements validate.Requestreq.Messages()Source: github.com/mbu-id/engine/transport/rest/context.go
// Inside ctx.Bind():
if err := c.Validate(v); !err.Valid {
return err
}
// Inside ctx.Validate():
if vr, ok := obj.(validate.Request); ok {
resp = c.validator.Request(vr) // Calls vr.Validate() and vr.Messages()
}
Key Point: You never call Validate() explicitly in handler code. ctx.Bind() does it automatically.
### Request Struct (5 Required Methods)
```go
package warehouse
import (
"context"
"time"
"github.com/mbu-id/engine/common"
"github.com/mbu-id/engine/transport/rest"
"github.com/mbu-id/engine/validate"
"github.com/mbu-id/service-myapi/src/usecase"
"github.com/mbu-id/service-myapi/entity"
)
type createRequest struct {
CompanyID string `json:"company_id" valid:"required|uuid"`
Code string `json:"code" valid:"required"`
Name string `json:"name" valid:"required"`
Status string `json:"status"`
Phone *string `json:"phone,omitempty"`
Email *string `json:"email,omitempty"`
company *entity.Company // cached during Validate
ctx context.Context
uc *usecase.Factory
Session *common.SessionClaims
}
// Validate — validation rules + FK existence checks
func (r *createRequest) Validate() *validate.Response {
v := validate.NewResponse()
var err error
if r.company, err = r.uc.Company.FindByID(r.CompanyID); err != nil {
v.SetError("company_id.invalid", "company not found")
}
return v
}
// Messages — custom validation error messages
func (r *createRequest) Messages() map[string]string {
return map[string]string{}
}
// toEntity — transform request to entity
func (r *createRequest) toEntity() *entity.Warehouse {
return &entity.Warehouse{
CompanyID: r.company.ID, // from cached entity, no uuid.Parse
Code: r.Code,
Name: r.Name,
Phone: r.Phone,
Email: r.Email,
Status: "ACTIVE",
CreatedAt: time.Now(),
}
}
// execute — persist via usecase
func (r *createRequest) execute() (*rest.ResponseBody, error) {
mx := r.toEntity()
if err := r.uc.Warehouse.Create(mx); err != nil {
return nil, err
}
return rest.NewResponseBody(mx), nil
}
// with — inject context and factory
func (r *createRequest) with(ctx context.Context, uc *usecase.Factory) *createRequest {
r.uc = uc.WithContext(ctx)
r.ctx = ctx
return r
}
existingThe update request fetches the existing entity in Validate(), applies only non-nil fields
in apply(), and saves with an explicit column list.
package warehouse
import (
"context"
"time"
"github.com/mbu-id/engine/common"
"github.com/mbu-id/engine/transport/rest"
"github.com/mbu-id/engine/validate"
"github.com/mbu-id/service-myapi/src/usecase"
"github.com/mbu-id/service-myapi/entity"
)
type updateRequest struct {
ID string `json:"id" param:"id" valid:"required|uuid"`
Code *string `json:"code,omitempty"`
Name *string `json:"name,omitempty"`
Phone *string `json:"phone,omitempty"`
Email *string `json:"email,omitempty"`
Status *string `json:"status,omitempty"`
ctx context.Context
uc *usecase.Factory
session *common.SessionClaims
existing *entity.Warehouse // fetched during Validate
}
// Validate — fetch existing + FK existence checks
func (r *updateRequest) Validate() *validate.Response {
v := validate.NewResponse()
var err error
r.existing, err = r.uc.Warehouse.FindByID(r.ID)
if err != nil {
v.SetError("id.invalid", "data not found.")
}
return v
}
// Messages — custom validation error messages
func (r *updateRequest) Messages() map[string]string {
return map[string]string{}
}
// apply — update only non-nil fields on the existing entity
func (r *updateRequest) apply(e *entity.Warehouse) {
if r.Code != nil {
e.Code = *r.Code
}
if r.Name != nil {
e.Name = *r.Name
}
if r.Phone != nil {
e.Phone = r.Phone
}
if r.Email != nil {
e.Email = r.Email
}
if r.Status != nil {
e.Status = *r.Status
}
e.UpdatedAt = time.Now()
}
// execute — apply changes and save with explicit field list
func (r *updateRequest) execute() (*rest.ResponseBody, error) {
r.apply(r.existing)
fields := []string{"code", "name", "phone", "email", "status", "updated_at"}
if err := r.uc.Warehouse.Update(r.existing, fields...); err != nil {
return nil, err
}
return rest.NewResponseBody(r.existing), nil
}
// with — inject context and factory
func (r *updateRequest) with(ctx context.Context, uc *usecase.Factory) *updateRequest {
r.uc = uc.WithContext(ctx)
r.ctx = ctx
return r
}
Factory lives in src/usecase/factory.go (package usecase). A single Factory struct holds all usecase instances.
package usecase
import (
"context"
)
// Factory holds all usecase instances for the service.
type Factory struct {
Warehouse *WarehouseUsecase
}
// NewFactory creates a new Factory with all usecases.
// Takes NO parameters — repositories call postgres.GetDB() internally.
func NewFactory() *Factory {
return &Factory{
Warehouse: NewWarehouseUsecase(),
}
}
// WithContext returns a new Factory with context propagated to all usecases.
func (f *Factory) WithContext(ctx context.Context) *Factory {
return &Factory{
Warehouse: f.Warehouse.WithContext(ctx),
}
}
| Tag | Description | Example |
|---|---|---|
required | Field must not be empty | valid:"required" |
email | Valid email format | valid:"email" |
uuid | Valid UUID format | valid:"uuid" |
min:n | Minimum value/length | valid:"min:3" |
max:n | Maximum value/length | valid:"max:100" |
gte:n | Greater than or equal | valid:"gte:0" |
lte:n | Less than or equal | valid:"lte:100" |
oneof:A;B;C | Value must be in list | valid:"oneof:admin;user" |
password | Password strength | valid:"password" |
phone | Phone number format | valid:"phone" |
alphanum | Alphanumeric only | valid:"alphanum" |
numeric | Numeric only | valid:"numeric" |
Use validate.NewResponse() + v.SetError(). Cache FK entities in struct fields
during validation — use .ID in toEntity()/apply(), never re-parse with uuid.Parse().
type createRequest struct {
CompanyID string `json:"company_id" valid:"required|uuid"`
VendorID string `json:"vendor_id" valid:"required|uuid"`
company *entity.Company // cached during Validate
vendor *entity.Vendor // cached during Validate
// ...
}
func (r *createRequest) Validate() *validate.Response {
v := validate.NewResponse()
var err error
if r.company, err = r.uc.Company.FindByID(r.CompanyID); err != nil {
v.SetError("company_id.invalid", "company not found")
}
if r.vendor, err = r.uc.Vendor.FindByID(r.VendorID); err != nil {
v.SetError("vendor_id.invalid", "vendor not found")
}
return v
}
func (r *createRequest) toEntity() *entity.Warehouse {
return &entity.Warehouse{
CompanyID: r.company.ID,
VendorID: r.vendor.ID, // ✅ from cached entity, no uuid.Parse
// ...
}
}
return resp
}
// Custom validation
if r.Latitude != nil && (*r.Latitude < -90 || *r.Latitude > 90) {
return &validate.Response{
Errors: map[string][]string{
"latitude": {"Latitude must be between -90 and 90"},
},
}
}
return nil
}
// ✅ Correct
warehouse, err := u.repo.WithContext(u.ctx).FindByID(id)
if err != nil {
return nil, err
}
// ❌ Wrong — wrapping loses error message
if err != nil {
return nil, fmt.Errorf("failed to find warehouse: %w", err)
}
import "errors"
var (
ErrWarehouseNotFound = errors.New("warehouse not found")
ErrInvalidCoordinates = errors.New("invalid coordinates")
)
func (u *WarehouseUsecase) Show(id string) (*entity.Warehouse, error) {
warehouse, err := u.Repo.WithContext(u.ctx).FindByID(id)
if err != nil {
return nil, ErrWarehouseNotFound
}
return warehouse, nil
}
Engine standard response envelope. Every service API contract must use these schemas.
ResponseBody:
type: object
properties:
success:
type: boolean
description: Indicates if the request was successful
message:
type: string
description: Human-readable status message
example: success
data:
description: Response payload (null on errors)
nullable: true
errors:
description: Error details (null on success)
nullable: true
meta:
$ref: "#/components/schemas/Meta"
required:
- success
- message
Meta:
type: object
properties:
page:
type: integer
page_size:
type: integer
total:
type: integer
total_pages:
type: integer
has_next:
type: boolean
has_prev:
type: boolean
Use allOf to compose domain data with the standard envelope:
WarehouseResponse:
allOf:
- $ref: "#/components/schemas/ResponseBody"
- type: object
properties:
data:
$ref: "#/components/schemas/Warehouse"
WarehouseListResponse:
allOf:
- $ref: "#/components/schemas/ResponseBody"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Warehouse"
meta:
$ref: "#/components/schemas/Meta"
All services share these reusable responses components:
components:
responses:
NotFound:
description: Resource not found
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ResponseBody"
- type: object
properties:
success:
enum: [false]
message:
example: resource not found
data:
nullable: true
example: null
errors:
nullable: true
example: null
meta:
nullable: true
BadRequest:
description: Bad request
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ResponseBody"
- type: object
properties:
success:
enum: [false]
message:
example: invalid request body. please check your input format
data:
nullable: true
example: null
errors:
nullable: true
example: null
meta:
nullable: true
Conflict:
description: Conflict
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ResponseBody"
- type: object
properties:
success:
enum: [false]
message:
example: conflict
data:
nullable: true
example: null
errors:
nullable: true
example: null
meta:
nullable: true
ValidationError:
description: Validation failed
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ResponseBody"
- type: object
properties:
success:
enum: [false]
message:
example: validation failed
data:
nullable: true
example: null
errors:
type: object
description: Field-level validation messages
example:
name: The name field is required
quantity: Must be a positive integer
meta:
nullable: true
RateLimited:
description: Rate limited
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ResponseBody"
- type: object
properties:
success:
enum: [false]
message:
example: too many requests
data:
nullable: true
example: null
errors:
nullable: true
example: null
meta:
nullable: true
InternalError:
description: Internal server error
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ResponseBody"
- type: object
properties:
success:
enum: [false]
message:
example: internal server error
data:
nullable: true
example: null
errors:
type: string
description: Raw error message (not exposed in production)
meta:
nullable: true
| Field | Success | Validation Error | HTTPError | Internal Error |
|---|---|---|---|---|
success | true | false | false | false |
message | "success" | "validation failed" | per error type | "internal server error" |
data | payload | null | null | null |
errors | null | field → message map | null | raw error string |
meta | pagination | null | null | null |
# 1. Reference ResponseBody + Meta as component schemas
# 2. Define domain schemas (Upload, Warehouse, etc.)
# 3. Define typed response wrappers with allOf
# 4. Reuse standard error responses via $ref
paths:
/warehouses:
get:
responses:
"200":
description: Warehouse list
content:
application/json:
schema:
$ref: "#/components/schemas/WarehouseListResponse"
"400":
$ref: "#/components/responses/BadRequest"
components:
schemas:
ResponseBody:
# ... copy from above
Meta:
# ... copy from above
Warehouse:
type: object
properties:
id: ...
WarehouseListResponse:
allOf:
- $ref: "#/components/schemas/ResponseBody"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Warehouse"
meta:
$ref: "#/components/schemas/Meta"
responses:
BadRequest:
# ... copy from above
| Engine Go Type | OAS Schema |
|---|---|
rest.ResponseBody | ResponseBody |
rest.Meta | Meta |
rest.HTTPError | NotFound, BadRequest, Conflict, etc. |
validate.Response | ValidationError |
| Internal/unknown error | InternalError |
Place the standard schemas in each service's api-contract.yaml. All service contracts share these identical schemas.
package publisher
import (
"context"
"github.com/mbu-id/engine/broker/rabbitmq"
)
type WarehouseCreatedPayload struct {
ID string `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
}
func WarehouseCreated(ctx context.Context, payload WarehouseCreatedPayload) error {
return rabbitmq.Publish(ctx, "warehouse.created", payload)
}
Use dot notation: noun.verb
✅ Correct:
- warehouse.created
- warehouse.updated
- delivery.plan.published
❌ Wrong:
- warehouse:created (colon separator)
- WarehouseCreated (PascalCase)
- warehouse_created (underscore)
func (r *createRequest) execute() (*rest.ResponseBody, error) {
mx := r.toEntity()
if err := r.uc.Warehouse.Create(mx); err != nil {
return nil, err
}
// Publish event async (capture context first)
capturedCtx := r.ctx
go publisher.WarehouseCreated(capturedCtx, publisher.WarehouseCreatedPayload{
ID: mx.ID.String(),
Name: mx.Name,
Address: mx.Address,
})
return rest.NewResponseBody(mx), nil
}
package repository_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mbu-id/service-myapi/src/repository"
"github.com/mbu-id/service-myapi/entity"
)
func TestWarehouseRepository_Create(t *testing.T) {
// Setup test database
repo := repository.NewWarehouseRepository()
ctx := context.Background()
// Test data
warehouse := &entity.Warehouse{
Name: "Test Warehouse",
Address: "123 Test St",
}
// Execute
err := repo.WithContext(ctx).Insert(warehouse)
// Assert
assert.NoError(t, err)
assert.NotEmpty(t, warehouse.ID)
}
Usecases depend on repositories internally injected via NewXxxUsecase(). Test with a real database connection:
package usecase_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mbu-id/mbu-service/entity"
"github.com/mbu-id/mbu-service/src/usecase"
)
func TestWarehouseUsecase_Create(t *testing.T) {
uc := usecase.NewWarehouseUsecase()
ctx := context.Background()
warehouse := &entity.Warehouse{
Name: "Test Warehouse",
Address: "123 Test St",
}
err := uc.WithContext(ctx).Create(warehouse)
assert.NoError(t, err)
assert.NotEmpty(t, warehouse.ID)
}
Always generate skeleton code with scaffold scripts, then customize.
// ✅ Correct
Latitude *float64 `bun:"lat,null"`
// ❌ Wrong — will panic on zero values
Latitude float64 `bun:"lat,null"`
Business logic should be simple. Complex logic goes in domain services.
err := repo.RunInTx(ctx, func(ctx context.Context, tx bun.Tx) error {
// Multiple operations...
return nil
})
go mod tidyAfter scaffolding or adding dependencies:
go mod tidy && go build ./...
// ✅ Correct
repo.WithContext(ctx).FindByID(id)
// ❌ Wrong
repo.FindByID(id)
Validate input in request structs, not in usecases or repositories.
For create/update requests, cache foreign key entities in Validate() via usecase
lookup. Use the cached entity's .ID field in toEntity()/apply() — never parse
UUIDs again with uuid.Parse() and discard the error.
// ✅ Correct — cache in Validate, use .ID
func (r *createRequest) Validate() *validate.Response {
var err error
if r.company, err = r.uc.Company.FindByID(r.CompanyID); err != nil {
v.SetError("company_id.invalid", "company not found")
}
}
func (r *createRequest) toEntity() *entity.Warehouse {
return &entity.Warehouse{CompanyID: r.company.ID} // .ID from cache
}
// ❌ Wrong — uuid.Parse discards errors
companyID, _ := uuid.Parse(r.CompanyID)
handler/business_partner/ ✅
handler/item_category/ ✅
handler/businesspartner/ ❌
Single-word directories stay flat: warehouse/, order/.
All fields in updateRequest must be pointer types (*string, *bool, *int).
Apply nil-checks in apply() — non-nil means the client sent the field.
type updateRequest struct {
Name *string `json:"name,omitempty"`
Status *string `json:"status,omitempty"`
}
func (r *updateRequest) apply(e *entity.Warehouse) {
if r.Name != nil { e.Name = *r.Name }
if r.Status != nil { e.Status = *r.Status }
}
Proto definitions live in a separate Go module:
service-order/
├── go.mod # Main service module
├── proto/
│ ├── go.mod # Separate proto module
│ ├── order.proto # Proto definition (manual)
│ ├── order.pb.go # Generated by protoc
│ ├── order_grpc.pb.go # Generated by protoc
│ ├── constant.go # ServiceName constant
│ └── converter.go # Entity ↔ Proto converters
└── src/handler/grpc/
└── order.go # gRPC handler implementation
syntax = "proto3";
package order;
option go_package = "github.com/mbu-id/service-order/proto;proto";
message Order {
string id = 1;
string code = 2;
string name = 3;
double total_charges = 4;
}
message ShowRequest {
string id = 1;
}
message OrderResponse {
Order order = 1;
}
service OrderService {
rpc Show(ShowRequest) returns (OrderResponse);
rpc List(ListRequest) returns (ListResponse);
}
Prerequisites:
.proto file manually in proto/ directoryGenerate boilerplate:
SCRIPTS=~/Works/Enigma/claude-plugins/skills/mbu-golang/scripts
cd ~/Works/Enigma/service-myapi
# Generate gRPC handler boilerplate
$SCRIPTS/scaffold-grpc.sh Order
# This generates:
# - proto/constant.go (ServiceName)
# - proto/converter.go (entity ↔ proto stubs)
# - src/handler/grpc/order.go (handler skeleton)
# - Updates src/handler.go (RegisterGrpcRoutes)
Generate proto code:
cd proto
protoc --go_out=. --go-grpc_out=. order.proto
package proto
import (
"github.com/mbu-id/service-order/entity"
"github.com/google/uuid"
)
// ConvertOrder converts entity.Order to proto.Order
func ConvertOrder(m *entity.Order) *Order {
if m == nil {
return nil
}
return &Order{
Id: m.ID.String(),
Code: m.Code,
Name: m.Name,
TotalCharges: m.TotalCharges,
}
}
// ConvertOrderToEntity converts proto.Order to entity.Order
func ConvertOrderToEntity(m *Order) (*entity.Order, error) {
if m == nil {
return nil, nil
}
id, err := uuid.Parse(m.Id)
if err != nil {
return nil, err
}
return &entity.Order{
ID: id,
Code: m.Code,
Name: m.Name,
TotalCharges: m.TotalCharges,
}, nil
}
package grpc
import (
"context"
"github.com/mbu-id/service-order/proto"
"github.com/mbu-id/service-order/src/usecase"
)
type orderHandler struct {
proto.UnimplementedOrderServiceServer
uc *usecase.Factory
}
func RegisterOrderHandler() proto.OrderServiceServer {
return &orderHandler{
uc: usecase.NewFactory(),
}
}
// Show implements OrderService.Show
func (h *orderHandler) Show(ctx context.Context, req *proto.ShowRequest) (*proto.OrderResponse, error) {
mx, err := h.uc.Order.WithContext(ctx).FindByID(req.Id)
if err != nil {
return nil, err
}
return &proto.OrderResponse{
Order: proto.ConvertOrder(mx),
}, nil
}
In src/handler.go:
package src
import (
"github.com/mbu-id/engine/transport/grpc"
"github.com/mbu-id/service-order/proto"
grpcHandler "github.com/mbu-id/service-order/src/handler/grpc"
)
// RegisterGrpcRoutes registers all gRPC service handlers.
func RegisterGrpcRoutes(srv *grpc.GrpcServer) {
proto.RegisterOrderServiceServer(srv, grpcHandler.RegisterOrderHandler())
}
In main.go, ensure gRPC server is active:
// Start gRPC server
transportGRPC := grpc.NewService(&grpc.Config{
ServiceName: engine.Config.Name,
Namespace: os.Getenv("PLATFORM"),
Address: os.Getenv("GRPC_SERVER"),
AdvertisedAddress: os.Getenv("GRPC_ADDRESS"),
}, engine.Logger, src.RegisterGrpcRoutes)
go transportGRPC.Start(ctx)
defer transportGRPC.Shutdown(ctx)
Install protoc compiler and Go plugins:
# Install protoc
brew install protobuf # macOS
# or download from https://github.com/protocolbuffers/protobuf/releases
# Install Go plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Generate code:
cd proto
protoc --go_out=. --go-grpc_out=. *.proto
go.mod.proto files manually, don't auto-generate from entitiesConvert*() and Convert*ToEntity()When your service needs to call another service via gRPC, create a client in src/services/.
Structure:
src/services/
└── service_ordering.go # gRPC client for service-ordering
Naming:
service_{name}.go — snake_case target service name{ServiceName}Service — PascalCaseservicesPattern:
package services
import (
"context"
"os"
"time"
"github.com/mbu-id/service-ordering/proto"
"github.com/mbu-id/engine/common"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type OrderingService struct {
client proto.OrderingServiceClient
conn *grpc.ClientConn
}
func NewOrderingService() (*OrderingService, error) {
addr := os.Getenv("ORDERING_GRPC_ADDRESS")
if addr == "" {
return nil, common.ErrConfigMissing("ORDERING_GRPC_ADDRESS")
}
conn, err := grpc.Dial(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
return invoker(ctx, method, req, reply, cc, opts...)
}),
)
if err != nil {
return nil, err
}
return &OrderingService{
client: proto.NewOrderingServiceClient(conn),
conn: conn,
}, nil
}
func (s *OrderingService) Close() error {
return s.conn.Close()
}
Usage in usecase:
type OrderUsecase struct {
repo *repository.OrderRepository
orderingSvc *services.OrderingService // gRPC client to service-ordering
ctx context.Context
}
func (u *OrderUsecase) Process(ctx context.Context, id string) error {
order, err := u.repo.FindByID(id)
if err != nil {
return err
}
// Call external service via gRPC
result, err := u.orderingSvc.Ship(ctx, order.ID.String())
if err != nil {
return err
}
// ...
}
Env convention:
# .env
ORDERING_GRPC_ADDRESS=ordering-svc.internal:9090
Scaffold:
SCRIPTS=~/Works/Enigma/claude-plugins/skills/mbu-golang/scripts
cd ~/Works/Enigma/service-myapi
$SCRIPTS/scaffold-service.sh Ordering github.com/mbu-id/service-ordering/proto OrderingService
Key rules:
src/services/service_{name}.go, never ad-hoc locationsservices — All gRPC clients share the same services package{UPPER_SNAKE}_GRPC_ADDRESSinsecure.NewCredentials() (mTLS handled by mesh/infrastructure)service.Close() in OnStopPublishers emit events when entity state changes. Follow the service-payment pattern:
Structure:
src/event/publisher/
└── order.go # Event struct + publish functions
Implementation:
package publisher
import (
"context"
"time"
"github.com/mbu-id/engine/broker/rabbitmq"
"github.com/mbu-id/service-order/entity"
)
// OrderEvent is the event payload for Order events.
type OrderEvent struct {
Order *entity.Order
PublishedAt time.Time
}
// OrderCreated publishes an order.created event.
func OrderCreated(ctx context.Context, m *entity.Order) {
rabbitmq.Publish(ctx, "order.created", &OrderEvent{
Order: m,
PublishedAt: time.Now(),
})
}
// OrderUpdated publishes an order.updated event.
func OrderUpdated(ctx context.Context, m *entity.Order) {
rabbitmq.Publish(ctx, "order.updated", &OrderEvent{
Order: m,
PublishedAt: time.Now(),
})
}
// OrderDeleted publishes an order.deleted event.
func OrderDeleted(ctx context.Context, m *entity.Order) {
rabbitmq.Publish(ctx, "order.deleted", &OrderEvent{
Order: m,
PublishedAt: time.Now(),
})
}
Usage in Usecase:
package usecase
import (
"github.com/mbu-id/service-order/src/event/publisher"
)
func (u *OrderUsecase) Create(req *entity.Order) error {
if err := u.repo.Insert(req); err != nil {
return err
}
// Publish event after successful state change
publisher.OrderCreated(u.ctx, req)
return nil
}
Scaffold Publisher:
SCRIPTS=~/Works/Enigma/claude-plugins/skills/mbu-golang/scripts
cd ~/Works/Enigma/service-order
# Generate publisher boilerplate
$SCRIPTS/scaffold-publisher.sh Order
# This generates:
# - src/event/publisher/order.go (event struct + Created/Updated/Deleted functions)
Subscribers listen to events from other services. Follow the service-tracking pattern:
Structure:
src/event/subscriber/
└── order.go # Message struct + handler functions
Implementation:
package subscriber
import (
"context"
"github.com/mbu-id/service-tracking/src/usecase"
entityOrder "github.com/mbu-id/service-order/entity"
amqp "github.com/rabbitmq/amqp091-go"
)
// OrderMessage wraps the Order entity from service-order.
type OrderMessage struct {
Order *entityOrder.Order
}
// SubscribeOrderCreated handles order.created events.
func SubscribeOrderCreated(req *OrderMessage, msg amqp.Delivery) error {
uc := usecase.NewFactory().WithContext(context.Background())
err := uc.Tracking.OrderCreated(req.Order)
return msg.Ack(err == nil)
}
// SubscribeOrderUpdated handles order.updated events.
func SubscribeOrderUpdated(req *OrderMessage, msg amqp.Delivery) error {
uc := usecase.NewFactory().WithContext(context.Background())
err := uc.Tracking.OrderUpdated(req.Order)
return msg.Ack(err == nil)
}
Error Handling with Requeue:
func SubscribeOrderProcess(req *OrderMessage, msg amqp.Delivery) error {
uc := usecase.NewFactory().WithContext(context.Background())
err := uc.Tracking.ProcessOrder(req.Order)
if err != nil {
// Requeue on failure (retry)
msg.Nack(false, true)
return err
}
return msg.Ack(false)
}
Registration in src/subscriber.go:
package src
import (
"github.com/mbu-id/service-tracking/src/event/subscriber"
"github.com/mbu-id/engine/broker/rabbitmq"
)
func RegisterSubscriber() {
// service-order events
rabbitmq.Subscribe("order.created", subscriber.SubscribeOrderCreated)
rabbitmq.Subscribe("order.updated", subscriber.SubscribeOrderUpdated)
rabbitmq.Subscribe("order.deleted", subscriber.SubscribeOrderDeleted)
// service-payment events
rabbitmq.Subscribe("payment.paided", subscriber.SubscribePaymentPaided)
rabbitmq.Subscribe("payment.expired", subscriber.SubscribePaymentExpired)
}
Scaffold Subscriber:
SCRIPTS=~/Works/Enigma/claude-plugins/skills/mbu-golang/scripts
cd ~/Works/Enigma/service-tracking
# Generate subscriber boilerplate
$SCRIPTS/scaffold-subscriber.sh order Order
# This generates:
# - src/event/subscriber/order.go (message struct + handler functions)
# - Updates src/subscriber.go (registers handlers)
| Pattern | Example | Description |
|---|---|---|
{entity}.{action} | order.created | Standard CRUD events |
{entity}.{status} | payment.paided | Status change events |
{entity}.{entity}.{action} | order.route.departed | Nested entity events |
Rules:
created, updated, deleted, paidedactive, expiredmsg.Ack(err == nil) for simple casesmsg.Nack(false, true) for retriable failurescontext.Background()entityOrder "github.com/mbu-id/service-order/entity"order.v2.createdUse Events For:
Don't Use Events For:
go mod tidy && go build ./....proto file with service definitionscaffold-grpc.shcd proto && protoc --go_out=. --go-grpc_out=. *.protoproto/converter.gosrc/handler/grpc/{module}.gomain.gogo.mod if neededgo mod tidy && go build ./...scaffold-service.sh → src/services/service_{name}.go{SERVICE}_GRPC_ADDRESS to .envgo get {proto_module}service.Close() in OnStopgo mod tidy && go build ./...scaffold-publisher.shscaffold-subscriber.shAutomated task sequences for common development patterns. These workflows execute intelligently based on context and handle errors gracefully.
Trigger: User asks to "initialize new service" or "create new Go service"
Steps:
github.com/mbu-id/service-myapi)scaffold-init.sh <module_path>
main.go with OnStart/OnStop/Runsrc/handler.go for route registrationsrc/subscriber.go for event registrationsrc/permission.go for permission syncentity/ module with separate go.modgo.modgo mod tidy in root and entity directoriesgo build ./...Error Recovery:
Example:
User: "Initialize new service for warehouse management"
→ Asks for module path: github.com/mbu-id/service-warehouse
→ Runs scaffold-init.sh
→ Reports: "Service initialized. Next: add database config in main.go, create first entity"
Trigger: User provides SQL DDL or asks to "create entity from table"
Steps:
migrations/<timestamp>_create_<table>.up.sql / .down.sql from the DDL. Skip if using auto-migrate or if migration already exists.scaffold-entity.sh with table DDL
scaffold-repo.shgo.modscaffold-usecase.sh <EntityName>scaffold-factory.sh <EntityName> (updates src/usecase/factory.go)api-contract.yaml exists and has no paths for this entity:
scaffold-handler.sh <EntityName> (generates REST handlers + requests)src/handler/<module>/request_create.go and request_update.go:
company *entity.Company) in Validate()toEntity() / apply()src/handler/<module>/handler.go uses *usecase.Factory.api-contract.yaml exists, add request/response schemas that match customized fields. Dead OAS = tribal knowledge.go mod tidy && go build ./...Error Recovery:
Example:
User: "Create entity from this table:
CREATE TABLE warehouses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
address TEXT
);"
→ Executes full scaffold workflow
→ Reports: "Created Warehouse entity with REST handlers. Next: implement business logic in src/usecase/warehouse.go"
Trigger: User asks to "add gRPC for " or "create gRPC service"
Steps:
proto/<entity>.proto
scaffold-grpc.sh <EntityName>
proto/constant.goproto/converter.go stubssrc/handler/grpc/<entity>.go handler skeletonsrc/handler.go with RegisterGrpcRoutescd proto && protoc --go_out=. --go-grpc_out=. <entity>.protomain.goError Recovery:
Example:
User: "Add gRPC service for Order"
→ Checks proto/order.proto exists
→ Runs scaffold-grpc.sh Order
→ Compiles proto
→ Reports: "gRPC handler created. Implement converter.go methods and RPC logic in src/handler/grpc/order.go"
Trigger: User asks to "call service-X from gRPC" or "add gRPC client for "
Steps:
scaffold-service.sh <ServiceName> <ProtoModule> <ProtoService>
src/services/service_{name}.goNew{Name}Service() with env-based address, timeout interceptor, round-robingo get <ProtoModule>{UPPER}_GRPC_ADDRESS to .envservice.Close() call in OnStopgo build ./...Example:
User: "Add gRPC client for ordering service"
→ Runs scaffold-service.sh Ordering github.com/mbu-id/service-ordering/proto OrderingService
→ Adds ORDERING_GRPC_ADDRESS to .env
→ Reports: "gRPC client created at src/services/service_ordering.go. Next: add RPC wrappers, inject in usecase"
Trigger: User asks to "publish events for " or "add event publisher"
Steps:
scaffold-publisher.sh <EntityName>
src/event/publisher/<entity>.goError Recovery:
Example:
User: "Add event publishing for Order"
→ Runs scaffold-publisher.sh Order
→ Adds publisher.OrderCreated() calls in usecase.Create()
→ Reports: "Event publisher created. Events: order.created, order.updated, order.deleted"
Trigger: User asks to "subscribe to events" or "consume events from "
Steps:
scaffold-subscriber.sh <event_source> <EntityName>
src/event/subscriber/<event_source>.gosrc/subscriber.goError Recovery:
Example:
User: "Subscribe to order events from service-order"
→ Runs scaffold-subscriber.sh order Order
→ Generates subscriber/order.go with SubscribeOrderCreated/Updated/Deleted
→ Reports: "Subscriber created. Implement usecase methods to handle order events"
Trigger: User asks to "add endpoint for " without full entity scaffold
Steps:
src/handler/<module>/<action>.gosrc/handler.goError Recovery:
Example:
User: "Add endpoint to publish delivery plan"
→ Detects DeliveryPlan module
→ Generates src/handler/delivery_plan/publish.go
→ Adds route in handler.go
→ Reports: "POST /delivery-plan/publish endpoint created"
go build before reporting successnpx claudepluginhub mbu-id/claude-plugins --plugin skillsGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.