Engineering Note
Engineering

Kafka for Event-Driven Systems

Decoupling Services Through Events

12 min read
AdvancedEngineering

Introduction

Kafka is useful when systems need to react to events independently. Instead of tightly connecting services through direct calls, Kafka allows producers to publish events and consumers to process them at their own pace.

This makes Kafka powerful for telemetry pipelines, audit logs, background processing, activity streams, analytics, crash detection, notification systems, and event-driven architectures where multiple services need to respond to the same system activity.

This note focuses on practical engineering decisions behind using Kafka for event-driven systems, especially the parts that affect scalability, reliability, maintainability, observability, and user experience.

The Problem

Direct service-to-service communication works at small scale, but it becomes harder to manage when systems grow. One service may become slow, another may fail, and tightly coupled workflows can make the entire system fragile.

Common Failures

  • Direct service calls create tight coupling between systems
  • One slow service can block another service
  • Events are difficult to replay without a durable log
  • Telemetry systems need reliable event flow and ordering
  • Consumers can fail silently without offset and error tracking
  • Payload changes can break downstream consumers without versioning

Engineering Impact

  • Failures spread across dependent services
  • Debugging becomes harder without event history
  • Scaling individual workflows becomes more difficult
  • Recovery becomes harder when events are not persisted
  • Backpressure becomes harder to control during traffic spikes
  • Event contracts become risky when producers and consumers evolve separately

The challenge is to design event flow in a way that keeps services independent while still making events reliable, traceable, versioned, and easy to process.

System Design / Approach

The approach is to treat events as system contracts. Producers should publish meaningful events, topics should separate event categories, and consumers should own their own processing logic.

Producer Service
    ↓
Kafka Topic
    ↓
Consumer Group
    ↓
Worker / Processor
    ↓
Database / Cache / Alerting
    ↓
Monitoring and Replay

1. Use Topics to Separate Event Categories

Topics should represent clear event streams such as telemetry, audit logs, user activity, payment events, notification events, or system crashes.

2. Keep Event Payloads Explicit

Events should include enough context for consumers to process them without needing unnecessary follow-up calls to other services.

3. Let Consumers Evolve Independently

Consumers should subscribe to the events they need and process them independently, without forcing changes in the producer.

4. Design for Replay and Failure Recovery

Events should be durable enough to support retries, debugging, backfills, and rebuilding downstream state.

Implementation

Step 1: Publish Events to a Topic

A producer sends an event when something important happens in the system. The producer does not need to know which services will consume that event.

producer.ts
await producer.send({
  topic: "aegis.telemetry.crashes",
  messages: [
    {
      key: event.service,
      value: JSON.stringify({
        eventId: event.id,
        eventType: "service.crashed",
        version: 1,
        service: event.service,
        severity: event.severity,
        timestamp: new Date().toISOString(),
      }),
    },
  ],
});

Topics create durable streams of system activity that different consumers can read and process independently.

Step 2: Consume Events Independently

Consumers subscribe to topics and process events separately. This allows analytics, alerting, auditing, and remediation workflows to evolve without changing the producer.

consumer.ts
await consumer.run({
  eachMessage: async ({ topic, partition, message }) => {
    const event = JSON.parse(message.value!.toString());

    await handleEvent({
      topic,
      partition,
      offset: message.offset,
      event,
    });
  },
});

Consumers can scale, fail, restart, and evolve independently because they are not directly blocking the producer.

Step 3: Design Event Payloads

Events should include enough context for downstream consumers to understand what happened, where it happened, and when it happened.

crash-event.json
{
  "eventId": "evt_01HZX",
  "eventType": "service.crashed",
  "version": 1,
  "service": "demo-api",
  "type": "oom",
  "severity": "critical",
  "timestamp": "2026-06-08T10:00:00Z",
  "metadata": {
    "containerId": "demo-api-01",
    "host": "local-aegis-node"
  }
}

Good event payloads reduce extra service calls and make auditing, debugging, replay, and recovery easier.

Step 4: Use Keys for Ordering

Kafka ordering is guaranteed within a partition. Using a stable key helps related events go to the same partition so consumers can process them in order for that entity.

partition-key.ts
await producer.send({
  topic: "aegis.telemetry.crashes",
  messages: [
    {
      key: crashEvent.service,
      value: JSON.stringify(crashEvent),
    },
  ],
});

Using the service name as a key helps keep events for the same service ordered within a partition.

Step 5: Handle Consumer Failures

Consumers should handle failures carefully. A malformed event or temporary database issue should not silently stop the entire processing pipeline.

consumer-errors.ts
try {
  await processCrashEvent(event);
} catch (error) {
  await deadLetterProducer.send({
    topic: "aegis.telemetry.crashes.dlq",
    messages: [
      {
        key: event.eventId,
        value: JSON.stringify({
          event,
          reason: error.message,
          failedAt: new Date().toISOString(),
        }),
      },
    ],
  });
}

Dead-letter topics keep failed events visible instead of losing them or blocking the full stream.

Step 6: Make Consumers Idempotent

Kafka consumers may process the same event more than once in some failure scenarios. Consumers should be designed so duplicate processing does not create duplicate side effects.

idempotent-consumer.ts
const alreadyProcessed = await db.processedEvent.findUnique({
  where: {
    eventId: event.eventId,
  },
});

if (alreadyProcessed) {
  return;
}

await handleEvent(event);

await db.processedEvent.create({
  data: {
    eventId: event.eventId,
    processedAt: new Date(),
  },
});

Idempotent consumers keep retries and reprocessing safe.

Step 7: Add Event Schema Validation

Since events are contracts, malformed payloads should be rejected or routed into a failure stream. Schema validation helps protect consumers from unexpected event shapes.

event-schema.ts
const CrashEventSchema = z.object({
  eventId: z.string(),
  eventType: z.literal("service.crashed"),
  version: z.number(),
  service: z.string(),
  severity: z.enum(["low", "medium", "high", "critical"]),
  timestamp: z.string(),
});

const event = CrashEventSchema.parse(rawEvent);

Validation makes event-driven systems safer as producers and consumers evolve independently.

Step 8: Monitor Consumer Lag

Consumer lag shows whether consumers are keeping up with the event stream. High lag usually means processing is too slow, consumers are failing, or traffic has increased.

consumer-lag.ts
console.info("Kafka consumer processed event", {
  topic,
  partition,
  offset: message.offset,
  eventId: event.eventId,
  processedAt: new Date().toISOString(),
});

Lag monitoring helps detect bottlenecks before downstream systems fall behind.

Trade-offs

Approach Benefit Cost
Kafka Topics Durable event streams for multiple independent consumers Adds operational complexity and infrastructure responsibility
Async Events Decouples services and prevents one slow service from blocking others Introduces eventual consistency and harder debugging flows
Event Replay Useful for debugging, recovery, backfills, and rebuilding projections Requires careful payload design, retention planning, and versioning
Partition Keys Preserves ordering for related events within a partition Bad keys can create hot partitions or uneven load
Dead-Letter Topics Keeps failed events available for debugging and recovery Requires inspection and replay tooling
Schema Validation Protects consumers from malformed payloads Requires schema maintenance and versioning discipline

Real-World Impact

Decoupled Services

Services become less tightly connected because producers and consumers do not need direct knowledge of each other.

Reliable Telemetry

Logs, crashes, audits, and activity events become easier to process through durable event streams.

Replay and Recovery

Events can be replayed for debugging, rebuilding state, recovering failed workflows, and running historical analysis.

What I Learned

  • Kafka is useful when multiple services need to react to the same event independently.
  • Topics should represent meaningful event streams, not random message buckets.
  • Event payloads should be explicit because they become contracts between services.
  • Partition keys affect ordering, load distribution, and processing behavior.
  • Consumers must be idempotent because events may be processed more than once.
  • Dead-letter topics are important for debugging failed event processing.
  • Consumer lag is a key signal for detecting processing bottlenecks.

Conclusion

Kafka makes systems more flexible by separating event production from event processing. Producers publish events once, while multiple consumers can process those events independently.

A strong Kafka architecture depends on clear topics, explicit event payloads, stable partition keys, idempotent consumers, schema validation, dead-letter topics, and monitoring for consumer lag.

The key lesson is simple: Kafka is not only a messaging tool. It is an event backbone that helps distributed systems stay decoupled, replayable, and resilient under real-world conditions.

Key Takeaways

Kafka is useful when events are part of the system architecture

Producers and consumers should be independently designed

Event payloads need enough context for future processing

Asynchronous systems introduce eventual consistency

Event replay is powerful for debugging and auditing

Future Improvements

Add event schema validation

Introduce dead-letter topics

Add event versioning strategy

Monitor consumer lag

Document topic ownership and payload formats