Skip to content

CQRS Buses

flux enforces strict Command Query Responsibility Segregation (CQRS) through three distinct message buses. You should never mix Intents, Facts, and Data Retrieval.

Command Bus (1:1)

Commands are Intents to change system state. They are dispatched to exactly one handler. If no handler exists, or multiple exist, it is an error.

go
cmdBus := command.New()
command.Register(cmdBus, myHandler)

// Dispatching
command.Execute(ctx, cmdBus, AddItemCommand{ID: "123"})

Event Bus (1:N)

Events represent Facts that have already occurred. They are dispatched to many handlers (Pub/Sub). Projections and Workflows listen to this bus.

go
eventBus := event.New()
event.Register(eventBus, myProjectionHandler)
event.Register(eventBus, myWorkflowHandler)

// Publishing
eventBus.Publish(ctx, ItemAdded{ID: "123"})

Query Bus (1:1 with Result)

Queries request data without modifying state. They are routed to exactly one handler and return a typed result.

go
queryBus := query.New()
query.RegisterHandler(queryBus, myQueryHandler)

// Asking
result := queryBus.Ask(ctx, GetItemQuery{ID: "123"})

Middleware Chaining (Interceptors)

All three dispatchers (command, query, and event) natively support middleware chaining.

This allows developers to inject cross-cutting concerns like global telemetry, authentication barriers, database transaction management, and OpenTelemetry spans without polluting domain logic.

Middlewares are registered using the Use() method:

go
bus.Use(func(ctx command.Context, cmd any, next func(command.Context, any) error) error {
    ctx.Logger().Info("Executing command", "type", fmt.Sprintf("%T", cmd))
    
    // Call the next middleware in the chain (or the final handler)
    err := next(ctx, cmd)
    
    return err
})

Middlewares execute in the exact order they are provided, chaining perfectly down to the underlying handler.