Deferring Stripe to a commit hook still leaves a window you cannot ROLLBACK. The row is durable when the hook throws, and you sit with a paid-looking ledger and no charge. Operators then retry the hook by hand, which lives on a different retry path than the callback, so a second charge lands if the first one actually went through. Put an idempotency key on the side effect itself or write an outbox row in the same transaction and drain it after COMMIT. Otherwise the hook just relocates the double-send one layer later.