Jordi Salazar
ExperienceArticles
Jordi Salazar
HomeExperienceArticles
HomeExperienceArticles
Building Event-Driven Microservices in Go

Building Event-Driven Microservices in Go

A deep dive into designing event-driven architectures with Go, covering CQRS, event sourcing, and message brokers like RabbitMQ.

June 15, 2025
12 min read
GoMicroservicesEvent-DrivenRabbitMQ

Event-driven architecture is one of the most powerful patterns for building scalable, decoupled backend systems. In this article, I'll walk through how I design and implement event-driven microservices in Go, drawing from production experience.

Why Event-Driven?

Traditional request-response architectures create tight coupling between services. When Service A needs to notify Service B, C, and D about a state change, you end up with a web of synchronous calls that are fragile and hard to scale.

Event-driven architecture flips this: services emit events about what happened, and other services react to those events independently.

Benefits:

  • Loose coupling — producers don't know about consumers
  • Scalability — consumers process events at their own pace
  • Resilience — if a consumer is down, events queue up and get processed later
  • Auditability — events create a natural audit log

The Core Pattern

Here's a simplified event structure I use in Go:

type Event struct {
    ID        string    `json:"id"`
    Type      string    `json:"type"`
    Payload   []byte    `json:"payload"`
    Timestamp time.Time `json:"timestamp"`
    Source    string    `json:"source"`
}

Each microservice publishes domain events to a message broker (RabbitMQ in our case) and subscribes to events it cares about.

CQRS: Separating Reads from Writes

Command Query Responsibility Segregation (CQRS) pairs naturally with event-driven systems. The write side processes commands and emits events, while the read side builds optimized projections from those events.

// Command side - handles writes
type OrderService struct {
    repo      OrderRepository
    publisher EventPublisher
}
 
func (s *OrderService) PlaceOrder(cmd PlaceOrderCommand) error {
    order, err := NewOrder(cmd)
    if err != nil {
        return err
    }
    if err := s.repo.Save(order); err != nil {
        return err
    }
    return s.publisher.Publish(OrderPlacedEvent{
        OrderID: order.ID,
        UserID:  cmd.UserID,
        Total:   order.Total,
    })
}

The read side listens for OrderPlaced events and updates a denormalized view optimized for queries.

Lessons from Production

After running event-driven systems in production, here are the key takeaways:

  1. Idempotency is non-negotiable — messages can be delivered more than once. Design handlers to be safely re-executable.
  2. Schema evolution matters — use versioned event schemas so you can evolve without breaking consumers.
  3. Dead letter queues save you — always configure DLQs for messages that fail processing.
  4. Correlation IDs are essential — trace a single user action across multiple services.

Event-driven architecture isn't about technology — it's about modeling your domain as a series of facts that happened.

What's Next

In the next article, I'll cover event sourcing — storing the full history of events as your source of truth, and how to implement it efficiently in Go with PostgreSQL.

Enjoyed this article? Share it.