Laravel Design Patterns

Observer Pattern in Laravel: Audit Logging Done Right

Quick Answer

The Observer Pattern lets you watch a model for changes and react automatically without touching controllers or services. In Laravel, you create an Observer class, define what happens on created, updated, or deleted, and register it once. From that point on, it fires everywhere automatically.

For audit logging, this is the correct and industry-standard approach. It is cleaner than MySQL Triggers for single-app projects because it knows who made the change, can queue follow-up jobs, and lives in your version-controlled codebase.

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

Observer Pattern Flow
User model
save() / delete()
Laravel fires event
created / updated / deleted
UserObserver::created()
UserObserver::updated()
UserObserver::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

PHP — app/Observers/UserObserver.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

PHP — app/Providers/AppServiceProvider.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.

Pro tip: Use getChanges() on the updated event, not getDirty(). The difference is that getChanges() only returns fields that were actually saved to the database, while getDirty() returns fields that were changed in memory but may not have been persisted yet.

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)

Rule of thumb: Use a Laravel Observer when your app is the only thing writing to the database, which covers most Laravel projects. Use a MySQL Trigger when multiple systems write to the same table and you need logging regardless of source.

Watch out: Observer methods do not fire when you use mass-update or mass-delete queries like User::where(...)->update([...]) or User::where(...)->delete(). These bypass Eloquent entirely. Always use model instances if you need observers to fire.

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.

Need Help with Your Laravel Architecture?

I build clean, maintainable Laravel backends for SaaS products including REST APIs, queue systems, and production-ready patterns like this one.

Based in Bangladesh · Remote worldwide

Kamruzzaman Polash

Software Engineer focused on Laravel, REST APIs, and scalable backend systems. 10+ projects delivered for clients worldwide.