z-go-messaging
Go messaging
The moment a consumer handler touches anything outside the broker's own transactional boundary — a Postgres write, an HTTP call, an email — delivery is at-least-once, full stop. Kafka's exactly-once semantics (idempotent producer + transactions) only hold for a consume-transform-produce loop that never leaves Kafka. RabbitMQ and NATS core have no exactly-once mode at all. Design every consumer as if redelivery will happen, because it will: broker crashes, rebalances, client restarts, and network partitions all trigger reprocessing of already-handled messages.
Idempotent consumers
Two ways to make a handler safe under redelivery, in order of preference:
| Strategy | When | Cost |
|---|---|---|
| Idempotent-by-construction | Repeating the write produces the same result; an upsert that increments a counter does not qualify | No extra dedup table |
| Dedup table keyed by message ID | Non-idempotent counters, appends, or multi-statement effects in the same database transaction | One extra row + index per message |
Dedup table pattern — the dedup INSERT and the business write share one transaction, so either both land or neither does:
External HTTP, email, or payment calls are outside this transaction. Use an outbox for durable dispatch and downstream idempotency for repeated calls.