The Inbox & Outbox Pattern: Reliable Events with Kafka π¬
How to stop losing (and duplicating) events when your service writes to a database and Kafka at the same time.
What makes Kafka a distributed log rather than a queue, and when to reach for RabbitMQ or Redis instead.
Hey everyone! π If you've been hanging around backend engineering and system architecture circles, you've inevitably heard the word "Kafka" thrown around. It often sounds like the ultimate silver bullet for any distributed system problem. But what exactly is it?
Today, let's grab a cup of coffee β and dive deep into Apache Kafka. We'll strip away the jargon, explore its core architecture, figure out how it works under the hood, compare it with popular alternatives, and discuss its key trade-offs.
At its core, Apache Kafka is a Distributed Streaming Platform.
Instead of thinking of Kafka as a traditional message queue where a message is processed and immediately deleted, think of Kafka as a highly scalable, distributed, append-only log.
Imagine a massive, indestructible ledger book:
To understand Kafka, you need to know its core building blocks:
Here is a logical view of how the data flows:
flowchart LR
subgraph Producers
P1[Payment Service]
P2[Clickstream Tracker]
end
subgraph Kafka_Cluster[Kafka Cluster]
B1[Broker 1]
B2[Broker 2]
B3[Broker 3]
ZK[(KRaft / Zookeeper)]
end
subgraph Consumers
C1[Fraud Detection Service]
C2[Analytics Engine]
end
P1 -->|Publishes Events| Kafka_Cluster
P2 -->|Publishes Events| Kafka_Cluster
Kafka_Cluster -->|Pulls Batches| C1
Kafka_Cluster -->|Pulls Batches| C2
Let's zoom in on a Topic and its Partitions:
Here's how one topic's partitions map to a consumer group β each partition is read by exactly one consumer in the group, which is what caps your parallelism at the partition count:
flowchart LR
subgraph Topic["Topic: orders"]
P0["Partition 0<br/>offsets 0 β 3 β"]
P1["Partition 1<br/>offsets 0 β 2 β"]
P2["Partition 2<br/>offsets 0 β 4 β"]
end
subgraph CG["Consumer Group: order-workers"]
C1[Consumer A]
C2[Consumer B]
end
P0 --> C1
P1 --> C1
P2 --> C2Add a third consumer and Kafka rebalances a partition onto it; add a fourth and it sits idle, because there's no unassigned partition left for it to own.
Developers often ask: "Why shouldn't I just use RabbitMQ or Redis?"
Let's look at a conceptual difference using RabbitMQ's routing architecture as an example:
While RabbitMQ uses intelligent brokers with complex routing (Exchanges) to push messages, Kafka uses a "dumb broker / smart consumer" model where consumers pull from a distributed log.
| Feature | Apache Kafka | RabbitMQ | Redis Pub/Sub |
|---|---|---|---|
| Primary Model | Distributed Commit Log | Traditional Message Queue (AMQP) | In-Memory Pub/Sub / Cache |
| Data Flow | Pull-based (Consumer polls data) | Push-based (Broker pushes to consumer) | Push-based (Fire & Forget) |
| Persistence | Permanent on disk (Retention-based) | Transient (Deleted upon acknowledgement) | Ephemeral (Lost if consumer is offline) |
| Throughput | Ultra-High (Millions of msgs/sec) | High (Tens of thousands/sec) | Extremely High (Limited by RAM) |
| Message Replay | β Yes (Rewind consumer offset) | β No (Once consumed, it's gone) | β No |
| Best For | Event streaming, log aggregation, real-time analytics | Complex job queuing, transaction routing, RPC | Lightweight real-time notifications, chat signaling |
As solution architects, we must weigh the trade-offs:
| Pros β | Cons β |
|---|---|
| Massive Scalability: Horizontal scaling across hundreds of brokers. | Operational Complexity: Managing and monitoring cluster partitions, rebalancing, and ISRs requires effort. |
| Durability & Replayability: Re-read historical events to backfill databases or recover from bugs. | High Latency for Small Workloads: Polling batches introduces minor overhead compared to direct socket pushes. |
| High Throughput: Optimized sequential disk I/O and zero-copy transfers. | Overkill for Simple Apps: Unnecessary if a lightweight Redis stream or PostgreSQL queue suffices. |
Apache Kafka isn't just a simple queueβit is an event streaming backbone designed for massive, durable, and asynchronous data flows. Understanding its architecture and knowing when to pick Kafka over alternatives like RabbitMQ or Redis will help you design far more resilient, scalable backend systems.
How to stop losing (and duplicating) events when your service writes to a database and Kafka at the same time.
When hand-wiring main.go stops scaling, and what Fx's graph resolution and lifecycle ordering buy you in return.
Practical patterns for retries, idempotency, and observability in Lambda-backed product APIs.