Back to Blogs
2026-07-09 Abdelrhman Yasser

Laravel Events & Observers: Production Patterns

Laravel Events & Observers: Decoupled Application Architecture

Reading Time: 25 minutes

Introduction

Events and Observers are Laravel's primary tools for decoupling application logic. They allow you to react to发生的事情 without coupling the triggering code to the reaction.

An Event is a recorded occurrence — "a user registered", "an order was placed", "a payment failed". A Listener is the code that responds to that event. An Observer is a listener specialized for Eloquent model events.

This separation lets you add features like sending emails, logging activity, updating caches, or notifying third-party services — without modifying the original code that triggered the event.

The Problem

Without events, adding cross-cutting concerns leads to tightly coupled, hard-to-maintain code:

class AuthController extends Controller
{
    public function register(RegisterRequest $request): JsonResponse
    {
        $user = User::create($request->validated());

        // Cross-cutting concerns mixed with registration logic
        Mail::to($user)->send(new WelcomeEmail($user));
        Log::info('User registered', ['id' => $user->id]);
        $this->analytics->track('user_registered', $user);
        $user->notify(new VerifyEmail);

        return response()->json($user, 201);
    }
}

Every new requirement — send a Slack notification, create a default workspace, sync to CRM — requires modifying AuthController. This breaks the Open/Closed Principle.

Events and Listeners

graph LR
    A[UserRegistered Event] --> B[SendWelcomeEmail]
    A --> C[LogRegistration]
    A --> D[SyncToCRM]
    A --> E[CreateDefaultWorkspace]
    A --> F[NotifyAdmins]

    G[Register Code] -- dispatches --> A

Creating Events and Listeners

# Create both event and listener
php artisan make:event UserRegistered
php artisan make:listener SendWelcomeEmail --event=UserRegistered

# Or generate the whole pair at once
php artisan event:generate

Defining an Event

class UserRegistered
{
    use Dispatchable, InteractsWithSockets;

    public function __construct(
        public User $user,
    ) {}
}

Defining a Listener

class SendWelcomeEmail
{
    public function __construct(
        private Mailer $mailer,
    ) {}

    public function handle(UserRegistered $event): void
    {
        $this->mailer->send(
            $event->user->email,
            new WelcomeEmail($event->user)
        );
    }
}

class LogRegistration
{
    public function handle(UserRegistered $event): void
    {
        Log::info('User registered', [
            'id' => $event->user->id,
            'email' => $event->user->email,
            'time' => now(),
        ]);
    }
}

Registering Events and Listeners

Laravel 11+ (bootstrap/app.php):

return Application::configure(basePath: dirname(__DIR__))
    ->withEvents()
    ->withRouting(...)
    ->create();

Use the EventServiceProvider pattern or the boot method:

// AppServiceProvider or EventServiceProvider
Event::listen(
    UserRegistered::class,
    SendWelcomeEmail::class,
);

Event::listen(
    UserRegistered::class,
    LogRegistration::class,
);

Dispatching Events

// Using the event helper
event(new UserRegistered($user));

// Using the dispatch method on the event
UserRegistered::dispatch($user);

// Using the Event facade
Event::dispatch(new UserRegistered($user));

The controller becomes clean:

class AuthController extends Controller
{
    public function register(RegisterRequest $request): JsonResponse
    {
        $user = User::create($request->validated());

        UserRegistered::dispatch($user);

        return response()->json($user, 201);
    }
}

Event Subscribers

Subscribers are classes that define multiple event listeners internally:

class UserEventSubscriber
{
    public function handleUserRegistered(UserRegistered $event): void
    {
        // Handle registration
    }

    public function handleUserDeleted(UserDeleted $event): void
    {
        // Handle deletion
    }

    public function handlePasswordReset(PasswordReset $event): void
    {
        // Handle password reset
    }

    public function subscribe(Dispatcher $events): array
    {
        return [
            UserRegistered::class => 'handleUserRegistered',
            UserDeleted::class => 'handleUserDeleted',
            PasswordReset::class => 'handlePasswordReset',
        ];
    }
}

Registration:

Event::subscribe(UserEventSubscriber::class);

Queued Listeners

Slow listeners should be queued to avoid delaying the response:

class SendWelcomeEmail implements ShouldQueue, ShouldBeUnique
{
    public string $queue = 'emails';
    public int $delay = 10; // Delay in seconds
    public int $tries = 3;

    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user)->send(new WelcomeEmail($event->user));
    }

    // Unique job — only one WelcomeEmail per user
    public function uniqueId(): string
    {
        return $event->user->id;
    }

    // Retry delay
    public function backoff(): array
    {
        return [5, 10, 30];
    }

    // On failure
    public function failed(UserRegistered $event, Throwable $e): void
    {
        Log::error('Welcome email failed', [
            'user' => $event->user->id,
            'error' => $e->getMessage(),
        ]);
    }
}

Conditional Queueing

// Always on queue
Event::listen(
    UserRegistered::class,
    SendWelcomeEmail::class,
);

// Conditionally (in listener constructor or middleware)
class SendWelcomeEmail
{
    public bool $queue = true; // Always queue

    // Or, use ShouldQueue interface on the listener class itself
}

Model Events

Eloquent fires events automatically during model lifecycle:

EventTriggered When
retrievedAfter a model is fetched from DB
creatingBefore a model is saved for the first time
createdAfter a model is saved for the first time
updatingBefore an existing model is saved
updatedAfter an existing model is saved
savingBefore a model is created OR updated
savedAfter a model is created OR updated
deletingBefore a model is deleted
deletedAfter a model is deleted
trashedBefore a model is soft-deleted
forceDeletedAfter a model is force-deleted
restoringBefore a soft-deleted model is restored
restoredAfter a soft-deleted model is restored
replicatingBefore a model is replicated

Using Model Events in the Boot Method

class User extends Model
{
    protected static function booted(): void
    {
        static::creating(function (User $user) {
            // Set default values before creation
            $user->uuid ??= (string) Str::uuid();
        });

        static::created(function (User $user) {
            // Create default profile
            $user->profile()->create([
                'avatar' => 'default.png',
            ]);
        });

        static::updated(function (User $user) {
            // Log profile changes
            Log::info('User updated', ['id' => $user->id]);
        });

        static::deleted(function (User $user) {
            // Clean up related data
            $user->posts()->delete();
            $user->comments()->delete();
        });
    }
}

Observers

Observers group all model event listeners into a single class:

php artisan make:observer UserObserver --model=User
class UserObserver
{
    public function creating(User $user): void
    {
        $user->uuid ??= (string) Str::uuid();
    }

    public function created(User $user): void
    {
        $user->profile()->create(['avatar' => 'default.png']);
        Log::info('User created', ['id' => $user->id, 'email' => $user->email]);
    }

    public function updating(User $user): void
    {
        // Track changes
        if ($user->isDirty('email')) {
            $user->email_verified_at = null;
        }
    }

    public function updated(User $user): void
    {
        if ($user->wasChanged('email')) {
            Mail::to($user)->send(new EmailChangedNotification($user));
        }
    }

    public function deleting(User $user): void
    {
        // Prevent deletion of super admin
        if ($user->isSuperAdmin()) {
            return false; // Cancel the delete
        }
    }

    public function deleted(User $user): void
    {
        Log::info('User deleted', ['id' => $user->id]);
        // Queue cleanup jobs
        CleanupUserData::dispatch($user->id)->onQueue('cleanup');
    }

    public function restored(User $user): void
    {
        Log::info('User restored', ['id' => $user->id]);
    }

    public function forceDeleted(User $user): void
    {
        Log::info('User force deleted', ['id' => $user->id]);
        // Permanently remove external references
        ExternalAPIService::removeUser($user->id);
    }
}

Registering Observers

// AppServiceProvider or EventServiceProvider
class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        User::observe(UserObserver::class);
    }
}

// Or use Laravel 11+ automatic discovery
// Observers in app/Observers/ are auto-registered

Observer Auto-Discovery (Laravel 11+)

Laravel 11 automatically finds observers in app/Observers/ and registers them for their corresponding model. The convention is ModelNameObserver in app/Observers/.

Events vs Observers

AspectEventsObservers
ScopeApplication-wideModel-specific
TriggerExplicit event() callAutomatic Eloquent lifecycle
Use caseBusiness logic consequencesData integrity, cross-cutting model concerns
DependenciesCan inject servicesCan inject services
QueuingVia ShouldQueueVia ShouldQueue (attached to the underlying event)

When to Use Which

Use Observers for:

  • Data integrity — UUID generation, default values, cascading deletes
  • Audit logging — track model changes
  • Automatic relationship management

Use Events for:

  • Business process coordination — "order was placed" triggers email, inventory, analytics
  • Cross-cutting features — notifications, webhooks
  • Workflows that span multiple models

Queued Model Events

Observers and model events can dispatch queued jobs:

class UserObserver
{
    public function created(User $user): void
    {
        // This runs synchronously during the request
        ProcessUserAvatar::dispatch($user)->onQueue('images');
    }
}

Event Discovery

Laravel can auto-discover events and listeners. In EventServiceProvider:

public function shouldDiscoverEvents(): bool
{
    return true;
}

By default, events are discovered by scanning the Listeners directory. Customize:

protected function configureDiscoverEvents(): void
{
    $this->discoverEvents(function () {
        return [
            app_path('Listeners'),
            app_path('Domain/*/Listeners'),
        ];
    });
}

Common Mistakes

Mistake 1: Side Effects in creating vs created

// ❌ Using `creating` for operations that need the model's ID
static::creating(function (User $user) {
    $user->profile()->create([]); // Fails — user has no ID yet
});

// ✅ Use `created` instead
static::created(function (User $user) {
    $user->profile()->create([]); // Works — user has ID
});

Mistake 2: Returning Values from the Wrong Hook

// ✅ `creating` and `saving` can return false to halt
static::creating(function (User $user) {
    if (User::where('email', $user->email)->exists()) {
        return false; // Prevents save
    }
});

// ❌ `created` and `saved` cannot halt — model is already saved
static::created(function (User $user) {
    return false; // No effect — model is already in DB
});

Mistake 3: Cascading Events in a Loop

// ❌ Each iteration fires created/saved events
foreach ($emails as $email) {
    $user = User::create(['email' => $email]);
    // Queue jobs fire N times
}

// ✅ Use `query() builder` to insert without events
User::withoutEvents(function () use ($emails) {
    foreach ($emails as $email) {
        User::create(['email' => $email]);
    }
});
// Then dispatch a single batch event
UserBatchCreated::dispatch(count($emails));

Mistake 4: Heavy Operations in Synchronous Listeners

// ❌ Blocks the response
class SendWelcomeEmail
{
    public function handle(UserRegistered $event): void
    {
        Mail::send(...); // Takes 500ms
        Slack::notify(...); // Takes 200ms
        SyncToCRM::sync($event->user); // Takes 1s
    }
}

// ✅ Queue the listener
class SendWelcomeEmail implements ShouldQueue
{
    public function handle(UserRegistered $event): void
    {
        // All work happens in the queue worker
    }
}

Mistake 5: Deadly Observer-Events Loop

class OrderObserver
{
    public function updated(Order $order): void
    {
        if ($order->wasChanged('status')) {
            OrderStatusChanged::dispatch($order);
        }
    }
}

// Listener updates the order again, triggering the observer again
class OrderStatusChanged
{
    public function handle(OrderStatusChanged $event): void
    {
        $event->order->update(['processed_at' => now()]);
        // This fires OrderObserver::updated again... infinite loop!
    }
}

Fix: Guard against recursion:

class OrderObserver
{
    public function updated(Order $order): void
    {
        if ($order->wasChanged('status') && !$order->wasChanged('processed_at')) {
            OrderStatusChanged::dispatch($order);
        }
    }
}

Best Practices Summary

  1. Use events for business workflows — decouple the trigger from the reaction
  2. Use observers for model integrity — UUIDs, defaults, cascades
  3. Queue slow listeners — any listener doing I/O (email, API calls, file processing)
  4. Use withoutEvents for bulk operations — avoid N events for N records
  5. Guard against infinite loops — check wasChanged before dispatching
  6. Use event subscribers for related events — groups User events, Order events, etc.
  7. Register observers in service providers — not in models' booted for Observers
  8. Use ShouldBeUnique for idempotent listeners — prevent duplicate processing

Interview Questions

  1. What is the difference between an Event and an Observer?
  2. How do you make an event listener run on the queue?
  3. What is the difference between creating and created model events?
  4. How do you prevent an Eloquent model from being saved from an observer?
  5. Explain the withoutEvents method and when you would use it.
  6. How do event subscribers differ from regular listeners?
  7. How can you prevent infinite loops between observers and events?

Related Articles

# Laravel# PHP# Events# Observers# Architecture# Backend
WhatsApp
Chat on WhatsApp