The Problem It Solves

Imagine you have an AuditLog table. Every time a user record changes, you want to save what changed, who changed it, and when.

The naive approach is to write the log call directly inside every controller method:

php
// Bad approach: you repeat this in every controller that touches User
public function update(Request $request, User $user): JsonResponse
{
    $user->update($request->all());

    // You manually log here
    AuditLog::create([...]);
}

public function destroy(User $user): JsonResponse
{
    $user->delete();

    // You manually log here again, hoping you remembered
    AuditLog::create([...]);
}

This gets messy fast. What if one developer forgets to add it? What if a queued job deletes the user in the background? The log gets missed entirely. You are relying on everyone remembering to do the right thing in every place.

The Observer Pattern removes that risk. Instead of calling the log yourself, you tell Laravel: whenever anything happens to this model, anywhere in the codebase, run this automatically.

How It Works

The flow has three moving parts:

  1. The model — a User instance calls save() or delete().
  2. The event — Laravel fires created, updated, or deleted.
  3. The observer — your UserObserver reacts via created(), updated(), or deleted().

Three things are involved: the Subject (the model being watched), the Observer (your listener class), and the Event (the trigger like created or updated). Laravel handles wiring them together. You just define what to do when each event fires.

Laravel Implementation

Step 1 — Generate the Observer

bash
php artisan make:observer UserObserver --model=User

Step 2 — Write the logic

app/Observers/UserObserver.php php
namespace App\Observers;

use App\Models\AuditLog;
use App\Models\User;

class UserObserver
{
    public function created(User $user): void
    {
        $this->log('created', $user);
    }

    public function updated(User $user): void
    {
        // getChanges() returns only fields that were actually saved to the DB
        $this->log('updated', $user, $user->getChanges());
    }

    public function deleted(User $user): void
    {
        $this->log('deleted', $user);
    }

    private function log(string $action, User $user, array $changes = []): void
    {
        AuditLog::create([
            'model'      => 'User',
            'model_id'   => $user->id,
            'action'     => $action,
            'old_values' => json_encode($user->getOriginal()),
            'new_values' => json_encode($changes),
            'changed_by' => auth()->id(),
            'changed_at' => now(),
        ]);
    }
}

Step 3 — Register it once

app/Providers/AppServiceProvider.php php
public function boot(): void
{
    User::observe(UserObserver::class);
}

That is the entire setup. Now forget about it. Every create, update, or delete on the User model, whether it comes from a controller, a queued job, an artisan command, or an API endpoint written six months from now, is logged automatically.

Is This Industry Standard?

Yes, fully. Audit logging via model observers is one of the most common patterns in production Laravel applications. Every major framework has an equivalent: Symfony uses Doctrine Event Listeners, Django has Signals, Rails has ActiveRecord Callbacks, and Spring uses Application Events. The concept is universal.

It maps directly to the Single Responsibility Principle: your controller is responsible for handling the request, and your observer is responsible for reacting to model changes. Neither one knows about the other, which is exactly how it should be.

Observer vs MySQL Triggers

A MySQL Trigger can also log changes at the database level. Here is when each one actually makes sense:

Capability Laravel Observer MySQL Trigger
Know who made the change Yes via auth()->id() No — no app context
Send emails or queue jobs Yes No
Unit testable Yes Hard
Visible in codebase Yes — version controlled No — hidden in DB
Works if Eloquent is bypassed No — raw SQL skips it Yes
Works across multiple apps No Yes
Performance overhead Slight PHP overhead Faster (in-DB)

Summary

The Observer Pattern is not a trick or an over-engineered solution. It is the correct tool for the job when you need to react consistently to model changes across your entire application. You write the logic once, register it once, and it works everywhere forever.

For audit logging specifically: use a Laravel Observer. It knows who made the change, lives in your codebase, is easy to test, and follows the same pattern every serious Laravel project uses. MySQL Triggers are only the right call in multi-system architectures where you cannot rely on Eloquent being in the picture.

Work with me

Need help with your Laravel architecture?

I build clean, maintainable Laravel backends for SaaS products — REST APIs, queue systems and production patterns like the one in this article.