Events as domain signals — why use events vs direct calls
Concept
Events as domain signals — the core concept that transforms events from a technical pattern into an architectural tool for modeling real-world business processes.
An event represents something that happened in the past. OrderPlaced, not PlaceOrder. UserRegistered, not RegisterUser. The past tense matters — events are facts, immutable records of what occurred.
Why events decouple code: Without events, a controller that registers a user must directly call every service that cares about registration:
$user = User::create($data);
$mailer->sendWelcomeEmail($user); // knows about mailer
$crm->syncUser($user); // knows about CRM
$analytics->trackRegistration($user); // knows about analyticsThe controller now depends on 3+ external systems. Adding a new integration means modifying the controller.
With events, the controller only knows one thing — that a user registered:
$user = User::create($data);
event(new UserRegistered($user)); // controller's job is doneNew integrations add a listener without touching the controller. Removing an integration deletes a listener. The controller is closed for modification, open for extension — the Open/Closed Principle.
Events vs direct calls trade-off: Events add indirection. Debugging an event dispatch requires finding all listeners. For simple sequential operations (validate → save → respond), direct calls are clearer. Use events when: the triggering code shouldn't know about reactions, reactions may grow over time, or reactions should be async.
Code Example
<?php
// WITHOUT events — tight coupling
class UserController extends Controller
{
public function __construct(
private \App\Services\Mailer $mailer,
private \App\Services\CrmService $crm,
private \App\Services\Analytics $analytics,
private \App\Services\SlackNotifier $slack,
) {}
public function store(Request $request)
{
$user = User::create($request->validated());
$this->mailer->sendWelcomeEmail($user); // controller knows about email
$this->crm->syncNewUser($user); // controller knows about CRM
$this->analytics->track('user_registered', $user); // controller knows about analytics
$this->slack->notify("New user: {$user->email}"); // controller knows about Slack
return response()->json($user, 201);
}
// Adding Hubspot integration requires MODIFYING this controller
}
// WITH events — controller is decoupled
class UserController extends Controller
{
public function store(Request $request)
{
$user = User::create($request->validated());
event(new \App\Events\UserRegistered($user)); // controller's only responsibility
return response()->json($user, 201);
// Adding Hubspot = add new Listener class, controller unchanged
}
}
// The event — a simple data carrier
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
use Dispatchable, SerializesModels;
public function __construct(public readonly \App\Models\User $user) {}
}
// Listeners register independently
// WelcomeEmailListener, CrmSyncListener, AnalyticsListener, SlackNotifierListener
// Each can be: sync or async, retryable, independently tested