Picture this: your Go application is like a overworked waiter at a Michelin-star restaurant. It’s taking orders (writes), serving dishes (reads), refilling drinks (updates), and dealing with “I-want-to-speak-to-the-manager” Karens (deletes) - all while wearing those uncomfortable dress shoes. Enter CQRS: the architectural equivalent of hiring a dedicated chef and sommelier team. Let’s cook up some scalable goodness!
Why Your Code Needs Therapy (and CQRS)
Traditional CRUD is like using a Swiss Army knife to perform brain surgery - possible, but messy. CQRS (Command Query Responsibility Segregation) splits your application into:
- Command Side: The “write” chefs handling state changes
- Query Side: The “read” waitstaff serving data appetizers
This separation lets you optimize each side independently - like having turbocharged sports cars for writes and luxury limos for reads.
Brewing CQRS in Go: Step-by-Step Recipe
Step 1: Command Your Kitchen
Let’s create our command structures. Think of these as recipe cards for state changes:
type CreateOrderCommand struct {
OrderID uuid.UUID
UserID uuid.UUID
Items []Item
SpecialNote string `json:"special_note" validate:"omitempty,max=500"`
}
type CancelOrderCommand struct {
OrderID uuid.UUID
Reason string `json:"reason" validate:"required"`
CancelledBy uuid.UUID
}
Our command handler acts as the head chef:
type OrderCommandHandler struct {
eventProducer EventProducer
}
func (h *OrderCommandHandler) Handle(ctx context.Context, cmd interface{}) error {
switch command := cmd.(type) {
case CreateOrderCommand:
if err := validate.Struct(command); err != nil {
return fmt.Errorf("invalid order: %w", err)
}
event := OrderCreatedEvent{
OrderID: command.OrderID,
Timestamp: time.Now().UTC(),
MenuItems: command.Items,
SpecialNote: command.SpecialNote,
}
return h.eventProducer.Publish(ctx, event)
// Handle other commands...
}
return ErrUnknownCommand
}
Step 2: Kafka as Your Event Sous-Chef
Let’s set up our Kafka producer to broadcast order events:
type KafkaEventProducer struct {
producer sarama.SyncProducer
topic string
}
func (k *KafkaEventProducer) Publish(ctx context.Context, event Event) error {
jsonData, _ := json.Marshal(event)
msg := &sarama.ProducerMessage{
Topic: k.topic,
Value: sarama.ByteEncoder(jsonData),
}
_, _, err := k.producer.SendMessage(msg)
if err != nil {
return fmt.Errorf("failed to send Kafka message: %w", err)
}
return nil
}
Pro tip: Add correlation IDs to your events - they’re like recipe tracking numbers for your distributed transactions.
Step 3: Query Models - The Food Critics’ Table
Our read model uses materialized views optimized for specific queries:
type OrderProjection struct {
db *sqlx.DB
}
func (p *OrderProjection) HandleOrderCreated(event OrderCreatedEvent) error {
return p.db.Exec(`
INSERT INTO active_orders
(id, user_id, items, special_notes, status)
VALUES ($1, $2, $3, $4, 'PENDING')`,
event.OrderID,
event.UserID,
event.Items,
event.SpecialNote,
)
}
func (p *OrderProjection) GetActiveOrders(userID uuid.UUID) ([]OrderSummary, error) {
var orders []OrderSummary
err := p.db.Select(&orders, `
SELECT id, items, status
FROM active_orders
WHERE user_id = $1`, userID)
return orders, err
}
Common Kitchen Fires (and How to Extinguish Them)
- Eventual Consistency Drama:
- Use version checks in commands
- Implement user-facing “operation in progress” notifications
- Add compensatory actions for critical paths
- Event Storming Nightmares:
// Bad event - too vague type OrderUpdatedEvent struct{} // Good event - specific state change type OrderLocationUpdatedEvent struct { NewLat float64 NewLong float64 UpdatedBy uuid.UUID }
- Kafka Consumer Lag Panic:
- Use consumer groups effectively
- Monitor lag with Prometheus metrics
- Implement dead-letter queues for poison pills
The Secret Sauce: When to Add This Spice
CQRS shines when:
- You need different read/write scalability
- Complex business logic requires audit trails
- Multiple teams work on same domain
- You want to experiment with different data stores But remember: like truffle oil, a little goes a long way. Don’t use it for simple CRUD apps - that’s like bringing a flamethrower to a candlelight dinner.
Parting Wisdom (and Dad Jokes)
Implementing CQRS is like teaching your code to tango - it takes practice, but once synchronized, it’s beautiful. Remember:
- “Event sourcing” isn’t just a pattern - it’s a lifestyle
- Kafka streams are the rivers of truth in your system
- A good correlation ID is worth its weight in debug logs As we say in distributed systems: “If at first you don’t succeed, destroy all evidence you tried.” Wait, no - that’s not right. Let’s stick with “eventually consistent” instead! Now go forth and build systems so resilient they make cockroaches jealous. Your application will thank you - probably through an event stream.