Overview
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.
Shared Kernel Contracts
Event Marker Interface
declare(strict_types=1);
namespace App\Shared\Domain\Contracts\ServiceBus;
interface Event {}
A pure marker interface — no methods. All domain events implement this contract.
EventBus 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/
HasDomainEvents Trait
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()isfinal protected— only the entity itself can register eventspublishDomainEvents()isfinal public— use cases call this after persistence- Events are cleared after publishing — prevents double-publishing
- Uses variadic unpacking:
$eventBus->publish(...$this->domainEvents)
Placement: app/Shared/Domain/HasDomainEvents.php
Domain Event Classes
Event with Entity Payload
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) {}
}
Event with Scalar ID Payload
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:
- Always
final class— events are immutable facts - Always
implements Event(the Shared Kernel marker interface) - Constructor uses promoted public properties for the payload
- Entity-carrying events use
SerializesModels— enables queue serialization of Eloquent models - Deletion events use
InteractsWithSocketsonly (noSerializesModels— there's no model to serialize) - Naming:
{Entity}{PastTenseVerb}— e.g.,ContactCreated,VisaActivated,UserDeleted,DocumentUploaded - Placement:
{Module}/Domain/Events/
Payload Guidelines
| 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).
Event Registration on Entities
Events are registered during domain methods — never published directly by entities.
In static new() — Creation
public static function new(array $attributes = []): self
{
$contact = new self($attributes);
$contact->registerDomainEvent(new ContactCreated($contact));
return $contact;
}
In toBeDeleted() — Deletion
public function toBeDeleted(): self
{
$this->registerDomainEvent(new ContactDeleted($this->id));
return $this;
}
In Domain Methods — Updates and State Transitions
public function updateName(string $first, string $last): self
{
$this->forceFill([
'first_name' => $first,
'last_name' => $last,
]);
$this->registerDomainEvent(new ContactNameUpdated($this));
return $this;
}
Conditional Registration
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.
Event Publishing in Use Cases
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:
- Entity registers events during domain methods (
registerDomainEvent()) - Use case persists the entity via repository (
$repository->save()) - Use case publishes events (
$entity->publishDomainEvents($this->eventBus)) IlluminateEventBusdispatches events through Laravel'sevent()helper- Registered listeners handle side effects (see
implementing-event-handlers)
Rules:
- Always publish after
$repository->save()— never before - Use cases inject
EventBusvia constructor - The entity's
publishDomainEvents()clears the event queue after dispatching - For deletion: publish before calling
$repository->delete()(the entity still exists at that point)
IlluminateEventBus Adapter
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:
- Thin adapter — delegates to Laravel's
event()helper - Guards against empty event arrays
- Framework coupling boundary: The Domain depends on
Event+EventBuscontracts only.IlluminateEventBusis Infrastructure. This keeps the Domain pure - Bound in
SharedServiceProvidervia$bindingsarray (seeconfiguring-bounded-contexts):
public array $bindings = [
EventBus::class => IlluminateEventBus::class,
];
Placement: app/Shared/Infrastructure/ServiceBus/IlluminateEventBus.php
Complete Event Flow
Entity Use Case Infrastructure
────── ──────── ──────────────
registerDomainEvent() → $repository->save() → Database write
publishDomainEvents() → IlluminateEventBus::publish()
→ Laravel event() helper
→ Registered listeners
→ domainEvents[] cleared
Common Mistakes
| 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 |
Pre-flight Checklist
-
Eventmarker interface exists inShared/Domain/Contracts/ServiceBus/? -
EventBuscontract definespublish(Event ...$events): void? -
HasDomainEventstrait inShared/Domain/withregisterDomainEvent()+publishDomainEvents()? - Domain events are
final classimplementingEvent? - Entity-carrying events use
SerializesModels? - Deletion events carry scalar ID, not entity?
- Events named as
{Entity}{PastTenseVerb}? - Events placed in
{Module}/Domain/Events/? - Use cases publish events AFTER persistence?
-
IlluminateEventBusadapter bound inSharedServiceProvider?
Laravel Reference
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