May 22, 2026

Stop Using UUIDs: Why B2B SaaS Needs ULIDs in Laravel

By Paresh Prajapati • Lead Architect

Stop Using UUIDs: Why B2B SaaS Needs ULIDs in Laravel

The Problem with Auto-Incrementing IDs

When building a B2B SaaS platform at Smart Tech Devs, using standard auto-incrementing integers (1, 2, 3) for your primary keys is an enormous security liability. If a user sees /invoices/405 in their URL, they immediately know you only have 405 invoices in your system. Worse, malicious actors can easily write a script to scrape /invoices/406, 407, and 408 (an Insecure Direct Object Reference attack).

The industry standard solution for the last decade has been UUIDs (Universally Unique Identifiers). A UUIDv4 looks like 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d. It is completely random and impossible to guess. But at massive scale, UUIDs introduce a catastrophic performance flaw at the database level.

The B-Tree Fragmentation Catastrophe

PostgreSQL and MySQL use B-Tree structures for their primary key indexes. B-Trees are designed for sequential data. When you insert row #1, then row #2, the database simply appends the new data to the end of the tree. It is blazingly fast.

UUIDv4s, however, are perfectly random. When you insert a new row, the database has to scan the massive B-Tree, split the existing nodes, rewrite the data, and rebalance the tree just to fit the new UUID somewhere in the middle. At 10 million rows, this Index Fragmentation destroys your write performance, spikes your server's CPU, and consumes massive amounts of RAM because the index no longer fits cleanly into memory.

The Enterprise Solution: ULIDs

To architect databases for infinite scale, we must abandon UUIDv4 and adopt ULIDs (Universally Unique Lexicographically Sortable Identifiers).

A ULID solves the fragmentation problem by combining a timestamp with a random secure string. Because the first half of the string represents the exact millisecond it was created, ULIDs naturally sort chronologically. To your database, inserting a ULID is just like inserting an auto-incrementing integer—it appends it to the end of the index. Write performance stays perfectly flat at O(1), regardless of table size, while retaining 100% of the unguessable security of a UUID.

Implementing ULIDs in Laravel

Laravel makes the transition to ULIDs incredibly elegant. You simply swap a single trait on your Eloquent models.


namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUlids;

class Invoice extends Model
{
    // 1. Replace the HasUuids trait with HasUlids
    use HasUlids;

    // 2. Disable auto-incrementing since our Primary Key is a string
    public $incrementing = false;

    // 3. Explicitly declare the Key Type
    protected $keyType = 'string';

    protected $fillable = [
        'tenant_id',
        'amount',
        'status',
    ];
}

Updating the Database Migration

Laravel's Blueprint class has a native ulid() method, allowing you to seamlessly set up your database schema.


use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateInvoicesTable extends Migration
{
    public function up(): void
    {
        Schema::create('invoices', function (Blueprint $table) {
            // Generates a CHAR(26) primary key column optimized for ULIDs
            $table->ulid('id')->primary();
            
            $table->foreignId('tenant_id')->constrained();
            $table->decimal('amount', 10, 2);
            $table->string('status');
            $table->timestamps();
        });
    }
}

Conclusion

Architecture is about foreseeing bottlenecks before they crush your servers. UUIDv4s were a great solution for security, but a terrible solution for database physics. By adopting ULIDs in your Laravel applications, you secure your endpoints from enumeration attacks while maintaining the blazing-fast write speeds of sequential indexing.

Paresh Prajapati
Lead Architect, Smart Tech Devs