The Complete Responsive CMS Blog created by Francesco Malagrino

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:



  1. INSERT order

  2. 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)



  1. Create an order (OrderService)

  2. OrderService writes order + outbox row (same DB transaction)

  3. OutboxWorker publishes order.created

  4. PaymentService consumes and emits payment.processed or payment.failed

  5. OrderService consumes the outcome and updates state (domain invariants enforced)

  6. 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


Share


Comments

Share your thoughts about this post