Event-driven systems scale by distributing the processing of events across multiple instances. This is achieved through partitioning, sharding, and fan-out mechanisms.

Imagine you have a Kafka cluster processing millions of orders per minute. Each order is an "event." If a single consumer instance tried to handle all these orders, it would quickly become a bottleneck. Partitioning, sharding, and fan-out are the techniques that prevent this bottleneck.

Let’s look at a simplified Kafka setup. We have a topic called orders.

{
  "topic": "orders",
  "partitions": 3,
  "replicationFactor": 3
}

This orders topic is divided into 3 partitions: orders-0, orders-1, and orders-2. Kafka guarantees that events within a single partition are processed in order. However, events across different partitions are not necessarily ordered relative to each other. This is the core of partitioning: dividing a large stream of data into smaller, manageable chunks.

Now, how do we decide which order goes into which partition? This is where the "key" comes in. If an order event has an order_id like ORD12345, that order_id can be used as the partition key. Kafka will hash this key and use the hash value to determine which partition the event belongs to. This ensures that all events for the same order_id always go to the same partition.

{
  "order_id": "ORD12345",
  "product": "Widget",
  "quantity": 2,
  "timestamp": "2023-10-27T10:00:00Z"
}

If order_id is ORD12345, and Kafka hashes this to, say, 1234, and 1234 % 3 (number of partitions) is 1, then this order will always go to orders-1.

This partitioning is crucial for scaling consumers. Multiple consumer instances can subscribe to the orders topic. Kafka’s consumer group mechanism ensures that each partition is assigned to at most one consumer within a group. So, if we have 3 consumer instances in a group, consumer-group-A, then consumer-1 might get orders-0, consumer-2 gets orders-1, and consumer-3 gets orders-2. Each consumer processes events from its assigned partitions independently. This is how we achieve parallel processing.

Sharding is a more general database concept that is analogous to partitioning in event streaming. When you shard a database, you’re splitting your data across multiple database servers. In event-driven systems, partitioning is the mechanism that achieves a similar effect for event streams. The term "sharding" is often used interchangeably with "partitioning" in this context, especially when discussing the underlying data distribution strategy.

Fan-out is about distributing a single event to multiple downstream systems or consumers. While partitioning distributes different events to different consumers for parallel processing, fan-out distributes the same event to multiple recipients.

Consider a scenario where an order_placed event needs to trigger several actions:

  1. Update inventory.
  2. Notify the shipping department.
  3. Send an email confirmation to the customer.
  4. Log the event for auditing.

If we had a single consumer processing order_placed events, it would have to perform all these tasks sequentially, or worse, it would need to know about all these downstream systems and integrate with them directly. This creates a tightly coupled and brittle system.

A more robust approach uses fan-out. An event is published to a topic, say order_events. Then, multiple independent consumers (or microservices) subscribe to this topic:

  • inventory-service
  • shipping-service
  • email-service
  • audit-log-service

Each of these services receives a copy of the order_placed event and performs its specific task. The order_events topic acts as a central hub, decoupling the event producer from the multiple event consumers. This is a classic fan-out pattern.

Kafka achieves fan-out through its publish-subscribe model. When an event is published to a topic, all active consumers in a consumer group receive a copy of that event (though each partition is only consumed by one instance within the group). If you have multiple consumer groups subscribed to the same topic, each group will receive a full copy of all messages.

For example, if we have consumer-group-A for processing and consumer-group-B for real-time analytics, both groups will independently receive every event published to the topic. This is how a single event can be fanned out to multiple distinct processing pipelines.

The key insight is that partitioning is about distributing the load of processing a large volume of events by dividing the event stream. Fan-out is about distributing the responsibility of acting on a single event to multiple independent services. Sharding is often used synonymously with partitioning in the context of data distribution.

One common point of confusion is how to achieve fan-out when you also need to ensure order for a specific entity, like an order_id. If you partition a topic by order_id for processing, and then you want to fan-out that same event to multiple services, you might think you need multiple topics. However, you can achieve this with a single topic and multiple consumer groups. The partitioning ensures that all events for ORD12345 go to the same partition and are thus processed in order by a single consumer instance within a group. Multiple consumer groups can then independently consume from that same partition, effectively fanning out the event to different processing paths.

The next challenge you’ll face is managing consumer offsets and ensuring exactly-once processing semantics in the face of failures.

Want structured learning?

Take the full Event-driven course →