DEV Community

Cover image for Laravel Eloquent: When to Use `boot()` vs. `booted()` (The Final Word)
Shakil Alam
Shakil Alam

Posted on

Laravel Eloquent: When to Use `boot()` vs. `booted()` (The Final Word)

Laravel is a framework that prides itself on elegance and clarity—but when it comes to model booting, many developers get confused between boot(), booted(), and even trait boots. If you’ve ever stared at an Eloquent model thinking, “Wait, when exactly is my event running?”, you’re in the right place.

This is your one-stop, no-fluff guide to understand, master, and use boot(), booted(), and trait boot methods like a pro.


1. What is Model Booting in Laravel?

Every Eloquent model in Laravel has a lifecycle—from creation, update, deletion, and query-building.

Booting is the static process Laravel runs before the model class is fully ready to be used. It gives you crucial hooks to:

  • Add static events (like creating, updating, deleting)
  • Apply global scopes
  • Initialize default attributes

Laravel’s boot process is flexible, but understanding the order is crucial to avoid “my event didn’t fire” moments.


2. Enter boot()

What it is:
boot() is a static method on the model itself. Override it when you need to add complex, model-level logic, or anything that requires the older, manual approach.

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected static function boot()
    {
        parent::boot(); // 🚨 Always call this

        static::creating(function ($product) {
            $product->uuid = (string) \Str::uuid();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Points

  • Runs before booted().
  • Must call parent::boot() to preserve trait and framework booting.
  • Ideal for global scopes and complex boot logic.
  • Runs once per model class (static context).
  • Historical Note: In early Laravel versions, this was the only way to register static model events.

3. Meet booted()

What it is:
booted() is a cleaner, safer alternative introduced in Laravel 7. It's a static method that runs after the model has been fully booted.

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected static function booted()
    {
        static::creating(function ($product) {
            $product->uuid = (string) \Str::uuid();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Why use booted()?

  • Automatically chains logic—No need to call parent::booted(), simplifying development.
  • Cleaner than overriding boot() for simple event registration.
  • Runs after all trait boot() methods and the main model boot().
  • Safer when multiple developers add boot logic, as it avoids accidental overwrites.

4. Trait Boot Methods

Laravel doesn’t just call boot()—it also calls trait boot methods automatically.

If a trait defines a method like boot[TraitName], Laravel will automatically call it before the model’s boot().

trait HasUuid
{
    // Note: Method name must be 'boot' + 'TraitName' (camel-cased)
    protected static function bootHasUuid() 
    {
        static::creating(fn($model) => $model->uuid = (string) \Str::uuid());
    }
}

class Product extends Model
{
    use HasUuid;
}
Enter fullscreen mode Exit fullscreen mode

Order of execution matters:

  1. All trait boots (bootHasUuid, bootSoftDeletes, etc.)
  2. Model boot()
  3. Model booted()

5. Visualization: The Boot Sequence

Here’s how Laravel actually runs things. This sequence is absolute:

[Traits boot* methods] -> [Model::boot()] -> [Model::booted()]
Enter fullscreen mode Exit fullscreen mode

The Takeaway:

  • Did you forget parent::boot()? The sequence may break.
  • Did you rely on booted() but needed a global scope? Scopes need boot() because they need to be applied earlier in the chain.

6. When to Use Which

Method Use Case
boot() Global scopes, complex boot logic, anything that needs parent::boot().
booted() Simple events, cleaner alternative to overriding boot(). The safer default for events.
Trait boot Shared logic across multiple models (e.g., UUID generation, Soft Deletes).

7. Real-Life Example: Combining All

trait HasUuid
{
    protected static function bootHasUuid() // Trait Boot
    {
        static::creating(fn($model) => $model->uuid = (string) \Str::uuid());
    }
}

class Product extends Model
{
    use HasUuid;

    protected static function boot() // Model boot()
    {
        parent::boot(); // REQUIRED

        // Logic here runs BEFORE booted()
        static::creating(fn($product) => $product->slug = \Str::slug($product->name));
    }

    protected static function booted() // Model booted()
    {
        // Logic here runs LAST
        static::created(fn($product) => logger("Product {$product->id} created!"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Execution order:

  1. HasUuid::bootHasUuid() → sets uuid
  2. Product::boot() → sets slug
  3. Product::booted() → logs creation

✅ All events happen in the right order, and nothing breaks.


8. Common Pitfalls

  • Forgetting parent::boot() in boot() → Trait boots won’t run, breaking features like Soft Deletes.
  • Using booted() for global scopes → It might run too late and the scope may not be applied to the initial query.
  • Multiple developers overriding boot() → Can overwrite each other's logic; booted() avoids this problem.

9. TL;DR – The Million-Dollar Takeaway

  • Trait boots → Run first.
  • Model boot() → Run second, required to call parent::boot().
  • Model booted() → Run last, perfect for event registration.
  • Use the right tool for the job: boot() for global logic/scopes, booted() for cleaner event hooks.

Mastering this will make you the Laravel developer everyone asks for when something “doesn’t fire”.


10. Closing Thoughts

Understanding boot(), booted(), and trait boots is not just theory—it’s the difference between fragile code and rock-solid models.

By respecting the boot sequence and using booted() where possible, you’ll write cleaner, safer, and easier-to-maintain Eloquent models.

Top comments (4)

Collapse
 
xwero profile image
david duymelinck

Scopes need boot()

So you are saying the official documentation got it wrong?
It clearly states use the ScopedBy attribute or add them to the booted method.

Multiple developers overriding boot() → Can overwrite each other's logic; booted() avoids this problem.

How is this a problem? There is only one boot method per model.
If that would be a problem it would also be a problem for the booted method.

Collapse
 
itxshakil profile image
Shakil Alam

Thanks for the thoughtful comment, David — that’s a really good point!

You’re right that each model only defines one boot() method, but the issue isn’t about having multiple boot() methods at the same time — it’s about how easy it is to accidentally break the boot chain when multiple developers touch the same model.

The boot() method participates in Laravel’s inheritance chain, which means every time you override it, you must call parent::boot() to ensure all trait boot methods (bootSoftDeletes, bootHasUuid, etc.) and parent model logic still execute. If even one developer forgets that call or replaces the method entirely, it overwrites the existing behavior — and that’s where problems creep in.

In contrast, booted() doesn’t rely on inheritance chaining at all. Laravel automatically collects and executes all booted() callbacks from the model and its traits. This makes it additive rather than fragile, which is why it’s safer when multiple developers or traits contribute boot logic to the same model.

So when I say:

“Multiple developers overriding boot() → Can overwrite each other’s logic; booted() avoids this problem.”

I don’t mean multiple methods literally exist — I mean boot() is prone to human error and overwritten logic due to inheritance, while booted() is designed to prevent that by construction.

👉 For global scopes, they should always be defined inside boot(), because scopes must attach before the model is fully booted to apply consistently across all queries. For example:

protected static function boot()
{
    parent::boot();

    static::addGlobalScope('active', function ($builder) {
        $builder->where('is_active', true);
    });
}

Meanwhile, booted() is ideal for lightweight event registration that happens after booting:

protected static function booted()
{
    static::created(fn ($model) => logger("{$model->getTable()} created: {$model->id}"));
}

In short:

  • boot() → for global scopes and early initialization (must call parent::boot()).
  • booted() → for post-boot events and cleaner, additive logic.

Both have their place — boot() gives you control, but booted() gives you safety.

Collapse
 
xwero profile image
david duymelinck

which means every time you override it

How can you override it many times? Can you give an example?

For global scopes, they should always be defined inside boot()

The example in the Laravel documentation is

use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The "booted" method of the model.
     */
    protected static function booted(): void
    {
        static::addGlobalScope(new AncientScope);
    }
}
Enter fullscreen mode Exit fullscreen mode

You can see it on their website. Why would they spread bad information about their own framework?

Thread Thread
 
itxshakil profile image
Shakil Alam

it’s not referring to multiple boot methods inside the same model class (because, as you said, there can only be one). The situation arises when traits or parent classes define their own boot() methods. PHP allows only one boot() method per final class, so if multiple traits define boot() directly, the last one wins, and earlier logic may be silently overwritten.

Here’s a minimal example:

trait HasUuid {
    protected static function boot(): void {
        static::creating(fn ($model) => $model->uuid = Str::uuid());
    }
}

trait LogsCreation {
    protected static function boot(): void {
        static::creating(fn ($model) => logger("Creating model {$model->id}"));
    }
}

class User extends Model
{
    use HasUuid, LogsCreation;

    protected static function boot(): void {
        parent::boot();
        static::creating(fn ($model) => $model->name = strtoupper($model->name));
    }
}
Enter fullscreen mode Exit fullscreen mode

Depending on trait resolution, some boot logic (like UUID generation or logging) may not run because only one boot() method exists in the final class.

The safe alternative is:

  1. Use bootTraitName() in traits (Laravel automatically calls them), or
  2. Use booted() in the model for event callbacks, which allows multiple hooks to coexist safely:
protected static function booted(): void {
    static::creating(fn ($model) => $model->name = strtoupper($model->name));
}
Enter fullscreen mode Exit fullscreen mode

Note: For global scopes, boot() is still recommended because they must exist before booted() callbacks.

I’m not denying the usefulness of boot() — it’s essential for global scopes, parent boot logic, or other critical pre-boot operations. My point is only that for multiple event callbacks (especially with traits), booted() is safer because it avoids subtle overwriting issues.