Skip to main content
Back to Research

Event-Driven Architecture with Apache Kafka

A deep-dive into designing event-driven systems with Apache Kafka — from topic modeling and consumer groups to exactly-once semantics and stream processing…

May 8, 2026
6 min read

Event-driven architecture (EDA) has become the backbone of modern distributed systems. When services communicate through events rather than direct API calls, they gain loose coupling, independent scalability, and natural audit trails. Apache Kafka has emerged as the de-facto event streaming platform for these workloads, handling trillions of events per day across industries.

Why Events Instead of APIs

Synchronous REST or gRPC calls create temporal coupling — the caller blocks until the receiver responds. If the receiver is slow or unavailable, the caller suffers. Events invert this: the producer publishes to a durable log and moves on immediately. Consumers process at their own pace.

This model unlocks three architectural properties that are hard to achieve with synchronous communication:

Decoupled scalability. A payment processor and a fraud detection service can scale completely independently because neither calls the other directly. The event log is the interface.

Replayability. Kafka retains events for a configurable retention period. A new consumer — a reporting service, a new ML model — can replay the entire history and derive its own materialized view without burdening other services.

Event sourcing compatibility. When every state change is expressed as an immutable event, your log becomes the system of record. CRUD databases become mere projections.

Core Kafka Concepts

Topics and Partitions

A topic is a named, durable, append-only log. Topics are divided into partitions — the unit of parallelism. Events with the same key are always written to the same partition, preserving order for that key.

Topic: order-events
  Partition 0: [order-placed, order-paid, order-shipped]   # key = order-123
  Partition 1: [order-placed, order-cancelled]              # key = order-456
  Partition 2: [order-placed, order-paid]                   # key = order-789

Choose your partition key carefully. For order events, order_id is the natural key — you want all events for a single order to land in the same partition and be processed in sequence.

Consumer Groups

A consumer group is a set of consumers that collectively read a topic. Each partition is assigned to exactly one consumer in the group at a time. Add more consumers (up to the number of partitions) to increase throughput linearly.

from confluent_kafka import Consumer

consumer = Consumer({
    'bootstrap.servers': 'kafka:9092',
    'group.id': 'fraud-detection-v2',
    'auto.offset.reset': 'earliest',
    'enable.auto.commit': False,   # manual commit for at-least-once
})

consumer.subscribe(['order-events'])

while True:
    msg = consumer.poll(timeout=1.0)
    if msg is None:
        continue
    if msg.error():
        handle_error(msg.error())
        continue

    process_event(msg.value())
    consumer.commit(asynchronous=False)  # commit after successful processing

Setting enable.auto.commit = False and committing only after successful processing gives you at-least-once delivery — events may be reprocessed after a crash, but they will never be lost.

Exactly-Once Semantics

Kafka 0.11+ supports idempotent producers and transactional APIs. Combine them for exactly-once semantics across produce and consume:

producer = Producer({
    'bootstrap.servers': 'kafka:9092',
    'enable.idempotence': True,
    'transactional.id': 'fraud-detector-txn-1',
    'acks': 'all',
})

producer.init_transactions()

try:
    producer.begin_transaction()
    result = detect_fraud(event)
    producer.produce('fraud-decisions', value=result)
    producer.send_offsets_to_transaction(offsets, consumer_group)
    producer.commit_transaction()
except Exception as e:
    producer.abort_transaction()
    raise

This ensures the output event is written and the input offset is committed atomically — either both happen or neither does.

Designing Topics for Scale

Event Schema Strategy

Use Apache Avro with a Schema Registry. Avro enforces schema evolution rules: adding optional fields is safe (backward compatible), removing required fields breaks consumers. The Schema Registry stores schema versions and validates compatibility before a producer publishes a new version.

{
  "type": "record",
  "name": "OrderPlaced",
  "namespace": "com.example.orders",
  "fields": [
    {"name": "order_id", "type": "string"},
    {"name": "customer_id", "type": "string"},
    {"name": "total_amount_cents", "type": "long"},
    {"name": "placed_at", "type": {"type": "long", "logicalType": "timestamp-millis"}},
    {"name": "shipping_address", "type": "string", "default": ""}
  ]
}

Topic Naming Conventions

A consistent naming convention becomes load-bearing at scale. One pattern that works well:

{domain}.{entity}.{event-type}.{version}

examples:
  orders.order.placed.v1
  payments.payment.processed.v2
  inventory.product.stock-updated.v1

This makes topic purpose immediately clear and allows wildcard subscriptions (orders.*) for cross-cutting consumers like audit loggers.

Stream Processing with Kafka Streams

Kafka Streams is a client library (not a separate cluster) for stateful stream processing. It reads from input topics, maintains local state stores backed by changelog topics, and writes results to output topics.

StreamsBuilder builder = new StreamsBuilder();

KStream<String, OrderEvent> orders = builder.stream("orders.order.placed.v1");

// Aggregate order counts per customer in a 1-hour window
KTable<Windowed<String>, Long> orderCounts = orders
    .groupBy((key, event) -> event.getCustomerId())
    .windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofHours(1)))
    .count(Materialized.as("order-count-store"));

// Flag customers exceeding 10 orders per hour (velocity check)
orderCounts
    .toStream()
    .filter((windowedKey, count) -> count > 10)
    .mapValues((windowedKey, count) -> new VelocityAlert(windowedKey.key(), count))
    .to("fraud.velocity-alerts.v1");

The state store (order-count-store) is backed by a Kafka topic and replicated across standby replicas — if the processing node fails, another node restores the store from the changelog and resumes processing with minimal lag.

Operational Considerations

Monitoring What Matters

Three metrics deserve dashboards and alerts above all others:

  • Consumer lag — the difference between the latest offset and the committed offset per partition. Rising lag means consumers are falling behind. Alert when lag exceeds your acceptable processing delay budget.
  • Producer error rate — failed produce calls indicate network issues, topic authorization problems, or broker overload.
  • Partition leader skew — if most partitions' leaders are on a single broker, that broker becomes a bottleneck. Use kafka-preferred-replica-election.sh to rebalance.

Retention and Storage

Kafka's default 7-day retention is often too short for event sourcing use cases. Consider topic-level overrides:

kafka-configs.sh --bootstrap-server kafka:9092 \
  --entity-type topics \
  --entity-name orders.order.placed.v1 \
  --alter \
  --add-config retention.ms=2592000000   # 30 days

For truly infinite retention (event sourcing), use log compaction: Kafka keeps the latest event per key indefinitely, discarding older versions. This gives you a compact, current-state snapshot at no additional storage cost.

When Kafka Is the Right Choice

Kafka excels when: you need durable, replayable event streams; throughput exceeds what a message queue can handle; multiple independent consumers need the same events; or you're building event-sourced systems with complex projections.

Kafka is overkill when: you have simple point-to-point messaging with a single consumer; message volumes are modest; or operational complexity is a primary constraint. In those cases, a managed queue (SQS, Cloud Tasks, RabbitMQ) is the pragmatic choice.

The payoff of event-driven architecture with Kafka is systems that scale independently, recover gracefully, and accumulate institutional memory in their event logs — but that payoff is proportional to the discipline applied to schema design, topic structure, and consumer group management.

Continue Reading
JCJOOTACEE / OPS

Operational laboratory for AI systems, automation infrastructures, and modular digital ecosystems.

Systems

  • AURA Orchestration
  • MCP Ecosystem
  • Graph Memory
  • AI Agents
  • Docker Infrastructure
  • Industrial Intelligence

System Status

PlatformOperational
APIHealthy
3D EngineActive
MCP Nodes8 Online

Try the Konami code...

Stay in the loop

Occasional updates on AI systems, autonomous infrastructure, and new releases.

© 2026 JootaCee. All systems operational.

RSSChangelogNext.js 16 + React 19 + R3F + GSAP