Note: I used ChatGPT to help structure parts of this explanation, but the approach described is from my actual production experience.
Laravel 12.x supports UUIDv7 out of the box (and Laravel 11.x introduced it). However, if you want to use both UUID7 and ULID together on the same model, you'll need a custom solution.
✅ Internal vs. External Identifiers
Using UUIDv7 for primary keys (e.g., id) is an excellent choice—especially for scalable applications. UUIDs are globally unique and make tasks like data merging and system interoperability much safer by minimizing the risk of key collisions.
One of the key advantages of using UUIDs as internal identifiers is that you can perform direct database operations without relying solely on APIs. This encourages cleaner, more decoupled API design.
However, UUIDs (especially v7) are long and not user-friendly. Therefore, I strongly recommend not exposing UUIDs to end users. Instead, use ULIDs for public-facing IDs.
For example:
- Use
id (UUIDv7) internally for primary keys, foreign keys, and unique constraints.
- Use
public_id (ULID) externally in URLs, APIs, and UI displays.
This separation protects internal infrastructure and keeps your system's internals abstracted from external consumers.
🛠 CRUD Operations and Routing
Always expose and interact with the public_id for all CRUD operations. This adds a slight abstraction layer but helps shield internal identifiers and improves system design.
Laravel allows you to customize route model binding to use public_id:
public function getRouteKeyName(): string
{
return 'public_id';
}
⚖️ Performance Considerations
It's true that UUIDs and ULIDs can be slightly slower than auto-incrementing integers due to their size and index performance. However, the benefits—global uniqueness, scalability, and flexibility—far outweigh these drawbacks, especially for distributed systems.
UUIDv7 offers the additional benefit of lexical sortability. Because it encodes a timestamp in milliseconds, newly generated UUIDs are naturally ordered. This improves index locality and query performance compared to UUIDv4.
🔄 Data Portability
Another major benefit of UUIDs: they're ideal for syncing data across environments or services. Since UUIDs are unique across systems, there's very little risk of collision when importing or migrating data.
✅ Recommended Strategy
- Internal IDs (
id, FK, PK, UK): Use UUIDv7. Never expose.
- External IDs (
public_id): Use ULID. Always expose.
🧩 Custom Trait: HasUuidsAndPublicId
Since Laravel does not currently support using both HasUuids and HasUlids traits together in a single model, you can define a custom trait to achieve this:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Support\Str;
trait HasUuidsAndPublicId
{
use HasUuids;
/**
* Boot the trait to auto-generate public_id before creation.
*/
public static function bootHasUuidsAndPublicId(): void
{
static::creating(function ($model) {
if (empty($model->public_id)) {
$model->public_id = $model->newPublicId();
}
});
}
/**
* Generate a lowercase ULID for public_id.
*/
public function newPublicId(): string
{
return strtolower(Str::ulid());
}
/**
* Use public_id as the route model binding key.
*/
public function getRouteKeyName(): string
{
return 'public_id';
}
}
Use this trait on your models:
class User extends Model
{
use HasUuidsAndPublicId;
protected $table = 'users';
}
⚠️ Note: Trying to use both HasUuids and HasUlids traits simultaneously, like this:
use HasUuids, HasUlids;
will not work, as Laravel does not currently support combining both traits on the same model.
Final Thoughts
For scalable Laravel applications:
- Use UUIDv7 for internal keys.
- Use ULIDs for external identifiers.
- Encapsulate this logic in custom traits like
HasUuidsAndPublicId.
This strategy balances performance, security, and maintainability, and is already adopted by many large-scale systems.