The 30 second answer
Writing to the database and Kafka as two separate steps can fail halfway. So I use the transactional outbox: the business row and an outbox row are written in one database transaction, and a relay using polling or change data capture publishes the outbox rows to Kafka. Delivery is at least once, so consumers deduplicate by event ID, and the idempotent producer stops duplicate writes on retry.
Don't write to the DB and Kafka in two separate steps, because one can fail after the other succeeds. Use the transactional outbox: write the business row and an outbox row in one DB transaction, and a relay (polling or CDC) publishes outbox rows to Kafka. Delivery is at least once, so consumers dedupe by event ID; the idempotent producer (enable.idempotence=true) stops producer retries from writing duplicates. Follow-up: Does Kafka exactly-once cover the DB write? (No, only Kafka-to-Kafka; side effects still need idempotency.)