Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
A command bus and query bus are the routing infrastructure that transforms CQRS from a design pattern into a first-class architectural seam. Without them, you wire handlers directly in controllers, and the mapping from HTTP verbs to business operations becomes implicit and scattered. With buses, every command and query has a single registered handler, the middleware chain (logging, auth, validation, metrics) is applied uniformly, and adding a new operation is additive — register the handler, nothing else changes. The bus is also the natural place to add cross-cutting concerns like idempotency keys on commands and cache control on queries, without touching individual handler logic.
A minimal in-process command bus that dispatches commands to registered handlers with a middleware chain for logging and timing.
Use these three in order. Each builds on the one before.
In one paragraph, explain what a command bus is and why it's more useful than calling handler functions directly.
Walk me through how middleware is applied in a command bus — trace a single dispatch call through two middlewares and the final handler, showing execution order.
Given a command bus that must enforce idempotency (no command with the same idempotency key processed twice), how would you implement this as middleware without modifying any handler?
package main
import (
"fmt"
"reflect"
"time"
)
// Command marker interface.
type Command interface{ commandName() string }
type Query interface{ queryName() string }
// Handler types.
type CommandHandler func(cmd Command) error
type QueryHandler func(q Query) (any, error)
// CommandBus with middleware support.
type CommandBus struct {
handlers map[string]CommandHandler
middlewares []func(CommandHandler) CommandHandler
}
func NewCommandBus() *CommandBus { return &CommandBus{handlers: map[string]CommandHandler{}} }
func (b *CommandBus) Use(mw func(CommandHandler) CommandHandler) {
b.middlewares = append(b.middlewares, mw)
}
func (b *CommandBus) Register(name string, h CommandHandler) {
b.handlers[name] = h
}
func (b *CommandBus) Dispatch(cmd Command) error {
h, ok := b.handlers[cmd.commandName()]
if !ok {
return fmt.Errorf("no handler for command %s", cmd.commandName())
}
// Apply middleware in reverse so first-registered wraps outermost.
for i := len(b.middlewares) - 1; i >= 0; i-- {
h = b.middlewares[i](h)
}
return h(cmd)
}
// Logging middleware.
func LoggingMiddleware(next CommandHandler) CommandHandler {
return func(cmd Command) error {
start := time.Now()
err := next(cmd)
fmt.Printf("CMD %s elapsed=%s err=%v\n", cmd.commandName(), time.Since(start), err)
return err
}
}
// Example commands.
type CreatePostCmd struct { Title string }
func (c CreatePostCmd) commandName() string { return "CreatePost" }
type DeletePostCmd struct { ID string }
func (c DeletePostCmd) commandName() string { return "DeletePost" }
func main() {
bus := NewCommandBus()
bus.Use(LoggingMiddleware)
bus.Register("CreatePost", func(cmd Command) error {
c := cmd.(CreatePostCmd)
fmt.Println("Handler: creating post:", c.Title)
return nil
})
bus.Register("DeletePost", func(cmd Command) error {
d := cmd.(DeletePostCmd)
fmt.Println("Handler: deleting post:", d.ID)
return nil
})
_ = reflect.TypeOf(CreatePostCmd{}) // ensure type is used
bus.Dispatch(CreatePostCmd{Title: "Hello Bus"})
bus.Dispatch(DeletePostCmd{ID: "p42"})
}go run main.go