Technology · Cloud & data

Kafka and event-driven systems, engineered for correctness.

Topics, outbox patterns, idempotent consumers and replay — the event backbone under our own platforms, built by engineers who run it.

KafkaWhat we build with it
01Event backbones between systems
02The outbox pattern by default
03Change data capture from legacy stores
04Idempotent consumers and stream processors
05Real-time pipelines for data and AI
06Operating the cluster
Usually runs with
  1. Kafka
  2. Kafka Connect
  3. Debezium
  4. Schema Registry
  5. Kafka Streams
  6. Flink
Why we use it

Enterprise integration often fails quietly.

A system calls another, the call times out, one side has written and the other has not, and nobody notices until the month-end reconciliation. Kafka changes the shape of the problem: a producer writes a fact once to a durable, ordered log, and every consumer reads it at its own pace, from the position it last acknowledged. A slow system no longer stalls a fast one, an outage becomes a backlog instead of a loss, and a bug can be fixed by replaying the log rather than re-keying the data.

It is part of the stack under our own platforms. A points transaction in Loyalty & Retention, a rated sale in Sales Performance, a load status in Order & Supply Management — each is the kind of fact that several services need to react to, and an event log lets reporting read it without touching the system of record. The integration estates we build for clients use the same patterns.

When we don’t: a request that needs an answer now is an API, not an event. A team with two services and one database should not install a distributed log to talk to itself. And a simple task queue — emails, thumbnails — is better served by RabbitMQ or a cloud queue than by a Kafka cluster.

What we build with it

Six things we build on Kafka.

01

Event backbones between systems

Domain events with owned topics, agreed schemas and a registry, so ERP, CRM, billing and the platforms in between exchange facts instead of nightly files.

02

The outbox pattern by default

The event is written in the same PostgreSQL transaction as the business change and relayed to Kafka by Debezium or a relay — which removes the dual-write race that loses messages.

03

Change data capture from legacy stores

Debezium on Oracle, SQL Server, MySQL or PostgreSQL write-ahead logs turns a system you cannot change into an event source — the standard first move in a legacy re-engineering programme.

04

Idempotent consumers and stream processors

Consumers in Go, Java or Node.js with explicit offset commits, deduplication keys, dead-letter topics and back-pressure; Kafka Streams or Flink where aggregation over windows is the job.

05

Real-time pipelines for data and AI

Streams into the warehouse and feature stores for fraud, operations and personalisation use cases where yesterday’s batch is too late — with Data Engineering owning the models downstream.

06

Operating the cluster

Managed Kafka (MSK, Confluent Cloud, Event Hubs) or Strimzi on Kubernetes, with partition strategy, retention, quotas, monitoring and the replay tooling that makes the log useful in an incident.

Fit

When to choose it.

Right fit

Choose Kafka when several systems need to react to the same facts, when the producer and consumers scale or fail independently, when you need the history — replay, audit, late-joining consumers — and when volume is high enough that per-message brokering becomes the bottleneck. It fits integration estates with more than a handful of systems, event-sourced or CQRS designs, and operational analytics that cannot wait for the nightly batch. It is also the honest answer to the question “how do we get data out of the mainframe without touching it”.

Look elsewhere

Look elsewhere when the interaction is a request that needs a synchronous response — that is REST or gRPC. Look elsewhere when you have a handful of background jobs and no ordering or replay requirement; a queue is simpler to run and to reason about. And be cautious when nobody in the organisation will own the cluster, the schemas and the consumer lag: an unowned Kafka is a very durable place to lose track of data.

In practice

One transaction, one event, no dual write.

award_points.sqlsql
begin;
insert into points_ledger (member_id, amount, reason, idempotency)
  values ($1, $2, 'flight-earn', $3);
-- the event is part of the same commit as the business fact
insert into outbox (aggregate, aggregate_id, type, payload)
  values ('member', $1, 'PointsAwarded', $4);
commit;
-- Debezium reads the outbox from the WAL and publishes to Kafka:
--   topic: loyalty.member.events   key: member_id   header: idempotency
-- consumers dedupe on the idempotency key and commit offsets after the write
+ create index on outbox (published_at) where published_at is null;
Reference architecture

An event backbone the way we hand it over.

Producers
Application outboxDebezium CDCPartner webhooksIoT / telematics feeds
Backbone
Kafka topicsSchema registryKafka ConnectDead-letter topics
Consumers
Idempotent servicesStream processorsNotification & searchWarehouse sink
Operations
Kubernetes / managed KafkaLag & throughput dashboardsReplay toolingTerraform
Our standards

What the event flows we hand over have.

  • No dual writes. Every event that must not be lost leaves the producer through an outbox table or CDC. A service that writes to its database and then publishes to Kafka in a second step fails our review.
  • Idempotent consumers keyed on a business identifier, with offsets committed only after the side effect is durable — so at-least-once delivery produces exactly-once outcomes.
  • Schemas in a registry (Avro or Protobuf, JSON Schema where the ecosystem needs it), with compatibility rules enforced in CI and a written ownership record per topic.
  • Partition keys chosen for ordering, not convenience. Events for one member, one load or one plan land on one partition; hot keys are looked for in load tests, not in production.
  • Dead-letter topics with a replay procedure, retry budgets with exponential back-off, and alerts on dead-letter volume and consumer lag.
  • Traces across the log. OpenTelemetry context propagated in headers, so one request can be followed from the API through three consumers to the warehouse.
  • Retention and residency decided per topic, with personal data mapped for GDPR / KVKK, encrypted in transit and at rest, and ACLs per service principal — no shared credentials.
  • Documented replay and disaster procedures — managed flows have a written, rehearsed way to reprocess a time range and to rebuild a consumer’s state from the log.
Typical pairings

What usually ships alongside the topics.

  • An event catalogue — topics, owners, schemas, keys, retention — kept in the repository next to the registry
  • Outbox and CDC connectors configured as code, with the Debezium or relay deployment
  • Consumer templates for your languages with idempotency, dead-lettering and tracing built in
  • Terraform for the managed cluster or the Strimzi operator, quotas and ACLs
  • Lag, throughput and dead-letter dashboards with alert policies exported as code
  • A replay and incident runbook, rehearsed with your team in a game day
Usually runs with
KafkaKafka ConnectDebeziumSchema RegistryKafka StreamsFlinkPostgreSQLGoJavaNode.jsKubernetesOpenTelemetry
FAQ

Frequently asked questions

Different jobs. Kafka is a durable, replayable log for facts that many systems consume; RabbitMQ and cloud queues are task queues for work that one worker should do once. Many estates need a deliberate mix, and we design for it rather than routing everything through one broker.
Managed — Amazon MSK, Confluent Cloud or Azure Event Hubs — unless residency or cost at very high volume argues otherwise. When self-hosting is the answer we run Strimzi on Kubernetes with the same automation, monitoring and rehearsed failover as the rest of the platform.
Usually yes. Debezium reads the database’s change log, so an Oracle, SQL Server or MySQL system that nobody dares to modify becomes an event source with no code change. We validate the feed against the source before anything downstream depends on it.
We guard against loss at the producer with the outbox pattern and at the consumer by committing offsets only after the effect is durable. Duplicates are expected and neutralised by idempotency keys.
It is visible — lag and dead-letter alerts are part of the flow. The log keeps the backlog safely while the fix ships, and the runbook covers replaying a time range or rebuilding the consumer’s state from scratch. An outage becomes a delay, not a reconciliation project.
Related

Neighbouring stacks and the services that use them.

Have two systems that disagree about the same fact?

Tell us which systems and what volume. A Kafka engineer reads it and replies in writing with how we would approach it.

✓ No pitch deck✓ Reply from an AI-native engineer✓ Written follow-up