Phase 3: Architecture Patterns

Event sourcing, CQRS & publish-subscribe patterns

Intermediate ~3 min read
Think of it this way A friendly analogy. Read this if the technical version feels dense. Show Hide

Imagine you're playing your favorite board game with friends. What if instead of just looking at the board and seeing who owns what and how much money everyone has right now, you had a super special notebook? In this notebook, you write down every single thing that happens during the game, in the order it happens: "Player 1 rolled a 6," "Player 1 landed on Park Place," "Player 1 bought Park Place," "Player 2 paid rent to Player 1." This isn't just a summary; it's a complete, never-changing history of every move and decision. If someone accidentally bumped the board and scattered all the pieces, no worries! You could grab a new board, open your notebook, and just replay every single entry, putting everything back exactly where it was. This way, you always know exactly how the game got to its current state, and you can even look back to see what happened five turns ago!

Now, think about how you play the game. Sometimes you're making a move – you roll the dice, decide to buy a property, or pay rent. These are like your "commands" to the game. When you do this, you're mostly interested in performing the action and making sure it gets added to our special notebook history. Other times, you're just checking something – like "How much money does Player 3 have?" or "Who owns Boardwalk?" These are like "queries." It might be easier to just quickly glance at a scoreboard or a visible part of the board for these questions, rather than re-reading the whole notebook every time. So, we have one way of interacting that's all about making new moves and recording them, and another way that's all about quickly finding information, separating those two tasks.

What if your game had some smart helper tools? Maybe a little robot that keeps track of everyone's money or a special screen that shows all the properties that are still available to buy. These helpers don't need to constantly ask the players, "Did anything happen? Did anything change?" Instead, whenever something important happens in the game (like "Player 1 bought a property!"), the game shouts it out for everyone to hear. The money-tracking robot "listens" for shouts about money changing hands, and the property screen "listens" for shouts about properties being bought or sold. They just quietly update themselves when they hear something relevant, without bothering anyone or slowing down the game.

So, when you build computer programs and online systems, using these ideas helps make them super reliable and easy to understand. Instead of just saving what something looks like right now, you can keep a perfect history of everything that ever happened to it. This means you can always fix mistakes, understand exactly why something is the way it is, and build smart tools that listen and react automatically, making everything work smoothly and correctly, just like your perfectly recorded board game!

As a Cloud Architect, understanding how to build resilient, scalable, and auditable systems is paramount. Event Sourcing, CQRS, and Publish-Subscribe patterns are cornerstone techniques within Event-Driven Architecture (EDA) that address these needs. Event Sourcing radically changes how we store data: instead of saving just the current state of an entity (like a User record), you store every single event that led to that state. Think of it like a ledger: instead of just seeing the current balance, you have every deposit and withdrawal transaction. This immutable sequence of events provides a complete audit trail, allows you to reconstruct any past state, and is a powerful mechanism for debugging, analytics, and even temporal queries.

Complementing Event Sourcing is Command Query Responsibility Segregation (CQRS). This pattern acknowledges that the requirements for updating data (Commands) are often very different from the requirements for reading data (Queries). With CQRS, you separate your write model (which processes commands and generates events, often leveraging Event Sourcing) from your read model. The read model is typically a highly optimized, potentially denormalized view designed purely for fast queries. When the write model processes a command and persists new events via Event Sourcing, these events are then used to update one or more read models. This separation allows independent scaling and optimization of both sides, crucial in high-load cloud environments.

Finally, the Publish-Subscribe (Pub/Sub) pattern is the glue that binds these components in an EDA. It’s a messaging pattern where components don't communicate directly. Instead, an event publisher (e.g., your write model after committing new events) sends messages (events) to a central channel or topic without knowing who the recipients are. Event subscribers (e.g., services responsible for updating read models, or other microservices reacting to business events) register their interest in specific topics and receive relevant messages. This pattern inherently decouples services, making your architecture more flexible, scalable, and resilient to failures, as services can operate and fail independently while still consuming events reliably via message brokers like AWS SNS/SQS, Azure Service Bus, or Kafka.

Key Takeaways

  • Event Sourcing stores a sequence of events (changes), not just the current state, providing a full audit trail and state reconstruction.
  • CQRS separates data writes (commands) from reads (queries) to allow independent optimization and scaling of different data models.
  • Publish-Subscribe decouples event producers from consumers, enhancing system flexibility, scalability, and resilience.
  • Together, these patterns enable powerful, auditable, and highly scalable distributed systems suited for cloud environments.

Code Example

python
class EventBus:
    def __init__(self):
        self.subscribers = {}

    def subscribe(self, topic, handler):
        if topic not in self.subscribers:
            self.subscribers[topic] = []
        self.subscribers[topic].append(handler)

    def publish(self, topic, event_data):
        print(f"Publishing event to topic '{topic}': {event_data}")
        if topic in self.subscribers:
            for handler in self.subscribers[topic]:
                handler(event_data)

# Example Usage:
def inventory_update_handler(event):
    print(f"  Inventory Service received: Item {event['itemId']} quantity changed to {event['newQuantity']}")

def email_notification_handler(event):
    print(f"  Email Service sending notification about: {event['itemId']} quantity change.")

event_bus = EventBus()
event_bus.subscribe("product_updated", inventory_update_handler)
event_bus.subscribe("product_updated", email_notification_handler)

event_bus.publish("product_updated", {"itemId": "P123", "oldQuantity": 10, "newQuantity": 8})

How this code works

This code demonstrates a fundamental "publish-subscribe" pattern, a core concept in Event-Driven Architecture. Its job is to create a central EventBus that facilitates communication between different parts of a system without direct dependencies. This allows components to announce events (publish) and others to react to them (subscribe) independently.

The EventBus class initializes an empty self.subscribers dictionary to map event topics to lists of functions. The subscribe method adds a handler function to the list associated with a specific topic. For example, event_bus.subscribe("product_updated", inventory_update_handler) registers the inventory_update_handler to listen for "product_updated" events. A subtle detail here: the if topic not in self.subscribers: check is crucial. It ensures that if a topic is new, an empty list is created for it before attempting to append a handler, preventing a KeyError. The publish method takes a topic and event_data, then iterates through all registered handler functions for that topic, calling each one with the event_data. This makes inventory_update_handler and email_notification_handler react to the single event_bus.publish call, demonstrating effective decoupling.