---
title: "Uniqueness & concurrency — Sluggable for Laravel | Roundly"
description: "Global, scoped or closure-based uniqueness, soft-delete handling, bounded probing and an automatic retry when two writers collide."
url: https://roundly-consulting.com/open-source/docs/sluggable-for-laravel/uniqueness
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Sluggable for Laravel](https://roundly-consulting.com/open-source/docs/sluggable-for-laravel.md)

# Uniqueness & concurrency

Slugs are unique globally by default. Narrow or drop the rule per definition:

```php
use Illuminate\Database\Eloquent\Builder;

SlugDefinition::for('slug')->from('name')->unique();                          // global (default)
SlugDefinition::for('slug')->from('name')->uniqueWithin('clinic_id');         // per clinic — DB-indexable
SlugDefinition::for('slug')->from('name')->uniqueWithin('shop_id', 'category_id');
SlugDefinition::for('slug')->from('name')->uniqueWhere(
    fn (Builder $query, Post $post) => $query->where('type', $post->type),   // application-level only
);
SlugDefinition::for('code')->from('sku')->notUnique();
SlugDefinition::for('slug')->from('name')->excludeTrashed();                  // trashed rows free their slug
```

## How collisions are checked

- Without global scopes — a tenancy, published or SoftDeletes scope never hides a taken slug — then with the definition’s scope columns or closure, the trashed rule and the model’s own key excluded. NULL is a scope value to the check.
- Soft-deleted rows keep their slug reserved by default (include\_trashed), so restores are safe and a plain unique index agrees; excludeTrashed() frees them, and a restore then re-checks and re-suffixes if needed.
- Reserved words (config and reserved()) count as taken.
- Changing a scope column on update re-checks uniqueness and re-suffixes from the source body — chair-2 becomes chair when that is free, never chair-2-2 (or throws for Strict).
- uniqueAcrossLocales() and uniqueWhere() closures are enforced by the application only.

## Bounded probing

Probing is bounded and batched: the base first (one query in the common case), then sequential candidates in batches of 10 — at most six queries for 50 collisions — then 10 random suffixes, then SlugGenerationException::exhausted(). The limits.\* config keys tune every bound.

## Concurrency

Two writers can pick the same free slug. The unique index rejects the loser; sluggable catches the UniqueConstraintViolationException, re-probes with a locking read, re-suffixes and retries (concurrency.retries, default 3). Inside an outer transaction each attempt runs in a SAVEPOINT, so the transaction stays usable on PostgreSQL.

- A violation that names no slug index is rethrown untouched.
- A re-probe that finds nothing taken throws SlugGenerationException::constraintMismatch() — your index disagrees with the definition; run sluggable:indexes --dry-run.
- A Strict manual value that loses the race throws SlugAlreadyTakenException; used-up retries throw retriesExhausted().
- Each retry dispatches SlugCollisionRetried and re-fires creating/updating for your own listeners — use retries(0) on models with non-idempotent listeners.

Only sluggable’s own values are re-suffixed. Inside Slugs::withoutGeneration() the retry is off too: a duplicate slug you write yourself fails with the database’s UniqueConstraintViolationException. Under ManualSlugPolicy::Strict, a value no generation pass produced in this save — a manual value, or one saved with saveQuietly() without Slugs::apply() — throws SlugAlreadyTakenException instead of being rewritten.

```php
SlugDefinition::for('slug')->from('name')->retries(5);   // more race retries
SlugDefinition::for('slug')->from('name')->retries(0);   // rethrow the violation untouched
```

Uniqueness without a database index — closure scopes, across-locales, or tables without the index — is best-effort under concurrency. Create the engine-native indexes to make it airtight.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/sluggable-for-laravel.md)
