Retry a deadlocked transaction without repeating remote effects
Two transactions acquire rows in different orders and one is selected as a deadlock victim. A bounded retry can be appropriate, but the transaction callback may run again. Anything inside it must be safe under that repetition.
Keep the callback focused on transactional work
DB::transaction(function () use ($transfer) {
// Lock the required rows in a consistent order.
// Validate current balances and apply database changes.
}, attempts: 3);
This fragment intentionally leaves the domain writes to your application. Laravel can retry the transaction for supported concurrency failures. Use deterministic lock ordering and suitable indexes to reduce contention rather than treating retries as the primary fix.
A database rollback cannot unsend an API request
Sending an email or charging a remote service inside the callback can repeat the effect even when the database changes were rolled back. Move effects behind a durable after-commit or outbox boundary, with their own idempotency policy.
Keep transactions short and avoid waiting for user input or slow network calls while locks are held. Record enough context to investigate repeated deadlocks, without logging account secrets or complete sensitive payloads.
Test the transaction's invariant and its retry behavior against the actual database engine when concurrency matters. SQLite tests may not reproduce MySQL's locking behavior. A retry-count assertion alone does not show that a transfer preserved both balances or that a notification was emitted only according to the intended delivery policy.