Guide 08
Example: FastStream + Kafka (events)
Audience: Services emitting or consuming domain/integration events. Policy: Kafka for multi-consumer events; RabbitMQ remains for commands/jobs.
TL;DR
text
Producer (API or worker): after commit → outbox → Kafka topic
Consumer: FastStream subscriber → inbox → local projection / new commandContents
- When Kafka vs RabbitMQ
- Topic design
- Producer pseudocode
- Consumer pseudocode
- Ordering and keys
- Offsets and lag
- Schema evolution
- Run
---
1. When Kafka vs RabbitMQ
| Use Kafka | Use RabbitMQ |
|---|---|
| Many consumers of the same fact | One worker should do a job |
| Replay / log history | Per-message ack work queue |
| Stream processing | Classic task queue, DLX jobs |
2. Topic design
text
billing.events # domain events from billing
billing.invoice.paid # optional finer topicsEnvelope:
json
{
"event_id": "uuid",
"type": "invoice.paid.v1",
"occurred_at": "2026-08-05T12:00:00Z",
"payload": { "invoice_id": "...", "customer_id": "...", "amount_cents": 1200 }
}3. Producer pseudocode
python
# PSEUDOCODE : prefer outbox, not dual-write
async with db.transaction():
invoice.pay()
await db.save(invoice)
await db.outbox_add(
topic="billing.events",
key=invoice.customer_id,
payload=envelope,
)Relay publishes to Kafka with the key for partition affinity.
4. Consumer pseudocode
python
# PSEUDOCODE : FastStream-style
from faststream import FastStream
from faststream.kafka import KafkaBroker
broker = KafkaBroker(settings.kafka_bootstrap)
app = FastStream(broker)
@broker.subscriber("billing.events", group_id="notifications")
async def on_billing_event(message: dict):
if message["type"] != "invoice.paid.v1":
return
if await inbox_seen(message["event_id"]):
return
await enqueue_email_job(message["payload"]["invoice_id"])
await inbox_insert(message["event_id"])5. Ordering and keys
- Set key = aggregate id when order per entity matters
- Different keys → parallel partitions
- Do not assume global order across keys
6. Offsets and lag
- Commit offsets after successful side effects (or use inbox so reprocessing is safe)
- Alert on consumer lag as the primary backlog SLO
- Scale consumers up to partition count
7. Schema evolution
- Version in
type(invoice.paid.v1→v2) - Additive fields first; consumers ignore unknown keys
- Document in the event catalog (11)
8. Run
bash
# consumer process
faststream run app.workers.stream:app
# or uvicorn if mounted; keep consumer separate from API when possibleRelated: 11 Event-driven design · 04 Outbox