Distributed Messaging & Event Systems
Messaging systems are the nervous system of distributed architectures, enabling asynchronous communication, decoupling, and event-driven processing. If databases are the βmemoryβ of your system, messaging is the βnervous systemβ β it carries signals between components without requiring the sender to wait for the receiver to process them, just as your brain can send a nerve impulse to your hand without pausing all other activity until the hand responds.Module Duration: 14-18 hours
Key Topics: Kafka, RabbitMQ, Event Sourcing, CQRS, Exactly-Once Semantics, Stream Processing
Interview Focus: Kafka internals, delivery guarantees, event-driven architecture
Key Topics: Kafka, RabbitMQ, Event Sourcing, CQRS, Exactly-Once Semantics, Stream Processing
Interview Focus: Kafka internals, delivery guarantees, event-driven architecture
Why Messaging Systems?
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SYNCHRONOUS vs ASYNCHRONOUS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β SYNCHRONOUS (RPC): β
β ββββββββββββββββββ β
β Client ββββββΊ Service A ββββββΊ Service B ββββββΊ Service C β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β Problems: β
β β’ Caller blocked until all complete β
β β’ One service down β entire chain fails β
β β’ Tight coupling between services β
β β’ Hard to add/remove services β
β β
β ASYNCHRONOUS (Messaging): β
β βββββββββββββββββββββββββ β
β Client ββββββΊ Message Queue β
β βββββββββ β β
β (returns) ββββββββΊ Service A β
β ββββββββΊ Service B β
β ββββββββΊ Service C β
β β
β Benefits: β
β β’ Caller returns immediately β
β β’ Services process independently β
β β’ Loose coupling, easy to scale β
β β’ Built-in retry, replay capability β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Message Queue Patterns
Point-to-Point (Work Queue)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POINT-TO-POINT (WORK QUEUE) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Each message consumed by exactly one consumer. β
β β
β βββββββββββββββββββ β
β β QUEUE β β
β β β β
β Producer ββββββββββΊβ [M1][M2][M3][M4]β β
β β β β
β ββββββββββ¬βββββββββ β
β β β
β βββββββββββββββββΌββββββββββββββββ β
β βΌ βΌ βΌ β
β Consumer A Consumer B Consumer C β
β M1 M2 M3 β
β β
β USE CASES: β
β ββββββββββ β
β β’ Task distribution (processing jobs) β
β β’ Load balancing work across workers β
β β’ Order processing, email sending β
β β
β ACKNOWLEDGMENT: β
β βββββββββββββββ β
β Consumer receives message β processes β ACKs β
β If consumer dies before ACK β message redelivered β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Publish-Subscribe (Fan-out)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PUBLISH-SUBSCRIBE (FAN-OUT) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Each message delivered to ALL subscribers. β
β β
β βββββββββββββββββββ β
β β TOPIC β β
β β β β
β Publisher βββββββββΊβ Message β β
β β β β
β ββββββββββ¬βββββββββ β
β β β
β βββββββββββββββββΌββββββββββββββββ β
β β β β β
β βΌ βΌ βΌ β
β βββββββββββββ βββββββββββββ βββββββββββββ β
β βSubscriber β βSubscriber β βSubscriber β β
β β (Email) β β (Analyticsβ β (Audit) β β
β βββββββββββββ βββββββββββββ βββββββββββββ β
β β
β All three receive the same message β
β β
β USE CASES: β
β ββββββββββ β
β β’ Event broadcasting β
β β’ Real-time updates β
β β’ Logging, monitoring, analytics β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Consumer Groups (Kafka Style)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CONSUMER GROUPS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Combines work queue (within group) and pub-sub (across groups). β
β β
β βββββββββββββββββββββββββββββββ β
β β TOPIC (4 partitions) β β
β β ββββββ¬βββββ¬βββββ¬βββββ β β
β β β P0 β P1 β P2 β P3 β β β
β β ββββββ΄βββββ΄βββββ΄βββββ β β
β βββββββββββββββ¬ββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββΌβββββββββββββββββββββββββ β
β βΌ βΌ βΌ β
β βββββββββββββββββββββ βββββββββββββββββββββ βββββββββββββββββββββ β
β β Consumer Group A β β Consumer Group B β β Consumer Group C β β
β β βββββ βββββ β β βββββ β β βββββ β β
β β βC1 β βC2 β β β βC1 β β β βC1 β β β
β β βP0 β βP1 β β β βP0 β β β βP0 β β β
β β βP2 β βP3 β β β βP1 β β β βP1 β β β
β β βββββ βββββ β β βP2 β β β βP2 β β β
β β β β βP3 β β β βP3 β β β
β βββββββββββββββββββββ β βββββ β β βββββ β β
β βββββββββββββββββββββ βββββββββββββββββββββ β
β β
β RULES: β
β ββββββ β
β β’ Each partition β one consumer in a group β
β β’ Multiple groups β each gets all messages β
β β’ Consumer count > partition count β some idle β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Apache Kafka Deep Dive
Must-Know: Kafka is the de facto standard for event streaming. Expect detailed questions about partitions, replication, and exactly-once semantics.
Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β KAFKA ARCHITECTURE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββββββ β
β β PRODUCERS β β
β ββββββββββ¬βββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β KAFKA CLUSTER ββ
β β ββ
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ ββ
β β β TOPIC: orders β ββ
β β β β ββ
β β β Partition 0 Partition 1 Partition 2 Partition 3 β ββ
β β β βββββββββββ βββββββββββ βββββββββββ βββββββββββ β ββ
β β β β Broker 1β β Broker 2β β Broker 3β β Broker 1β β ββ
β β β β (Leader)β β (Leader)β β (Leader)β β (Leader)β β ββ
β β β ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ β ββ
β β β β β β β β ββ
β β β ββββββΌβββββ ββββββΌβββββ ββββββΌβββββ ββββββΌβββββ β ββ
β β β β Broker 2β β Broker 3β β Broker 1β β Broker 2β β ββ
β β β β(Replica)β β(Replica)β β(Replica)β β(Replica)β β ββ
β β β ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ β ββ
β β β β β β β β ββ
β β β ββββββΌβββββ ββββββΌβββββ ββββββΌβββββ ββββββΌβββββ β ββ
β β β β Broker 3β β Broker 1β β Broker 2β β Broker 3β β ββ
β β β β(Replica)β β(Replica)β β(Replica)β β(Replica)β β ββ
β β β βββββββββββ βββββββββββ βββββββββββ βββββββββββ β ββ
β β β β ββ
β β β Replication Factor = 3 β ββ
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ ββ
β β ββ
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ ββ
β β β ZOOKEEPER / KRAFT β ββ
β β β β’ Cluster membership β ββ
β β β β’ Leader election β ββ
β β β β’ Topic/partition metadata β ββ
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ ββ
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β
β βΌ β
β βββββββββββββββββββ β
β β CONSUMERS β β
β βββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Partition Internals
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PARTITION STRUCTURE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β PARTITION = Ordered, immutable sequence of records β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β PARTITION 0 β β
β β β β
β β Offset: 0 1 2 3 4 5 6 7 8 9 ... β β
β β ββββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ¬βββββ β β
β β β M0 β M1 β M2 β M3 β M4 β M5 β M6 β M7 β M8 β M9 β β β
β β ββββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ΄βββββ β β
β β β β
β β β² β² β² β β
β β β β β β β
β β Log Start Consumer High-Water β β
β β (retention) Offset Mark β β
β β β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β SEGMENTS: β
β βββββββββ β
β Partition split into segment files for efficient cleanup β
β β
β /kafka-logs/topic-partition-0/ β
β βββ 00000000000000000000.log (segment 1: offsets 0-999) β
β βββ 00000000000000000000.index (offset β position) β
β βββ 00000000000000001000.log (segment 2: offsets 1000-1999) β
β βββ 00000000000000001000.index β
β βββ ... β
β β
β RETENTION: β
β ββββββββββ β
β β’ Time-based: Delete segments older than X days β
β β’ Size-based: Delete when partition exceeds X GB β
β β’ Compaction: Keep only latest value per key β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Producer Delivery Guarantees
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PRODUCER ACKNOWLEDGMENTS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ACKS = 0 (Fire and forget) β
β βββββββββββββββββββββββββ β
β Producer ββββΊ Broker β
β (returns immediately, doesn't wait for ACK) β
β β
β β’ Fastest, lowest latency β
β β’ No delivery guarantee β
β β’ Use for: Metrics, logs where loss is acceptable β
β β
β ACKS = 1 (Leader acknowledgment) β
β ββββββββββββββββββββββββββββββββ β
β Producer ββββΊ Leader ββββΊ ACK β
β (returns after leader writes) β
β β
β β’ Good balance of speed and safety β
β β’ Leader crash before replication = data loss β
β β’ Default setting for most use cases β
β β
β ACKS = -1/ALL (All ISR acknowledgment) β
β ββββββββββββββββββββββββββββββββββββββ β
β Producer ββββΊ Leader ββββΊ Replicas (all ISR) ββββΊ ACK β
β (returns after all in-sync replicas have it) β
β β
β β’ Strongest durability guarantee β
β β’ Higher latency β
β β’ Combined with min.insync.replicas for safety β
β β
β IDEMPOTENT PRODUCER: β
β ββββββββββββββββββββ β
β enable.idempotence = true β
β β’ Producer assigns sequence number to each message β
β β’ Broker rejects duplicates β
β β’ Exactly-once within a partition β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Consumer Offset Management
Think of Kafka offsets like a bookmark in a book. If you lose the bookmark, you have to re-read from wherever you last remember β potentially re-reading pages (reprocessing messages). The critical question is: do you move the bookmark before or after you read the page?from kafka import KafkaConsumer, TopicPartition
# Manual offset management -- the "I control the bookmark" approach.
# This is essential for at-least-once delivery in production systems.
consumer = KafkaConsumer(
'orders',
bootstrap_servers=['localhost:9092'],
group_id='order-processor',
enable_auto_commit=False, # CRITICAL: Never use auto-commit for
# anything where duplicates matter.
# Auto-commit moves the bookmark on a timer,
# not when you've actually finished processing.
auto_offset_reset='earliest' # On first join or offset expiry, start
# from the beginning -- not 'latest',
# which would skip unprocessed messages.
)
for message in consumer:
try:
# Process message first -- this is the at-least-once pattern.
process_order(message.value)
# Commit offset AFTER successful processing.
# Note: offset + 1 because we're committing the NEXT offset to read,
# not the one we just processed. This off-by-one is a common source
# of bugs in Kafka consumer implementations.
consumer.commit({
TopicPartition(message.topic, message.partition):
OffsetAndMetadata(message.offset + 1, None)
})
except Exception as e:
# Don't commit -- message will be redelivered on next poll.
# This is the safety net: if processing fails, we retry.
log.error(f"Failed to process: {e}")
# Optionally: seek back explicitly for immediate retry
# rather than waiting for the next consumer restart.
consumer.seek(
TopicPartition(message.topic, message.partition),
message.offset
)
# The three delivery patterns and their failure modes:
#
# AT-LEAST-ONCE (process then commit) -- shown above
# Crash after process, before commit --> message redelivered
# Your handler MUST be idempotent to handle this safely
#
# AT-MOST-ONCE (commit then process)
# Crash after commit, before process --> message lost forever
# Only acceptable for non-critical data (metrics, logs)
#
# EXACTLY-ONCE (transactional consume-transform-produce)
# Kafka transactions wrap offset commit + output writes atomically
# Highest overhead, but no duplicates and no data loss
Production pitfall: If your consumer group has more instances than partitions, the extra consumers sit idle β they cannot share a partition. Plan your partition count at topic creation time, because increasing it later changes the partitioning of keyed messages and can break ordering guarantees for in-flight data.
Kafka Transactions (Exactly-Once)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β KAFKA TRANSACTIONS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β TRANSACTIONAL PRODUCER: β
β ββββββββββββββββββββββββ β
β Atomically write to multiple partitions β
β β
β ```python β
β producer = KafkaProducer( β
β bootstrap_servers=['localhost:9092'], β
β transactional_id='my-producer' β
β ) β
β β
β producer.init_transactions() β
β β
β producer.begin_transaction() β
β try: β
β producer.send('topic-a', value=b'message1') β
β producer.send('topic-b', value=b'message2') β
β producer.commit_transaction() β
β except: β
β producer.abort_transaction() β
β ``` β
β β
β READ_COMMITTED CONSUMERS: β
β ββββββββββββββββββββββββββ β
β Only see committed transactional messages β
β β
β isolation.level = "read_committed" β
β β
β CONSUME-TRANSFORM-PRODUCE: β
β βββββββββββββββββββββββββββ β
β Exactly-once stream processing β
β β
β Input ββββΊ Process ββββΊ Output β
β β β
β ββββΊ Offset Commit β
β β
β All three operations in ONE transaction! β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Event Sourcing
Pattern Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EVENT SOURCING β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β TRADITIONAL (State-based): β
β βββββββββββββββββββββββββββ β
β Store CURRENT state only β
β β
β Account { id: 123, balance: 500 } β
β β
β Problem: No history, hard to debug, can't replay β
β β
β EVENT SOURCING: β
β βββββββββββββββ β
β Store sequence of EVENTS that led to current state β
β β
β Event 1: AccountCreated { id: 123, owner: "Alice" } β
β Event 2: MoneyDeposited { id: 123, amount: 1000 } β
β Event 3: MoneyWithdrawn { id: 123, amount: 300 } β
β Event 4: MoneyWithdrawn { id: 123, amount: 200 } β
β β
β Current state = replay(events) β balance: 500 β
β β
β BENEFITS: β
β βββββββββ β
β β Complete audit trail β
β β Can rebuild state at any point β
β β Debug by replaying events β
β β Temporal queries (what was state on Jan 1?) β
β β Easy to add new projections β
β β
β CHALLENGES: β
β βββββββββββ β
β β Schema evolution for events β
β β Replay can be slow β
β β Eventual consistency β
β β More complex than CRUD β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Event Store Implementation
from dataclasses import dataclass
from datetime import datetime
from typing import List
import json
@dataclass
class Event:
aggregate_id: str
event_type: str
data: dict
timestamp: datetime
version: int
class EventStore:
"""
Simple event store backed by a database.
Production: Use EventStoreDB, Kafka, or DynamoDB Streams.
The event store is analogous to an accounting ledger: you never
erase or modify past entries -- you only append new ones. To know
the current balance, you replay the ledger from the beginning
(or from the last checkpoint/snapshot).
"""
def __init__(self, db):
self.db = db
def append(self, aggregate_id: str, events: List[Event], expected_version: int):
"""
Append events with optimistic concurrency control.
The expected_version check is our guard against lost updates:
if two requests try to append simultaneously, only the first
succeeds -- the second sees a version mismatch and must retry
with fresh state. This is the event-sourcing equivalent of a
compare-and-swap (CAS) operation.
"""
# Check current version
current_version = self._get_version(aggregate_id)
if current_version != expected_version:
raise ConcurrencyError(
f"Expected version {expected_version}, got {current_version}"
)
# Append events
for i, event in enumerate(events):
event.version = expected_version + i + 1
self.db.execute(
"""
INSERT INTO events (aggregate_id, event_type, data, timestamp, version)
VALUES (?, ?, ?, ?, ?)
""",
(aggregate_id, event.event_type, json.dumps(event.data),
event.timestamp, event.version)
)
# Publish to subscribers
self._publish(events)
def get_events(self, aggregate_id: str, from_version: int = 0) -> List[Event]:
"""Get all events for an aggregate from a given version."""
rows = self.db.execute(
"""
SELECT * FROM events
WHERE aggregate_id = ? AND version > ?
ORDER BY version
""",
(aggregate_id, from_version)
)
return [self._row_to_event(row) for row in rows]
class BankAccount:
"""
Event-sourced aggregate.
"""
def __init__(self, account_id: str):
self.id = account_id
self.balance = 0
self.is_closed = False
self.version = 0
self._pending_events = []
# Command handlers
def deposit(self, amount: float):
if self.is_closed:
raise ValueError("Account is closed")
if amount <= 0:
raise ValueError("Amount must be positive")
self._apply(MoneyDeposited(self.id, amount))
def withdraw(self, amount: float):
if self.is_closed:
raise ValueError("Account is closed")
if amount > self.balance:
raise ValueError("Insufficient funds")
self._apply(MoneyWithdrawn(self.id, amount))
# Event handlers
def _apply(self, event):
self._pending_events.append(event)
self._handle(event)
def _handle(self, event):
if isinstance(event, AccountCreated):
self.balance = 0
elif isinstance(event, MoneyDeposited):
self.balance += event.amount
elif isinstance(event, MoneyWithdrawn):
self.balance -= event.amount
elif isinstance(event, AccountClosed):
self.is_closed = True
self.version += 1
# Rehydration
@classmethod
def from_events(cls, account_id: str, events: List[Event]) -> 'BankAccount':
account = cls(account_id)
for event in events:
account._handle(event)
return account
# Usage
def transfer_money(from_id: str, to_id: str, amount: float, event_store: EventStore):
"""
Saga pattern for cross-aggregate operation.
"""
from_events = event_store.get_events(from_id)
from_account = BankAccount.from_events(from_id, from_events)
to_events = event_store.get_events(to_id)
to_account = BankAccount.from_events(to_id, to_events)
try:
from_account.withdraw(amount)
to_account.deposit(amount)
# Save both in transaction (or use saga with compensation)
event_store.append(from_id, from_account._pending_events, from_account.version)
event_store.append(to_id, to_account._pending_events, to_account.version)
except Exception as e:
# Compensation logic if needed
raise
CQRS (Command Query Responsibility Segregation)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CQRS PATTERN β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β TRADITIONAL: β
β ββββββββββββ β
β Same model for reads and writes β
β β
β API ββββΊ Service ββββΊ Database β
β β β² β
β βββββββββββββββββββββββββ β
β β
β CQRS: β
β βββββ β
β Separate models for commands (writes) and queries (reads) β
β β
β WRITE PATH READ PATH β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β Commands β β Queries β β
β β (Create, Update) β β (List, Search) β β
β ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββββ β
β β β β
β βΌ βΌ β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β Command Handler β β Query Handler β β
β β (Domain Logic) β β (Simple reads) β β
β ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββββ β
β β β β
β βΌ βΌ β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β Write Model ββββββββββββββΊβ Read Model β β
β β (Normalized) β Events/ β (Denormalized) β β
β β β Projectionsβ β β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β
β BENEFITS: β
β βββββββββ β
β β’ Optimize read/write models independently β
β β’ Scale reads and writes separately β
β β’ Different storage for each (SQL writes, Elasticsearch reads) β
β β’ Complex queries without affecting write performance β
β β
β PROJECTIONS (Event β Read Model): β
β ββββββββββββββββββββββββββββββββββ β
β Events from write model β Update denormalized read model β
β Example: OrderPlaced β Update UserOrderHistory, ProductSalesCount β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Projection Example
class OrderProjection:
"""
Builds read model from order events.
"""
def __init__(self, read_db, event_store):
self.read_db = read_db
self.event_store = event_store
def handle(self, event):
"""Route event to appropriate handler."""
handler = getattr(self, f'on_{event.event_type}', None)
if handler:
handler(event)
def on_OrderPlaced(self, event):
# Insert into orders read model
self.read_db.execute(
"""
INSERT INTO order_summaries (id, user_id, total, status, created_at)
VALUES (?, ?, ?, 'placed', ?)
""",
(event.data['order_id'], event.data['user_id'],
event.data['total'], event.timestamp)
)
# Update user statistics
self.read_db.execute(
"""
UPDATE user_stats
SET order_count = order_count + 1,
total_spent = total_spent + ?
WHERE user_id = ?
""",
(event.data['total'], event.data['user_id'])
)
def on_OrderShipped(self, event):
self.read_db.execute(
"""
UPDATE order_summaries
SET status = 'shipped', shipped_at = ?
WHERE id = ?
""",
(event.timestamp, event.data['order_id'])
)
def rebuild(self):
"""Rebuild entire read model from events."""
self.read_db.execute("TRUNCATE order_summaries")
self.read_db.execute("TRUNCATE user_stats")
for event in self.event_store.get_all_events():
self.handle(event)
Message Queue Comparison
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MESSAGE QUEUE COMPARISON β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ β
β β Feature β Kafka β RabbitMQ β AWS SQS β Pulsar β β
β ββββββββββββββΌββββββββββββββΌββββββββββββββΌββββββββββββββΌββββββββββββββ€ β
β β Model β Log β Queue β Queue β Log β β
β β Ordering β Per-partitionβ Per-queue β Best-effort β Per-partitionβ β
β β Retention β Configurableβ Until ACK β 14 days max β Configurableβ β
β β Replay β Yes β No β No β Yes β β
β β Throughput β Very high β Moderate β High β Very high β β
β β Latency β Low β Very low β Moderate β Low β β
β β Exactly-onceβ Yes (EOS) β No β No β Yes β β
β β Multi-tenancyβ Topics β vHosts β Queues β Tenants β β
β ββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ β
β β
β WHEN TO USE: β
β βββββββββββββ β
β β
β KAFKA: β
β β’ Event streaming, log aggregation β
β β’ High throughput (millions/sec) β
β β’ Need replay capability β
β β’ Event sourcing, CQRS β
β β
β RABBITMQ: β
β β’ Complex routing (exchanges, bindings) β
β β’ Low latency messaging β
β β’ RPC patterns β
β β’ Traditional message queue patterns β
β β
β SQS: β
β β’ AWS-native workloads β
β β’ Fully managed, no operations β
β β’ Simple queue patterns β
β β’ Lambda triggers β
β β
β PULSAR: β
β β’ Multi-tenancy requirements β
β β’ Geo-replication β
β β’ Unified queuing and streaming β
β β’ Functions (serverless) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Dead Letter Queues
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DEAD LETTER QUEUES (DLQ) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β PROBLEM: What happens to messages that can't be processed? β
β β
β Without DLQ: β
β β’ Poison message blocks queue β
β β’ Infinite retry loop β
β β’ Messages lost if discarded β
β β
β With DLQ: β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β β Main Queue ββββΊ Consumer βββ¬βββΊ Success β β
β β β β β
β β Retry β Failure (after N retries) β β
β β β β β β
β β ββββ΄βββββββΌβββββββββββββ β β
β β β Dead Letter Queue β β β
β β β β β β
β β β Failed messages for β β β
β β β inspection/replay β β β
β β ββββββββββββββββββββββββ β β
β β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β IMPLEMENTATION: β
β βββββββββββββββ β
β β
β ```python β
β MAX_RETRIES = 3 β
β β
β def process_message(message): β
β retry_count = message.headers.get('retry_count', 0) β
β β
β try: β
β handle(message) β
β ack(message) β
β except TransientError: β
β if retry_count < MAX_RETRIES: β
β requeue_with_delay(message, retry_count + 1) β
β else: β
β send_to_dlq(message) β
β except PermanentError: β
β send_to_dlq(message) # Don't retry β
β ``` β
β β
β DLQ HANDLING: β
β βββββββββββββ β
β β’ Manual inspection and fix β
β β’ Automated replay after fix deployed β
β β’ Alerts for DLQ depth β
β β’ Retention policy for old messages β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Advanced Design Scenarios
Scenario 1: End-to-End Exactly-Once Pipeline
You need to build a consume β transform β produce pipeline that processes financial events exactly once, even across restarts and network failures. Requirements:- Input events from Kafka
transactions.raw - Processed events written to Kafka
transactions.clearedand a database - No duplicate effects (no double-charging, no missing debits)
- Use idempotent + transactional producer for the output topic
- Use read-committed consumers and manual offset commit
- Wrap DB writes and Kafka offset/produce in a single transaction (per-partition)
- Maintain an idempotency key (e.g., event ID) table in the DB
- On each event:
- If key seen before β skip (already processed)
- Else β apply business logic, insert key, produce output message, commit transaction and offset
Scenario 2: Handling Backpressure and Slow Consumers
You run a high-throughput Kafka topic with occasionally slow consumers. Symptoms:- Consumer lag grows during spikes
- Retention window is threatened
- Downstream services start timing out
- Horizontal scale: Increase consumer instances within the group (up to partitions count)
- Backpressure-aware processing: Use bounded thread pools and queues in consumers
- Prioritization: Route high-priority messages to a separate topic/consumer group
- Graceful degradation: Drop non-critical messages or aggregate at coarser granularity under load
Scenario 3: Region Outage and Replay
Your system runs active-active in two regions, both consuming from and producing to regional topics. Requirements:- If one region fails for 2 hours, you must replay missed events when it comes back
- No double-processing and no lost events
- Use per-region topics (e.g.,
events.us,events.eu) plus a global replication pipeline - Keep checkpoints per consumer group and region (last processed offset / timestamp)
- On recovery:
- Start consumers from last committed checkpoint
- Apply the same idempotent processing pattern as Scenario 1
Interview Practice
Q1: Design a notification system
Q1: Design a notification system
Question: Design a system that sends notifications (push, email, SMS) for an e-commerce platform.Design:
Architecture:
Event Sources ββββΊ Kafka ββββΊ Notification Service ββββΊ Channels
(orders, user) (router, templates) (Push/Email/SMS)
Components:
1. Event Ingestion
- Kafka topic: notifications.events
- Events: OrderPlaced, OrderShipped, PriceDropped, etc.
2. Notification Router
- Reads events from Kafka
- Applies user preferences
- Routes to appropriate channel
3. Channel Workers
- Separate consumer groups per channel
- Push: Firebase/APNS
- Email: SES/SendGrid
- SMS: Twilio
4. Template Service
- Render message from event + template
- i18n support
5. Rate Limiting
- Don't spam users
- Per-user, per-channel limits
6. Dead Letter Queue
- Failed deliveries for retry
- Alerting on high DLQ depth
Q2: Kafka vs RabbitMQ
Q2: Kafka vs RabbitMQ
Question: When would you choose Kafka over RabbitMQ?Choose Kafka when:
- High throughput needed (100K+ msg/sec)
- Need to replay messages
- Event sourcing / audit log
- Stream processing (Kafka Streams, ksqlDB)
- Multiple consumers reading same messages
- Long-term message retention
- Complex routing needed (fanout, topic, headers)
- Low latency is critical (< 1ms)
- Need RPC patterns (request/reply)
- Traditional work queues
- Smaller scale, simpler operations
- Need message priority
Q3: Exactly-once processing
Q3: Exactly-once processing
Question: How do you achieve exactly-once message processing?Answer:True exactly-once is impossible (Two Generals Problem). We achieve βeffectively exactly-onceβ:Option 1: Idempotent ConsumerOption 2: Kafka Transactions (EOS)Option 3: Outbox PatternOption 4: Change Data Capture (CDC)
def process(message):
if already_processed(message.id):
return # Deduplicate
with transaction:
do_work(message)
mark_processed(message.id)
- Producer: enable.idempotence = true
- Consumer: isolation.level = read_committed
- Consume-transform-produce in single transaction
1. Write to DB + outbox table (same transaction)
2. Separate process reads outbox, publishes to Kafka
3. Delete from outbox after publish confirmed
- Debezium reads DB transaction log
- Publishes changes to Kafka
- Inherently ordered and exactly-once
Key Takeaways
Kafka is a Log, Not a Queue
Messages persist, can be replayed, and multiple consumers read independently.
Partitions Enable Parallelism
More partitions = more consumers = higher throughput. Choose partition key wisely.
Event Sourcing Provides Audit Trail
Store events, derive state. Enables debugging, replay, and temporal queries.
Exactly-Once = Idempotency
At-least-once delivery + idempotent processing = effectively exactly-once.
Next Steps
Data Systems
Learn about Kafkaβs storage, compaction, and stream processing
Transactions
Understand distributed transactions and the Saga pattern