6.4 KiB
Domain Rules (DOM-01..09)
DOM-01: Private Entity Fields (CRITICAL)
ALL entity struct fields MUST be unexported (lowercase). Entities are "types with behavior," not data bags.
Check: Scan all structs in domain/ for exported fields. Any uppercase field name is a violation.
Correct:
type Hour struct {
hour time.Time
availability Availability
}
Wrong:
type Hour struct {
Hour time.Time // VIOLATION: exported field
Availability Availability // VIOLATION: exported field
}
Exception: DB model structs in adapters/ MAY have exported fields for serialization tags.
DOM-02: Factory Constructors (WARNING)
Entities MUST be created through factory constructors, never by direct struct literal.
Pattern: func New{Type}(args...) (*Type, error)
The constructor:
- Validates all invariants
- Returns an error if validation fails
- Returns a pointer to the new entity
Reference:
func NewAvailableHour(hour time.Time) (*Hour, error) {
if err := validateTime(hour); err != nil {
return nil, err
}
return &Hour{hour: hour, availability: Available}, nil
}
func NewTraining(uuid, userUUID, userName string, trainingTime time.Time) (*Training, error) {
if uuid == "" {
return nil, errors.New("empty training uuid")
}
if userUUID == "" {
return nil, errors.New("empty training user uuid")
}
// ... validate all fields
return &Training{uuid: uuid, userUUID: userUUID, userName: userName, time: trainingTime}, nil
}
DOM-03: MustNew Panic Constructors (INFO)
For use in tests and initialization code, provide MustNew{Type} that panics on error.
func MustNewFactory(fc FactoryConfig) Factory {
f, err := NewFactory(fc)
if err != nil {
panic(err)
}
return f
}
DOM-04: UnmarshalFromDatabase (WARNING)
Entities MUST provide an Unmarshal{Type}FromDatabase function for reconstruction from persistence. This function:
- Bypasses normal validation (data was already valid when stored)
- Accepts all fields needed to reconstruct full state
- Is used ONLY by repository adapters
Reference:
func UnmarshalHourFromDatabase(hour time.Time, availability Availability) *Hour {
return &Hour{hour: hour, availability: availability}
}
func UnmarshalTrainingFromDatabase(
uuid, userUUID, userName string,
trainingTime time.Time,
notes string,
canceled bool,
proposedNewTime time.Time,
moveProposedBy UserType,
) (*Training, error) {
return &Training{
uuid: uuid, userUUID: userUUID, userName: userName,
time: trainingTime, notes: notes, canceled: canceled,
proposedNewTime: proposedNewTime, moveProposedBy: moveProposedBy,
}, nil
}
DOM-05: Value Objects as Structs (CRITICAL)
Value objects MUST be structs wrapping a private field, NOT raw strings, ints, or type aliases.
This ensures they cannot be constructed with arbitrary values — only through validated constructors or predefined constants.
Correct:
type Availability struct {
a string // private — cannot be set directly
}
var (
Available = Availability{"available"}
NotAvailable = Availability{"not_available"}
TrainingScheduled = Availability{"training_scheduled"}
)
type UserType struct {
s string
}
var (
Trainer = UserType{"trainer"}
Attendee = UserType{"attendee"}
)
Wrong:
type Availability string // VIOLATION: can be set to any string
const (
Available Availability = "available"
NotAvailable Availability = "not_available"
)
DOM-06: IsZero Method (WARNING)
Value objects and factory structs SHOULD implement IsZero() bool to check for zero-value state.
func (a Availability) IsZero() bool {
return a == Availability{}
}
func (f Factory) IsZero() bool {
return f == Factory{}
}
DOM-07: Behavior Methods Use Domain Language (CRITICAL)
Entity methods MUST use domain-specific language, NOT generic CRUD terms.
| Forbidden | Use Instead |
|---|---|
SetStatus, Update |
ScheduleTraining, CancelTraining, MakeAvailable |
Create |
Schedule, Register, Place, Submit |
Delete |
Cancel, Archive, Revoke |
Get |
Use query noun phrases |
Reference:
func (h *Hour) ScheduleTraining() error {
if !h.IsAvailable() {
return ErrHourNotAvailable
}
h.availability = TrainingScheduled
return nil
}
func (h *Hour) CancelTraining() error { ... }
func (h *Hour) MakeAvailable() error { ... }
func (h *Hour) MakeNotAvailable() error { ... }
func (t *Training) ProposeReschedule(newTime time.Time, proposedBy UserType) error { ... }
func (t *Training) ApproveReschedule(approvedBy UserType) error { ... }
func (t *Training) RejectReschedule() error { ... }
DOM-08: String Constructors Validate Input (WARNING)
When a value object can be constructed from a string, use New{Type}FromString with validation.
func NewAvailabilityFromString(availabilityStr string) (Availability, error) {
switch availabilityStr {
case "available":
return Available, nil
case "not_available":
return NotAvailable, nil
case "training_scheduled":
return TrainingScheduled, nil
default:
return Availability{}, fmt.Errorf("unknown availability: %s", availabilityStr)
}
}
DOM-09: Factory Struct for Complex Creation (INFO)
When entity creation requires configuration or external dependencies, use a Factory struct pattern.
type FactoryConfig struct {
MaxWeeksInTheFutureToSet int
MinUtcHour int
MaxUtcHour int
}
func (c FactoryConfig) Validate() error {
var errs []error
if c.MaxWeeksInTheFutureToSet <= 0 {
errs = append(errs, errors.New("MaxWeeksInTheFutureToSet must be > 0"))
}
// ... more validations
return multierr.Combine(errs...)
}
type Factory struct {
fc FactoryConfig
}
func NewFactory(fc FactoryConfig) (Factory, error) {
if err := fc.Validate(); err != nil {
return Factory{}, err
}
return Factory{fc: fc}, nil
}
func MustNewFactory(fc FactoryConfig) Factory {
f, err := NewFactory(fc)
if err != nil {
panic(err)
}
return f
}
func (f Factory) IsZero() bool {
return f == Factory{}
}
func (f Factory) NewAvailableHour(hour time.Time) (*Hour, error) {
// uses f.fc for validation bounds
}