threedotslab/references/rules-domain.md

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
}