A Practical DDD + Kafka Microservices PoC: Outbox, DLQ, and Kubernetes (Helm + Kustomize)
Category: Software Architecture & Written by Francesco Malagrino On February-21-2026 16:32:35
This post is a guided tour of the repo:
https://github.com/Vegetam/microservices-ddd-kafka
Goal of this PoC:
Show a realistic baseline for event-driven microservices using DDD, Kafka, and reliable event publishing with the Transactional Outbox pattern — plus a deployable story on Kubernetes.
If you’ve seen too many tutorials that do “save to DB then publish to Kafka” and call it a day, this repo goes closer to the real world:
- Outbox to avoid losing events
- Hardened consumers (retry/backoff + DLQ + manual commits)
- Event schema validation
- K8s with Bitnami Kafka + one Postgres per service
TL;DR
- DDD for invariants and testability
- Outbox to prevent event loss
- Hardened consumers (manual commit + retry + DLQ)
- Schema validation to avoid outages
- Deployable on Kubernetes (Helm + Kustomize)
- Makefile so it’s runnable in two commands
Architecture in 30 seconds
Services
- OrderService: owns the Order domain (aggregate + invariants)
- PaymentService: consumes
order.created and processes payments
- NotificationService: reacts to business events (confirmed, failed, etc.)
Communication
- Kafka for domain events
- Each service has its own Postgres DB (no shared database)
- Redis where needed (cache/locks), optional depending on your setup
Why Transactional Outbox (what it actually fixes)
Classic failure mode:
INSERT order ✅
- publish
order.created ❌ (crash / network / deploy / SIGKILL)
Now you have an order in the DB but no event. The saga breaks.
How Transactional Outbox fixes it
- In the same transaction that saves the order, also insert a row into
outbox_events
- A separate worker reads
outbox_events and publishes to Kafka
- If Kafka is down: retry + backoff
- If an event is poison: DLQ
This doesn’t give you exactly-once end-to-end (almost nobody truly has that), but it does give you a robust baseline:
- at-least-once publishing
- no silent event loss
- better operability (inspect outbox backlog, retries, DLQs)
Kafka consumers: why “log error” is not enough
A “best-effort” consumer often:
- tries to process
- fails, logs, and still commits → message lost
- or crashes and loops forever → storm
In this repo, consumers are hardened with:
- manual commit: commit offsets only after success
- retry with exponential backoff + jitter
- DLQ: after N attempts, publish to
${topic}.dlq
- schema validation: if payload shape changes → DLQ immediately
Suggested naming convention:
- consumer DLQ:
topic.dlq
- outbox DLQ:
topic.outbox.dlq
Schema validation: small change, huge impact
One of the top causes of outages in event-driven systems:
one team changes the payload, another consumer breaks in production.
Here:
- events have schemas (e.g., Zod)
- consumers validate before invoking handlers
- invalid payloads go to DLQ + metrics
This turns “random crashes” into visible, manageable issues.
Kubernetes: Bitnami Kafka + one Postgres per service (Helm + Kustomize)
The repo includes:
- Helm charts for infrastructure dependencies (Kafka/Redis/Postgres) using Bitnami
- Kustomize overlays:
kind (local dev)
prod (baseline)
What people often skip — but matters — is included:
- readiness/liveness probes
securityContext (non-root, drop caps, read-only fs)
- resource requests/limits
- graceful shutdown
- env from Secrets/ConfigMaps (no plaintext credentials in manifests)
Makefile: a human workflow
Instead of 30 commands, you get:
make kind-up
make kind-deploy
make status
make logs SERVICE=order-service
make reset
Why this matters:
- fewer mistakes
- easier onboarding
- readers actually run the repo
How I’d demo it (happy path)
- Create an order (OrderService)
- OrderService writes order + outbox row (same DB transaction)
- OutboxWorker publishes
order.created
- PaymentService consumes and emits
payment.processed or payment.failed
- OrderService consumes the outcome and updates state (domain invariants enforced)
- NotificationService reacts and sends notifications
If you want to extend:
- add “refund”
- add consumer idempotency (processed-events table / unique constraints)
- add tracing (OpenTelemetry)
What I’d add for “real production” (beyond a PoC)
If this is going to real prod:
- OpenTelemetry tracing + correlation IDs end-to-end
- richer metrics + alerting (consumer lag, DLQ rate, outbox backlog)
- a schema registry (Avro/Protobuf) as the team grows
- Kafka auth/TLS + ACLs
- integration tests in CI (kind) with happy-path + poison-pill tests
But as a learning baseline, this repo is already “serious”: it shows real problems and pragmatic fixes.
Repo
https://github.com/Vegetam/microservices-ddd-kafka