Every broker worth using guarantees at-least-once delivery, and the phrase is a warning rather than a feature: a message can and will arrive twice. Exactly-once is not available at any price the system can pay.
public function handle(OrderPlaced $event)
{
// natural key, unique constraint, insert wins or throws
if (! $this->invoices->createIfAbsent($event->orderId)) {
return; // already done, and that is fine
}
$this->send($event->orderId);
}
The duplicate arrives because the consumer crashed after doing the work and before acknowledging — the broker has no way to know which. The fix is always the same: find a natural key and let the database reject the second attempt. Where no natural key exists, a processed-message table keyed by message id is the fallback, and it needs an expiry policy of its own.