Laravel Observers Explained: From Clean CRUD Hooks to Production-Grade Event Architecture
What Is an Observer in Laravel?
In Laravel, an Observer is a class that listens to Eloquent model lifecycle events and encapsulates side effects away from controllers and models.
Instead of this:
public function store(Request $request)
{
$user = User::create($request->all());
Mail::to($user->email)->send(new WelcomeMail($user));
}You move the side effect into an observer tied to the model.
Eloquent Model Events (The Trigger Points)
Laravel fires these events automatically:
retrievedcreating/createdupdating/updatedsaving/saveddeleting/deletedrestoring/restoredforceDeleted
Rule of thumb:
Use
creating/updatingto mutate attributes.Use
created/updatedfor side effects (notifications, logs, jobs).
Basic Implementation
1. Create Observer
php artisan make:observer UserObserver --model=UserThis generates:
class UserObserver
{
public function created(User $user)
{
//
}
}2. Register the Observer
In AppServiceProvider:
use App\Models\User;
use App\Observers\UserObserver;
public function boot()
{
User::observe(UserObserver::class);
}3. Real-World Example: Auto-Generate Slug
public function creating(Post $post)
{
$post->slug = Str::slug($post->title);
}Why creating?
Because it runs before insert, allowing attribute mutation.
Practical Use Cases (That Actually Matter)
1. Audit Logging (Production-Grade SaaS)
public function updated(Order $order)
{
ActivityLog::create([
'model' => Order::class,
'model_id' => $order->id,
'changes' => json_encode($order->getChanges()),
]);
}Use case:
Financial platforms
Compliance-heavy SaaS
Enterprise dashboards
2. Dispatch Jobs Instead of Doing Heavy Work
Never send emails directly in observers.
Bad:
Mail::to($user)->send(new WelcomeMail($user));Correct:
public function created(User $user)
{
SendWelcomeEmail::dispatch($user);
}This keeps request lifecycle fast and scalable.
3. Enforcing Business Rules
Prevent deletion if dependent data exists:
public function deleting(Project $project)
{
if ($project->tasks()->exists()) {
throw new \Exception("Cannot delete project with tasks.");
}
}Now the rule is centralized, not scattered across controllers.
Advanced Patterns
1. Observer + Domain Events (Clean Architecture)
Instead of putting logic directly in the observer:
public function created(User $user)
{
event(new UserRegistered($user));
}This keeps observers thin and moves business logic into listeners.
2. Prevent Infinite Loops
If you update a model inside updated, you can trigger recursion.
Bad:
public function updated(User $user)
{
$user->update(['synced' => true]);
}Fix:
$user->updateQuietly(['synced' => true]);Or:
User::withoutEvents(function () use ($user) {
$user->update(['synced' => true]);
});3. Handling Soft Deletes
If using SoftDeletes:
deletingrunsdeletedrunsforceDeletedruns only on permanent delete
Design logic accordingly.
4. Queue Observers Globally (High-Scale Apps)
Instead of dispatching jobs manually:
class UserObserver implements ShouldQueueNow observer logic is queued automatically.
This is powerful in:
High-traffic SaaS
Marketplace platforms
Payment systems
5. Observers in Multi-Tenant Apps
Inject tenant context before persistence:
public function creating(Model $model)
{
$model->tenant_id = tenant()->id;
}This guarantees tenant isolation at persistence level.
Performance Considerations
Observers run automatically on every model lifecycle event.
Be careful when:
Bulk importing data
Running seeders
Running migrations
Executing batch updates
Disable when needed:
Model::withoutEvents(function () {
User::insert($bulkData);
});Observers vs Model Events vs Global Scopes
| Feature | Use Case |
|---|---|
| Observer | Side effects, automation |
| Model Event Closure | Quick inline logic |
| Global Scope | Query filtering |
| Middleware | HTTP-level rules |
Do not use observers for:
Authorization
Validation
HTTP-level decisions
Anti-Patterns (Avoid These)
Business logic explosion inside observers
Sending emails synchronously
Hidden side effects developers cannot trace
Mutating unrelated models silently
Observers should be:
Deterministic
Lightweight
Transparent
Real SaaS Example Architecture
Imagine a subscription platform:
When Subscription is created:
Observer:
public function created(Subscription $subscription)
{
SyncWithStripe::dispatch($subscription);
GenerateInvoice::dispatch($subscription);
NotifyAdmin::dispatch($subscription);
}Now:
Controllers stay clean
Business rules centralized
Side effects scalable
Testing Observers Properly
Use:
Event::fake();
Queue::fake();Or test observer behavior directly:
$user = User::factory()->create();
$this->assertDatabaseHas('activity_logs', [
'model_id' => $user->id,
]);When NOT to Use Observers
Do not use them when:
Logic depends on HTTP request context
You need explicit control over execution order
The logic is domain-specific and not model-lifecycle specific
In complex systems, domain events may be cleaner.
Strategic Guidance for Founders & CTOs
Observers are valuable when:
You want clean controllers
You want automatic system behaviors
You need centralized persistence rules
But in large-scale SaaS:
Combine Observers with Domain Events
Avoid heavy synchronous processing
Document side effects clearly
Final Takeaway
Observers are not just “nice Laravel features.”
They are persistence-layer automation tools.
Used properly:
They reduce duplication
They centralize rules
They enforce data integrity
Used poorly:
They create hidden chaos
Architect them deliberately.
