Use when defining domain events, setting up the event bus contract, adding event support to entities, implementing the event bus adapter, or understanding the event publishing flow.
Domain events decouple side effects from domain logic. Entities register events during domain methods; use cases publish them after persistence. The Domain layer depends only on contracts (Event, EventBus); the Infrastructure layer provides the Laravel adapter.
declare(strict_types=1);
namespace App\Shared\Domain\Contracts\ServiceBus;
interface Event {}
A pure marker interface — no methods. All domain events implement this contract.
declare(strict_types=1);
namespace App\Shared\Domain\Contracts\ServiceBus;
interface EventBus
{
public function publish(Event ...$events): void;
}
Single method accepting variadic Event arguments. The Domain layer depends on this contract only.
Placement: app/Shared/Domain/Contracts/ServiceBus/
The trait that gives entities event registration and publishing capabilities:
declare(strict_types=1);
namespace App\Shared\Domain;
use App\Shared\Domain\Contracts\ServiceBus\Event;
use App\Shared\Domain\Contracts\ServiceBus\EventBus;
trait HasDomainEvents
{
/**
* @var Event[]
*/
private array $domainEvents = [];
final public function publishDomainEvents(EventBus $eventBus): void
{
$eventBus->publish(...$this->domainEvents);
$this->domainEvents = [];
}
final protected function registerDomainEvent(Event $domainEvent): void
{
$this->domainEvents[] = $domainEvent;
}
}
Key design decisions:
registerDomainEvent() is final protected — only the entity itself can register eventspublishDomainEvents() is final public — use cases call this after persistence$eventBus->publish(...$this->domainEvents)Placement: app/Shared/Domain/HasDomainEvents.php
The standard pattern — event carries the full entity for listeners to access:
declare(strict_types=1);
namespace App\CustomerRelationshipManagement\Contacts\Domain\Events;
use App\CustomerRelationshipManagement\Contacts\Domain\Contact;
use App\Shared\Domain\Contracts\ServiceBus\Event;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Queue\SerializesModels;
final class ContactCreated implements Event
{
use InteractsWithSockets;
use SerializesModels;
public function __construct(public Contact $contact) {}
}
Used for deletion events — the entity is about to be destroyed, so pass only the ID:
declare(strict_types=1);
namespace App\CustomerRelationshipManagement\Contacts\Domain\Events;
use App\Shared\Domain\Contracts\ServiceBus\Event;
use Illuminate\Broadcasting\InteractsWithSockets;
final class ContactDeleted implements Event
{
use InteractsWithSockets;
/**
* @param string $contact The deleted contact uuid
*/
public function __construct(public string $contact) {}
}
Rules:
final class — events are immutable factsimplements Event (the Shared Kernel marker interface)SerializesModels — enables queue serialization of Eloquent modelsInteractsWithSockets only (no SerializesModels — there's no model to serialize){Entity}{PastTenseVerb} — e.g., ContactCreated, VisaActivated, UserDeleted, DocumentUploaded{Module}/Domain/Events/| Event Type | Payload | Traits |
|---|---|---|
| Created | Full entity | InteractsWithSockets + SerializesModels |
| Updated / State change | Full entity | InteractsWithSockets + SerializesModels |
| Deleted | Scalar ID (string) | InteractsWithSockets only |
Scalar ID variation also works for events where the listener only needs the identifier (e.g., AccountInvitationAccepted carrying string $invitationId).
Events are registered during domain methods — never published directly by entities.
static new() — Creationpublic static function new(array $attributes = []): self
{
$contact = new self($attributes);
$contact->registerDomainEvent(new ContactCreated($contact));
return $contact;
}
toBeDeleted() — Deletionpublic function toBeDeleted(): self
{
$this->registerDomainEvent(new ContactDeleted($this->id));
return $this;
}
public function updateName(string $first, string $last): self
{
$this->forceFill([
'first_name' => $first,
'last_name' => $last,
]);
$this->registerDomainEvent(new ContactNameUpdated($this));
return $this;
}
Register events only when state actually changes:
public function updateEmail(string $email): self
{
if ($email !== $this->email) {
$this->forceFill(['email' => $email]);
$this->registerDomainEvent(new UserEmailUpdated($this));
}
return $this;
}
See the implementing-domain-entities skill for the full entity patterns.
Use cases publish events after persistence — this ensures events reflect committed state:
declare(strict_types=1);
namespace App\CustomerRelationshipManagement\Contacts\Application;
use App\CustomerRelationshipManagement\Contacts\Domain\Contact;
use App\CustomerRelationshipManagement\Contacts\Domain\Contracts\ContactRepository;
use App\Shared\Domain\Contracts\ServiceBus\EventBus;
final readonly class CreateContact
{
public function __construct(
private ContactRepository $repository,
private EventBus $eventBus,
) {}
public function __invoke(array $attributes): Contact
{
$contact = $this->repository->save(Contact::new($attributes));
$contact->publishDomainEvents($this->eventBus);
return $contact;
}
}
The publishing flow:
registerDomainEvent())$repository->save())$entity->publishDomainEvents($this->eventBus))IlluminateEventBus dispatches events through Laravel's event() helperimplementing-event-handlers)Rules:
$repository->save() — never beforeEventBus via constructorpublishDomainEvents() clears the event queue after dispatching$repository->delete() (the entity still exists at that point)The Infrastructure adapter that bridges domain events to Laravel's event system:
declare(strict_types=1);
namespace App\Shared\Infrastructure\ServiceBus;
use App\Shared\Domain\Contracts\ServiceBus\Event;
use App\Shared\Domain\Contracts\ServiceBus\EventBus;
class IlluminateEventBus implements EventBus
{
public function publish(Event ...$events): void
{
if (empty($events)) {
return;
}
event(...$events);
}
}
Key points:
event() helperEvent + EventBus contracts only. IlluminateEventBus is Infrastructure. This keeps the Domain pureSharedServiceProvider via $bindings array (see configuring-bounded-contexts):public array $bindings = [
EventBus::class => IlluminateEventBus::class,
];
Placement: app/Shared/Infrastructure/ServiceBus/IlluminateEventBus.php
Entity Use Case Infrastructure
────── ──────── ──────────────
registerDomainEvent() → $repository->save() → Database write
publishDomainEvents() → IlluminateEventBus::publish()
→ Laravel event() helper
→ Registered listeners
→ domainEvents[] cleared
| Mistake | Fix |
|---|---|
| Publishing events before persistence | Always call publishDomainEvents() AFTER $repository->save() |
Entity calling publishDomainEvents() on itself | Publishing is the use case's responsibility, not the entity's |
| Passing full entity in deletion events | Pass $this->id (string) — the entity is about to be deleted |
Missing SerializesModels on entity-carrying events | Required for queue serialization of Eloquent models |
Adding SerializesModels to deletion events | Deletion events carry scalar IDs, not models — omit it |
Importing IlluminateEventBus in Domain layer | Domain depends on EventBus contract only |
| Registering events in Infrastructure code | Events are registered in Domain methods and published by Application use cases |
Event marker interface exists in Shared/Domain/Contracts/ServiceBus/?EventBus contract defines publish(Event ...$events): void?HasDomainEvents trait in Shared/Domain/ with registerDomainEvent() + publishDomainEvents()?final class implementing Event?SerializesModels?{Entity}{PastTenseVerb}?{Module}/Domain/Events/?IlluminateEventBus adapter bound in SharedServiceProvider?This skill uses Laravel-specific features. Consult the official Laravel documentation (via Context7 or web search) for details on:
event() helper — dispatching events through Laravel's event systemIlluminate\Queue\SerializesModels — queue-safe serialization of Eloquent models in eventsIlluminate\Broadcasting\InteractsWithSockets — WebSocket broadcasting support for eventsEvent::listen() — registering event listeners in service providersShouldQueue — marking event listeners for async processing