0

Events as domain signals — why use events vs direct calls

Intermediate5 min read·lv-18-001
interviewsolid

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:

php
$user = User::create($data);
$mailer->sendWelcomeEmail($user);       // knows about mailer
$crm->syncUser($user);                  // knows about CRM
$analytics->trackRegistration($user);  // knows about analytics

The 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:

php
$user = User::create($data);
event(new UserRegistered($user)); // controller's job is done

New 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
<?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