Brokle Backend Development Skill
This skill provides comprehensive guidance for Go backend development following Brokle's scalable monolith architecture with Domain-Driven Design.
Architecture Overview
Scalable Monolith with Independent Scaling
- •Separate Binaries: HTTP server (
cmd/server) + Background workers (cmd/worker) - •Shared Codebase: Single codebase with modular DI container
- •Multi-Database Strategy: PostgreSQL (transactional) + ClickHouse (analytics) + Redis (cache/queues)
- •Domain-Driven Design: Clean layer separation with domain-driven architecture
Application Structure
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
Domains
Primary domains in internal/core/domain/:
- •auth - Authentication, sessions, API keys
- •billing - Usage tracking, subscriptions
- •common - Shared transaction patterns, utilities
- •gateway - AI provider routing and management
- •observability - Traces, spans, quality scores
- •organization - Multi-tenant org management
- •user - User management and profiles
Pattern: Each domain has entities.go, repository.go, service.go, errors.go
Reference: See internal/core/domain/ directory for complete list and implementation status
Critical Development Patterns
1. Domain Alias Pattern (MANDATORY)
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 |
2. Industrial Error Handling (MANDATORY)
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)
3. Authentication Patterns
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
4. Testing Philosophy (CRITICAL)
Test Business Logic, Not Framework Behavior
✅ What to Test:
- •Complex business logic and calculations
- •Batch operations and orchestration workflows
- •Error handling patterns and retry mechanisms
- •Analytics, aggregations, and metrics
- •Multi-step operations with dependencies
❌ What NOT to Test:
- •Simple CRUD operations without business logic
- •Field validation (already in domain layer)
- •Trivial constructors and getters
- •Framework behavior (ULID, time.Now(), errors.Is)
- •Static constant definitions
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)
})
}
}
Code Templates
See code-templates.md for complete repository, service, and handler templates.
Development Commands
# 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
Key References
When working on specific areas, invoke these specialized skills:
- •Domain Architecture: Use
brokle-domain-architectureskill - •API Routes: Use
brokle-api-routesskill - •Error Handling: Use
brokle-error-handlingskill - •Testing: Use
brokle-testingskill - •Migrations: Use
brokle-migration-workflowskill
Documentation
- •Architecture Overview:
CLAUDE.md - •Error Handling:
docs/development/ERROR_HANDLING_GUIDE.md - •Domain Patterns:
docs/development/DOMAIN_ALIAS_PATTERNS.md - •API Development:
docs/development/API_DEVELOPMENT_GUIDE.md - •Testing Guide:
docs/TESTING.md
Development Workflow
- •Analyze First: Use Read, Grep, Glob to understand existing patterns
- •Find Similar Code: Search for existing implementations
- •Follow Patterns: Match the established architecture
- •Test Business Logic: Write pragmatic tests for complex logic only
- •Review Errors: Ensure proper error handling across all layers
Multi-Tenant Scoping
All entities must be organization-scoped:
type Project struct {
ID ulid.ULID
OrganizationID ulid.ULID // Required for multi-tenancy
Name string
// ... other fields
}
Enterprise Edition Pattern
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.
Quick Decision Tree
Creating new functionality?
- •Is it a new domain? → Use
brokle-domain-architectureskill - •Is it an API endpoint? → Use
brokle-api-routesskill - •Is it complex business logic? → Follow service layer pattern + write tests
- •Does it need database schema? → Use
brokle-migration-workflowskill
Debugging errors?
- •Check error handling layer (repo vs service vs handler)
- •Verify domain alias imports
- •Ensure AppError constructors in services
- •Confirm
response.Error()in handlers
Adding tests?
- •Is it complex business logic? → Write tests
- •Is it simple CRUD? → Skip tests
- •Use table-driven test pattern
- •Mock complete interfaces
- •Verify expectations with
AssertExpectations(t)