| name | brokle-domain-architecture |
| description | Use this skill when working with Brokle's domain-driven architecture, including creating new domains, modifying domain entities, designing cross-domain interactions, refactoring domain boundaries, or implementing complex domain logic. This is a specialized architectural skill. |
Brokle Domain Architecture Skill
Expert guidance for Brokle's Domain-Driven Design (DDD) architecture.
Domains
Primary domains in internal/core/domain/:
| Domain | Purpose |
|---|---|
| auth | Authentication, sessions, API keys |
| billing | Usage tracking, subscriptions |
| common | Shared transaction patterns, utilities |
| gateway | AI provider routing |
| observability | Traces, observations, quality scores |
| organization | Multi-tenant org management |
| user | User management and profiles |
Structure: Each domain has entities.go, repository.go, service.go, errors.go, types.go
Reference: List domains with ls -1 internal/core/domain/ to see current implementation status
Domain Layer Structure
internal/core/domain/{domain}/
├── entities.go # Domain entities
├── repository.go # Repository interfaces
├── service.go # Service interfaces
├── errors.go # Domain-specific errors
├── types.go # Domain types and enums
└── validators.go # Domain validation logic
Entity Pattern
// internal/core/domain/auth/entities.go
package auth
import (
"time"
"brokle/pkg/ulid"
)
type User struct {
ID ulid.ULID
Email string
Name string
Status UserStatus
Role Role
CreatedAt time.Time
UpdatedAt time.Time
}
// Domain enums
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusInactive UserStatus = "inactive"
UserStatusSuspended UserStatus = "suspended"
)
type Role string
const (
RoleOwner Role = "owner"
RoleAdmin Role = "admin"
RoleUser Role = "user"
RoleViewer Role = "viewer"
)
Domain Errors
// internal/core/domain/auth/errors.go
package auth
import "errors"
var (
ErrNotFound = errors.New("user not found")
ErrAlreadyExists = errors.New("user already exists")
ErrInvalidCredentials = errors.New("invalid credentials")
ErrSessionExpired = errors.New("session expired")
)
Repository Interfaces
// internal/core/domain/auth/repository.go
package auth
import (
"context"
"brokle/pkg/ulid"
)
type UserRepository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id ulid.ULID) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id ulid.ULID) error
List(ctx context.Context, filter UserFilter) ([]*User, error)
}
Service Interfaces
// internal/core/domain/auth/service.go
package auth
import "context"
type AuthService interface {
Register(ctx context.Context, req *RegisterRequest) (*RegisterResponse, error)
Login(ctx context.Context, req *LoginRequest) (*LoginResponse, error)
Logout(ctx context.Context, token string) error
ValidateSession(ctx context.Context, token string) (*User, error)
}
Multi-Tenant Scoping Patterns
NOT all entities have organization_id - scoping depends on entity type:
1. Organization-Scoped Entities (Direct organization_id)
// organization/organization.go:54-57
type Project struct {
ID ulid.ULID
OrganizationID ulid.ULID `json:"organization_id" gorm:"type:char(26);not null"`
Name string
}
// organization/organization.go:37-39
type Member struct {
OrganizationID ulid.ULID `json:"organization_id" gorm:"type:char(26);not null;primaryKey"`
UserID ulid.ULID
}
2. Project-Scoped Entities (Organization via Project)
// auth/auth.go:94 - APIKey is project-scoped
type APIKey struct {
ID ulid.ULID
ProjectID ulid.ULID `json:"project_id" gorm:"type:char(26);not null;index"`
// Organization derived via Project join
}
3. Scoped Entities (Flexible Scoping)
// auth/auth.go:113-114 - Role uses flexible scope_type pattern
type Role struct {
ID ulid.ULID
ScopeType string `json:"scope_type" gorm:"size:20;not null"` // "organization", "project", "global"
ScopeID *ulid.ULID `json:"scope_id,omitempty" gorm:"type:char(26);index"`
}
4. Global Entities (No organization_id)
// user/user.go:14-39 - User is global with optional org reference
type User struct {
ID ulid.ULID
Email string
DefaultOrganizationID *ulid.ULID `json:"default_organization_id,omitempty" gorm:"type:char(26)"`
// NOT required - users can belong to multiple orgs via Member table
}
// organization/organization.go:16 - Organization IS the tenant
type Organization struct {
ID ulid.ULID
Name string
// No organization_id - it IS the organization
}
Reference Files:
- Organization-scoped:
internal/core/domain/organization/organization.go:54-73 - Project-scoped:
internal/core/domain/auth/auth.go:94 - Scoped (flexible):
internal/core/domain/auth/auth.go:113-114 - Global:
internal/core/domain/user/user.go:14-39
Cross-Domain Relationships
// Example: Organization domain referencing User domain
package organization
import (
userDomain "brokle/internal/core/domain/user"
)
type Member struct {
ID ulid.ULID
OrganizationID ulid.ULID
UserID ulid.ULID // References user domain
Role string
Status MemberStatus
}
// Service can accept interfaces from other domains
type OrganizationService struct {
orgRepo OrganizationRepository
userRepo userDomain.UserRepository // Cross-domain dependency
memberRepo MemberRepository
}
Creating a New Domain
Step 1: Create Domain Structure
mkdir -p internal/core/domain/my-domain
touch internal/core/domain/my-domain/{entities,repository,service,errors,types}.go
Step 2: Define Entities
// entities.go
package mydomain
import (
"time"
"brokle/pkg/ulid"
)
type MyEntity struct {
ID ulid.ULID
OrganizationID ulid.ULID // Always include for multi-tenancy
Name string
Status MyStatus
CreatedAt time.Time
UpdatedAt time.Time
}
Step 3: Define Domain Errors
// errors.go
package mydomain
import "errors"
var (
ErrNotFound = errors.New("entity not found")
ErrAlreadyExists = errors.New("entity already exists")
ErrInvalidInput = errors.New("invalid input")
)
Step 4: Define Repository Interface
// repository.go
package mydomain
import (
"context"
"brokle/pkg/ulid"
)
type MyEntityRepository interface {
Create(ctx context.Context, entity *MyEntity) error
GetByID(ctx context.Context, id ulid.ULID) (*MyEntity, error)
Update(ctx context.Context, entity *MyEntity) error
Delete(ctx context.Context, id ulid.ULID) error
}
Step 5: Define Service Interface
// service.go
package mydomain
import "context"
type MyDomainService interface {
CreateEntity(ctx context.Context, req *CreateEntityRequest) (*CreateEntityResponse, error)
GetEntity(ctx context.Context, id ulid.ULID) (*GetEntityResponse, error)
}
Step 6: Implement Service
In internal/core/services/my-domain/
Step 7: Implement Repository
In internal/infrastructure/repository/my-domain/
Step 8: Register in DI Container
In internal/app/app.go
Domain Validation
// validators.go
package auth
import (
"errors"
"regexp"
)
func (u *User) Validate() error {
if u.Email == "" {
return errors.New("email is required")
}
if !isValidEmail(u.Email) {
return errors.New("invalid email format")
}
if u.Name == "" {
return errors.New("name is required")
}
return nil
}
func isValidEmail(email string) bool {
return regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`).MatchString(email)
}
Key Principles
- Domain Purity: Domain layer has no external dependencies
- Multi-Tenancy: All entities scoped by organization
- Domain Errors: Use domain-specific errors
- Validation: Domain entities validate themselves
- Interfaces: Define repository and service interfaces in domain
- Cross-Domain: Use domain aliases for cross-domain references
References
- Existing domains in
internal/core/domain/for patterns CLAUDE.md- Architecture overviewdocs/development/PATTERNS.md- Domain patterns