// Use this skill when developing, implementing, or modifying Go backend code for the Brokle platform. This includes creating services, repositories, handlers, domain entities, API endpoints, middleware, workers, or any other Go backend components. Invoke this skill at the start of backend development tasks.
| name | brokle-backend-dev |
| description | Use this skill when developing, implementing, or modifying Go backend code for the Brokle platform. This includes creating services, repositories, handlers, domain entities, API endpoints, middleware, workers, or any other Go backend components. Invoke this skill at the start of backend development tasks. |
This skill provides comprehensive guidance for Go backend development following Brokle's scalable monolith architecture with Domain-Driven Design.
cmd/server) + Background workers (cmd/worker)internal/
โโโ core/
โ โโโ domain/{domain}/ # Entities, interfaces (see internal/core/domain/)
โ โโโ services/{domain}/ # Business logic implementations
โโโ infrastructure/
โ โโโ database/
โ โ โโโ postgres/repository/
โ โ โโโ clickhouse/repository/
โ โโโ repository/ # Main repository layer
โโโ transport/http/
โ โโโ handlers/{domain}/
โ โโโ middleware/
โ โโโ server.go
โโโ app/ # DI container
โโโ workers/ # Background jobs
Primary domains in internal/core/domain/:
Pattern: Each domain has entities.go, repository.go, service.go, errors.go
Reference: See internal/core/domain/ directory for complete list and implementation status
Always use professional domain aliases for imports:
// โ
Correct
import (
"context"
"fmt"
"gorm.io/gorm"
authDomain "brokle/internal/core/domain/auth"
orgDomain "brokle/internal/core/domain/organization"
userDomain "brokle/internal/core/domain/user"
"brokle/pkg/ulid"
)
// โ Incorrect
import (
"brokle/internal/core/domain/auth"
)
Standard Domain Aliases:
| Domain | Alias | Usage |
|---|---|---|
| auth | authDomain | authDomain.User, authDomain.ErrNotFound |
| organization | orgDomain | orgDomain.Organization |
| user | userDomain | userDomain.User |
| billing | billingDomain | billingDomain.Subscription |
| observability | obsDomain | obsDomain.Trace |
Three-Layer Pattern: Repository (Domain Errors) โ Service (AppErrors) โ Handler (HTTP Response)
Repository Layer (Standard Pattern - errors.Is() for GORM):
All repositories use errors.Is() for GORM error checking (standardized for wrapped error compatibility).
โ Standard Pattern:
import "errors"
if errors.Is(err, gorm.ErrRecordNotFound) { // โ
Standardized
return nil, fmt.Errorf("context: %w", domainError)
}
โ Don't Use:
if err == gorm.ErrRecordNotFound { // โ Old pattern - don't use
Example:
func (r *userRepository) GetByID(ctx context.Context, id ulid.ULID) (*authDomain.User, error) {
var user authDomain.User
err := r.db.WithContext(ctx).Where("id = ?", id).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { // โ
Standardized pattern
return nil, fmt.Errorf("get user by ID %s: %w", id, authDomain.ErrNotFound)
}
return nil, fmt.Errorf("database query failed for user ID %s: %w", id, err)
}
return &user, nil
}
Service Layer:
func (s *userService) GetUser(ctx context.Context, id ulid.ULID) (*GetUserResponse, error) {
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
if errors.Is(err, authDomain.ErrNotFound) {
return nil, appErrors.NewNotFoundError("User not found")
}
return nil, appErrors.NewInternalError("Failed to retrieve user")
}
return &GetUserResponse{User: user}, nil
}
Handler Layer:
func (h *userHandler) GetUser(c *gin.Context) {
userID := c.Param("id")
id, err := ulid.Parse(userID)
if err != nil {
response.Error(c, appErrors.NewValidationError("Invalid user ID format", "id must be a valid ULID"))
return
}
resp, err := h.userService.GetUser(c.Request.Context(), id)
if err != nil {
response.Error(c, err) // Automatic HTTP mapping
return
}
response.Success(c, resp)
}
Common AppError Constructors (see pkg/errors/errors.go for complete list):
appErrors.NewValidationError(message, details string)appErrors.NewNotFoundError(resource string)appErrors.NewConflictError(message string)appErrors.NewUnauthorizedError(message string)appErrors.NewForbiddenError(message string)appErrors.NewInternalError(message string, err error)appErrors.NewRateLimitError(message string)Two Authentication Systems:
SDK Routes (/v1/*): API Key Authentication
// Middleware: SDKAuthMiddleware
// Extract context: middleware.GetSDKAuthContext(c)
// Rate limiting: API key-based
sdkRoutes := r.Group("/v1")
sdkRoutes.Use(sdkAuthMiddleware.RequireSDKAuth())
sdkRoutes.Use(rateLimitMiddleware.RateLimitByAPIKey())
Dashboard Routes (/api/v1/*): JWT Authentication
// Middleware: Authentication()
// Extract user: middleware.GetUserID(c)
// Rate limiting: IP + user-based
// Middleware instances injected via DI (server.go:219-223)
protected := router.Group("")
protected.Use(s.authMiddleware.RequireAuth()) // Instance method - JWT validation
protected.Use(s.csrfMiddleware.ValidateCSRF()) // Instance method - CSRF protection
protected.Use(s.rateLimitMiddleware.RateLimitByUser()) // Instance method - User rate limiting
API Key Format: bk_{40_char_random} with SHA-256 hashing
Test Business Logic, Not Framework Behavior
โ What to Test:
โ What NOT to Test:
Test Pattern (Table-Driven):
func TestUserService_CreateUser(t *testing.T) {
tests := []struct {
name string
input *CreateUserRequest
mockSetup func(*MockUserRepository)
expectedErr error
checkResult func(*testing.T, *CreateUserResponse)
}{
{
name: "success - valid user",
input: &CreateUserRequest{
Email: "test@example.com",
Name: "Test User",
},
mockSetup: func(repo *MockUserRepository) {
repo.On("Create", mock.Anything, mock.Anything).Return(nil)
},
expectedErr: nil,
checkResult: func(t *testing.T, resp *CreateUserResponse) {
assert.NotNil(t, resp)
assert.NotEqual(t, ulid.ULID{}, resp.User.ID)
},
},
// More test cases...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo := new(MockUserRepository)
tt.mockSetup(mockRepo)
service := NewUserService(mockRepo)
result, err := service.CreateUser(context.Background(), tt.input)
if tt.expectedErr != nil {
assert.ErrorIs(t, err, tt.expectedErr)
} else {
assert.NoError(t, err)
}
if tt.checkResult != nil {
tt.checkResult(t, result)
}
mockRepo.AssertExpectations(t)
})
}
}
See code-templates.md for complete repository, service, and handler templates.
# Start development stack
make dev # Full stack (server + worker)
make dev-server # HTTP server only
make dev-worker # Workers only
# Testing
make test # All tests
make test-unit # Unit tests only
make test-integration # Integration tests
# Database
make migrate-up # Run migrations
make migrate-status # Check status
make seed-dev # Load dev data
# Code quality
make lint # Lint all code
make fmt # Format code
When working on specific areas, invoke these specialized skills:
brokle-domain-architecture skillbrokle-api-routes skillbrokle-error-handling skillbrokle-testing skillbrokle-migration-workflow skillCLAUDE.mddocs/development/ERROR_HANDLING_GUIDE.mddocs/development/DOMAIN_ALIAS_PATTERNS.mddocs/development/API_DEVELOPMENT_GUIDE.mddocs/TESTING.mdAll entities must be organization-scoped:
type Project struct {
ID ulid.ULID
OrganizationID ulid.ULID // Required for multi-tenancy
Name string
// ... other fields
}
Use build tags for enterprise features:
# OSS build
go build ./cmd/server
# Enterprise build
go build -tags="enterprise" ./cmd/server
Enterprise features go in internal/ee/ with stub implementations for OSS.
Creating new functionality?
brokle-domain-architecture skillbrokle-api-routes skillbrokle-migration-workflow skillDebugging errors?
response.Error() in handlersAdding tests?
AssertExpectations(t)