threedotslab/templates/entity.md

3.5 KiB

Domain Entity Scaffold Template

Generate a domain entity with factory constructor, value objects, and errors.

Placeholders

  • {{Name}} — PascalCase entity name (e.g., Training, Hour, Order)
  • {{name}} — camelCase (e.g., training)
  • {{name_lower}} — all lowercase package name (e.g., training)
  • {{name_snake}} — snake_case (e.g., training)

File: domain/{{name_lower}}/{{name_snake}}.go

package {{name_lower}}

import (
	"errors"
	"time"
)

// {{Name}} is the aggregate root for the {{name_lower}} domain.
type {{Name}} struct {
	uuid      string
	createdAt time.Time
	// TODO: Add domain fields (all private)
	// status    Status  // value object, not raw string
}

// New{{Name}} creates a new {{Name}} with validated invariants.
func New{{Name}}(uuid string) (*{{Name}}, error) {
	if uuid == "" {
		return nil, errors.New("empty {{name_lower}} uuid")
	}

	return &{{Name}}{
		uuid:      uuid,
		createdAt: time.Now(),
	}, nil
}

// Unmarshal{{Name}}FromDatabase reconstructs a {{Name}} from persistence.
// Bypasses validation — data was valid when stored.
func Unmarshal{{Name}}FromDatabase(
	uuid string,
	createdAt time.Time,
	// TODO: Add all persisted fields
) *{{Name}} {
	return &{{Name}}{
		uuid:      uuid,
		createdAt: createdAt,
	}
}

// Accessor methods — expose state without allowing mutation.

func (t {{Name}}) UUID() string {
	return t.uuid
}

func (t {{Name}}) CreatedAt() time.Time {
	return t.createdAt
}

// TODO: Add behavior methods using domain language.
// Examples:
//
// func (t *{{Name}}) Approve() error {
//     if t.status != Pending {
//         return ErrNotPending
//     }
//     t.status = Approved
//     return nil
// }
//
// func (t *{{Name}}) Cancel() error { ... }
// func (t *{{Name}}) Submit(details string) error { ... }

File: domain/{{name_lower}}/errors.go

package {{name_lower}}

import "errors"

// Sentinel errors — simple, no context needed.
var (
	ErrNotFound = errors.New("{{name_lower}} not found")
	// TODO: Add domain-specific errors
	// ErrAlreadyCanceled = errors.New("{{name_lower}} already canceled")
	// ErrNotPending      = errors.New("{{name_lower}} is not in pending state")
)

// Typed errors — carry context for logging/display.
// Example:
//
// type ForbiddenError struct {
//     RequestingUserUUID string
//     OwnerUUID          string
// }
//
// func (e ForbiddenError) Error() string {
//     return fmt.Sprintf("user %s cannot access {{name_lower}} owned by %s",
//         e.RequestingUserUUID, e.OwnerUUID)
// }

File: domain/{{name_lower}}/status.go (Optional Value Object)

package {{name_lower}}

import "fmt"

// Status is a value object — cannot be constructed with arbitrary values.
type Status struct {
	s string
}

var (
	Pending  = Status{"pending"}
	Approved = Status{"approved"}
	Canceled = Status{"canceled"}
)

func NewStatusFromString(s string) (Status, error) {
	switch s {
	case "pending":
		return Pending, nil
	case "approved":
		return Approved, nil
	case "canceled":
		return Canceled, nil
	default:
		return Status{}, fmt.Errorf("unknown {{name_lower}} status: %s", s)
	}
}

func (s Status) String() string {
	return s.s
}

func (s Status) IsZero() bool {
	return s == Status{}
}

Post-Creation Checklist

  • All struct fields are private (unexported)
  • Factory constructor validates all invariants
  • UnmarshalFromDatabase accepts all persisted fields
  • Value objects are struct wrappers, not type aliases
  • Behavior methods use domain language, not CRUD
  • Errors are sentinel vars or typed structs