Building a Collision-Safe URL Shortener in Laravel
URL shorteners look simple until you have to guarantee two users never get the same short code and figure out how to track clicks without slowing down every redirect. Here's a complete, production-shaped implementation.
Step 1: The migration
Schema::create('short_urls', function (Blueprint $table) {
$table->id();
$table->string('code', 10)->unique();
$table->text('destination_url');
$table->unsignedInteger('clicks')->default(0);
$table->foreignId('user_id')->nullable()->constrained();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
Step 2: Generating collision-safe codes
Rather than generating a random string and hoping it's unique, encode the row's own auto-incrementing ID into base-62. This guarantees uniqueness by construction, no collision checking required:
namespace App\Support;
class Base62
{
private const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
public static function encode(int $number): string
{
if ($number === 0) {
return self::ALPHABET[0];
}
$base = strlen(self::ALPHABET);
$encoded = '';
while ($number > 0) {
$encoded = self::ALPHABET[$number % $base] . $encoded;
$number = intdiv($number, $base);
}
return $encoded;
}
}
Step 3: The ShortUrl model
namespace App\Models;
use App\Support\Base62;
use Illuminate\Database\Eloquent\Model;
class ShortUrl extends Model
{
protected $fillable = ['destination_url', 'user_id', 'expires_at'];
protected static function booted(): void
{
static::created(function (ShortUrl $shortUrl) {
$shortUrl->update(['code' => Base62::encode($shortUrl->id)]);
});
}
public function isExpired(): bool
{
return $this->expires_at !== null && $this->expires_at->isPast();
}
}
Because the code is derived from the auto-incrementing primary key after the row is created, there is no window where two requests could generate and race to claim the same code. The database's own uniqueness guarantee for id does the work.
Step 4: The controller
namespace App\Http\Controllers;
use App\Models\ShortUrl;
use Illuminate\Http\Request;
class ShortUrlController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'destination_url' => ['required', 'url', 'max:2048'],
]);
$shortUrl = ShortUrl::create([
'destination_url' => $validated['destination_url'],
'user_id' => $request->user()?->id,
]);
return response()->json([
'short_url' => url("/s/{$shortUrl->code}"),
]);
}
public function redirect(string $code)
{
$shortUrl = ShortUrl::where('code', $code)->firstOrFail();
abort_if($shortUrl->isExpired(), 410, 'This link has expired.');
ShortUrl::whereKey($shortUrl->id)->increment('clicks');
return redirect()->away($shortUrl->destination_url);
}
}
Using increment() on the query builder rather than $shortUrl->increment('clicks') on the loaded model avoids an extra SELECT and issues a single atomic UPDATE ... SET clicks = clicks + 1, which matters once a popular link is getting hit concurrently.
Step 5: Routes
use App\Http\Controllers\ShortUrlController;
Route::post('/urls', [ShortUrlController::class, 'store']);
Route::get('/s/{code}', [ShortUrlController::class, 'redirect'])->name('short-url.redirect');
Step 6: Keeping redirects fast with caching
If a link goes viral, you don't want every click hitting the database. Cache the lookup with a short TTL:
public function redirect(string $code)
{
$destination = cache()->remember("short_url:{$code}", now()->addMinutes(10), function () use ($code) {
return ShortUrl::where('code', $code)->firstOrFail()->destination_url;
});
ShortUrl::where('code', $code)->increment('clicks');
return redirect()->away($destination);
}
Wrapping up
The trick that makes this whole system simple is letting the database's own auto-incrementing ID do the uniqueness work instead of generating random codes and checking for collisions. Combine that with an atomic increment for click counts and a short-lived cache in front of the lookup, and you have a shortener that holds up under real traffic without any locking or retry logic.