6.9 KiB
CQRS Rules (CQRS-01..10)
CQRS-01: Command Struct Pattern (CRITICAL)
Commands MUST be:
- Named with imperative verb + noun (domain language, NOT CRUD)
- Plain data structs (no methods, no interfaces)
- Their handler returns
erroronly — no data
Correct:
type ScheduleTraining struct {
Hour time.Time
}
type CancelTraining struct {
Hour time.Time
}
type MakeHoursAvailable struct {
Hours []time.Time
}
Wrong:
type CreateTraining struct { ... } // VIOLATION: CRUD naming
type UpdateHour struct { ... } // VIOLATION: CRUD naming
CQRS-02: Query Struct Pattern (CRITICAL)
Queries MUST be:
- Named with noun phrases (NOT "Get" + noun)
- Plain data structs
- Their handler returns
(ResultType, error)
Correct:
type AvailableHours struct {
From time.Time
To time.Time
}
type HourAvailability struct {
Hour time.Time
}
Wrong:
type GetAvailableHours struct { ... } // VIOLATION: "Get" prefix
type FetchTrainings struct { ... } // VIOLATION: "Fetch" prefix
CQRS-03: Exported Handler Type Alias (WARNING)
Each handler file MUST define an exported type alias using the generic decorator interface.
// For commands:
type CancelTrainingHandler decorator.CommandHandler[CancelTraining]
// For queries:
type AvailableHoursHandler decorator.QueryHandler[AvailableHours, []Date]
This allows callers to depend on the decorated interface, not the concrete struct.
CQRS-04: Unexported Handler Struct (WARNING)
The concrete handler struct MUST be unexported (lowercase). It holds dependencies injected via constructor.
type cancelTrainingHandler struct {
hourRepo hour.Repository
}
type availableHoursHandler struct {
readModel AvailableHoursReadModel
}
CQRS-05: Constructor Wraps with Decorators (WARNING)
Handler constructors MUST wrap the concrete handler with ApplyCommandDecorators or ApplyQueryDecorators.
func NewCancelTrainingHandler(
hourRepo hour.Repository,
logger *logrus.Entry,
metricsClient decorator.MetricsClient,
) CancelTrainingHandler {
return decorator.ApplyCommandDecorators[CancelTraining](
cancelTrainingHandler{hourRepo: hourRepo},
logger,
metricsClient,
)
}
func NewAvailableHoursHandler(
readModel AvailableHoursReadModel,
logger *logrus.Entry,
metricsClient decorator.MetricsClient,
) AvailableHoursHandler {
return decorator.ApplyQueryDecorators[AvailableHours, []Date](
availableHoursHandler{readModel: readModel},
logger,
metricsClient,
)
}
CQRS-06: Constructor Nil-Checks with Panic (WARNING)
Handler constructors SHOULD nil-check all injected dependencies and panic if any are nil. This is a fail-fast pattern — misconfiguration is caught at startup, not at runtime.
func NewCancelTrainingHandler(
hourRepo hour.Repository,
logger *logrus.Entry,
metricsClient decorator.MetricsClient,
) CancelTrainingHandler {
if hourRepo == nil {
panic("nil hourRepo")
}
if logger == nil {
panic("nil logger")
}
if metricsClient == nil {
panic("nil metricsClient")
}
return decorator.ApplyCommandDecorators[CancelTraining](
cancelTrainingHandler{hourRepo: hourRepo},
logger,
metricsClient,
)
}
CQRS-07: Application Struct (CRITICAL)
The app/app.go file MUST define an Application struct that bundles Commands and Queries sub-structs.
type Application struct {
Commands Commands
Queries Queries
}
type Commands struct {
CancelTraining command.CancelTrainingHandler
ScheduleTraining command.ScheduleTrainingHandler
MakeHoursAvailable command.MakeHoursAvailableHandler
MakeHoursUnavailable command.MakeHoursUnavailableHandler
}
type Queries struct {
HourAvailability query.HourAvailabilityHandler
TrainerAvailableHours query.AvailableHoursHandler
}
Check: Look for app.go in the app/ package. Verify it has Application, Commands, and Queries types.
CQRS-08: Read Model Interface for Queries (WARNING)
Query handlers SHOULD depend on a dedicated read model interface, not the write repository.
// In app/query/ — defines what it needs
type AvailableHoursReadModel interface {
AvailableHours(ctx context.Context, from, to time.Time) ([]Date, error)
}
This keeps reads and writes separate. The same adapter may implement both the write Repository and a read model interface, but the query handler only knows about the read model.
CQRS-09: Command/Query Separation (CRITICAL)
- Commands MUST modify state and return only
error - Queries MUST read state and return
(ResultType, error)— they MUST NOT modify state
A handler that both reads and writes violates CQRS.
Check: Command handlers returning anything besides error is a violation. Query handlers calling mutation methods on repositories is a violation.
CQRS-10: No Business Logic in Handlers (WARNING)
Handlers are orchestrators. Business rules live in domain entities.
Correct — handler delegates to domain:
func (h cancelTrainingHandler) Handle(ctx context.Context, cmd CancelTraining) error {
return h.hourRepo.UpdateHour(ctx, cmd.Hour, func(h *hour.Hour) (*hour.Hour, error) {
if err := h.CancelTraining(); err != nil { // domain method
return nil, err
}
return h, nil
})
}
Wrong — business logic in handler:
func (h cancelTrainingHandler) Handle(ctx context.Context, cmd CancelTraining) error {
hour, _ := h.hourRepo.GetHour(ctx, cmd.Hour)
if hour.Availability != "training_scheduled" { // VIOLATION: logic belongs in domain
return errors.New("no training to cancel")
}
hour.Availability = "available" // VIOLATION: direct field mutation
return h.hourRepo.Save(ctx, hour)
}
Complete Handler File Template
Every command/query handler file follows this 4-component pattern:
package command
// 1. Command struct
type CancelTraining struct {
Hour time.Time
}
// 2. Exported handler type (alias to decorator interface)
type CancelTrainingHandler decorator.CommandHandler[CancelTraining]
// 3. Unexported concrete handler
type cancelTrainingHandler struct {
hourRepo hour.Repository
}
// 4. Constructor with nil-checks + decorator wrapping
func NewCancelTrainingHandler(
hourRepo hour.Repository,
logger *logrus.Entry,
metricsClient decorator.MetricsClient,
) CancelTrainingHandler {
if hourRepo == nil {
panic("nil hourRepo")
}
return decorator.ApplyCommandDecorators[CancelTraining](
cancelTrainingHandler{hourRepo: hourRepo},
logger,
metricsClient,
)
}
// Handle method on unexported struct
func (h cancelTrainingHandler) Handle(ctx context.Context, cmd CancelTraining) error {
// orchestration only — delegate to domain
}