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
}