threedotslab/references/rules-repository.md

5.2 KiB

Repository Rules (REPO-01..07)

REPO-01: Interface Defined in Domain (CRITICAL)

Repository interfaces MUST be defined in the domain package, next to the entity they persist. This follows the Dependency Inversion Principle — the domain defines what it needs, adapters implement it.

Correct:

// domain/hour/repository.go
package hour

type Repository interface {
    GetHour(ctx context.Context, hourTime time.Time) (*Hour, error)
    UpdateHour(ctx context.Context, hourTime time.Time,
        updateFn func(h *Hour) (*Hour, error)) error
}

Wrong:

// adapters/repository.go  ← VIOLATION: interface in adapter layer
package adapters

type HourRepository interface { ... }

Check: Grep domain/ for type.*Repository interface. Grep adapters/ for the same — if found in adapters, it's a violation.


REPO-02: Update Callback Pattern (WARNING)

Repository update methods SHOULD use a callback/closure pattern. The repository handles transaction lifecycle; the callback handles domain logic.

// Interface
UpdateHour(ctx context.Context, hourTime time.Time,
    updateFn func(h *Hour) (*Hour, error)) error

// Usage in handler
err := h.hourRepo.UpdateHour(ctx, cmd.Hour, func(h *hour.Hour) (*hour.Hour, error) {
    if err := h.CancelTraining(); err != nil {
        return nil, err
    }
    return h, nil
})

Benefits:

  • Transaction scope is clear
  • Domain logic is isolated from persistence details
  • Enables optimistic locking, retries, etc. transparently

REPO-03: Separate DB Model Structs (WARNING)

Adapter implementations MUST use separate structs for database representation. Domain entities should NOT have serialization tags.

Correct:

// adapters/ — DB model
type mysqlHour struct {
    ID           int       `db:"id"`
    Hour         time.Time `db:"hour"`
    Availability string    `db:"availability"`
}

// or for Firestore (needs exported fields for tags)
type TrainingModel struct {
    UUID            string    `firestore:"Uuid"`
    UserUUID        string    `firestore:"UserUuid"`
    Time            time.Time `firestore:"Time"`
}

// Conversion in adapter
func (r *MySQLHourRepository) toHour(m mysqlHour) (*hour.Hour, error) {
    availability, err := hour.NewAvailabilityFromString(m.Availability)
    if err != nil {
        return nil, err
    }
    return hour.UnmarshalHourFromDatabase(m.Hour, availability), nil
}

Wrong:

// domain/hour/hour.go
type Hour struct {
    Hour         time.Time `json:"hour" db:"hour"`  // VIOLATION: DB tags on domain entity
    Availability string    `json:"availability"`     // VIOLATION: serialization concern in domain
}

REPO-04: Adapter Constructor Naming (INFO)

Repository adapter constructors follow: New{Technology}{Entity}Repository

func NewFirestoreHourRepository(client *firestore.Client, factory hour.Factory) *FirestoreHourRepository
func NewMySQLHourRepository(db *sqlx.DB) *MySQLHourRepository
func NewMemoryHourRepository(factory hour.Factory) *MemoryHourRepository

REPO-05: Technology Suffix Naming (INFO)

Adapter types use technology as a suffix/prefix to distinguish implementations.

type FirestoreHourRepository struct { ... }
type MySQLHourRepository struct { ... }
type MemoryHourRepository struct { ... }

// For external service clients
type TrainerGrpc struct { ... }
type UsersGrpc struct { ... }

REPO-06: Shared Test Suite (WARNING)

Repository tests SHOULD run the same test logic against ALL implementations (memory, MySQL, Firestore, etc.). This ensures behavioral consistency.

Pattern:

func createRepositories(t *testing.T) []Repository {
    return []Repository{
        {Name: "Firebase", Repository: newFirebaseRepository(t)},
        {Name: "MySQL", Repository: newMySQLRepository(t)},
        {Name: "memory", Repository: adapters.NewMemoryHourRepository(testFactory)},
    }
}

func TestRepository(t *testing.T) {
    repositories := createRepositories(t)
    for i := range repositories {
        r := repositories[i]  // capture loop variable
        t.Run(r.Name, func(t *testing.T) {
            t.Parallel()
            testUpdateHour(t, r.Repository)
            testUpdateHour_parallel(t, r.Repository)
        })
    }
}

Check: Look for test files in adapters/ that test repository implementations. Verify they use a shared test function or table-driven approach.


REPO-07: UnmarshalFromDatabase Usage (WARNING)

Adapter implementations MUST use the entity's UnmarshalFromDatabase function to reconstruct domain objects from persistence, not the regular constructor.

Correct:

func (r *FirestoreHourRepository) toHour(doc *firestore.DocumentSnapshot) (*hour.Hour, error) {
    var m HourModel
    if err := doc.DataTo(&m); err != nil {
        return nil, err
    }
    availability, err := hour.NewAvailabilityFromString(m.Availability)
    if err != nil {
        return nil, err
    }
    return hour.UnmarshalHourFromDatabase(m.Hour, availability), nil
}

Wrong:

func (r *FirestoreHourRepository) toHour(doc *firestore.DocumentSnapshot) (*hour.Hour, error) {
    // VIOLATION: using business constructor for DB reconstruction
    return hour.NewAvailableHour(m.Hour)  // This re-validates and may reject valid stored data
}