Skip to content

QueryBus & QueryGateway

QueryBus

type QueryBus struct { ... }

func NewQueryBus() *QueryBus

Central registry for query handlers. Handlers are keyed by the pair (QueryType, ResultType).

RegisterQueryHandler

func RegisterQueryHandler[T Query, R any](
    bus     *QueryBus,
    handler QueryHandler[T, R],
    opts    ...HandlerOption,
)

Registers a handler for query type T returning result type R.

bus := eventsourcing.NewQueryBus()
eventsourcing.RegisterQueryHandler(bus, listTasksHandler)

Validate

func (q QueryBus) Validate() error

Returns an error listing any query types that have been requested (via NewQueryGateway) but not registered. Call this at startup to catch misconfiguration early.

if err := bus.Validate(); err != nil {
    log.Fatal(err)
}

QueryHandler

type QueryHandler[T Query, R any] interface {
    HandleQuery(ctx context.Context, qry T) (R, error)
}

Generic interface for query handlers. Implement this on your read model structs, or use NewQueryHandlerFunc.

NewQueryHandlerFunc

func NewQueryHandlerFunc[T Query, R any](
    fn func(ctx context.Context, qry T) (R, error),
) QueryHandler[T, R]

Wraps a plain function as a QueryHandler:

handler := eventsourcing.NewQueryHandlerFunc(func(ctx context.Context, q GetTask) (*Task, error) {
    return store.Find(q.TaskID)
})
eventsourcing.RegisterQueryHandler(bus, handler)

QueryGateway

type QueryGateway[T Query, R any] func(ctx context.Context, qry T) (R, error)

func NewQueryGateway[T Query, R any](bus *QueryBus) QueryGateway[T, R]

Typed, callable facade over QueryBus. Call it directly like a function. It also implements QueryHandler[T, R], so it can be passed to decorators such as WithQueryTelemetry or WithQueryLogging.

gateway := eventsourcing.NewQueryGateway[GetTask, *Task](bus)

task, err := gateway(ctx, GetTask{TaskID: id})

Creating a gateway registers the (T, R) key as a "requestee", which is checked by bus.Validate().

Because QueryGateway is a function type, a service struct can hold multiple gateways for the same query type with different result shapes — each independently registered on the bus:

type service struct {
    listTasks eventsourcing.QueryGateway[TaskQuery, *Iterator[*Task]]
    findTask  eventsourcing.QueryGateway[TaskQuery, *Task]
}

Typical setup

// 1. Create bus
bus := eventsourcing.NewQueryBus()

// 2. Register handlers
eventsourcing.RegisterQueryHandler(bus, listTasksQueryHandler)
eventsourcing.RegisterQueryHandler(bus, getTaskQueryHandler)

// 3. Validate
if err := bus.Validate(); err != nil {
    log.Fatal(err)
}

// 4. Create gateways for use in HTTP handlers
listGateway := eventsourcing.NewQueryGateway[ListTasks, *TaskList](bus)
getGateway  := eventsourcing.NewQueryGateway[GetTask, *Task](bus)

// Call directly — no .HandleQuery needed
tasks, err := listGateway(ctx, ListTasks{})
task,  err := getGateway(ctx, GetTask{TaskID: id})