Back to list
dairectiv

domain-event

by dairectiv

0🍴 0📅 Jan 3, 2026

SKILL.md


name: domain-event description: Guide for implementing Domain Events following DDD principles. Use when adding events to aggregates or creating event listeners. allowed-tools: Read, Write, Edit, Glob, Grep

Domain Event Implementation Guide

This Skill provides patterns for implementing Domain Events in a DDD context.

When to Use

  • Recording significant state changes in aggregates
  • Enabling cross-context communication
  • Triggering side effects (notifications, sync, audit)
  • Creating event listeners

Core Concepts

What is a Domain Event?

A Domain Event captures something that happened in business terms:

  • DirectiveDrafted - A directive was created as a draft
  • DirectivePublished - A directive was published
  • DirectiveArchived - A directive was archived

Events vs Commands

EventsCommands
Past tense (DirectivePublished)Imperative (PublishDirective)
Describe what happenedRequest an action
ImmutableCan be validated/rejected
Multiple listenersSingle handler

Directory Structure

src/{BoundedContext}/Domain/Object/{Aggregate}/Event/
├── {Aggregate}Created.php
├── {Aggregate}Updated.php
└── {Aggregate}Archived.php

src/{BoundedContext}/UserInterface/Listener/
└── {Aggregate}PublishedListener.php

Event Implementation

namespace Dairectiv\{Context}\Domain\Object\{Aggregate}\Event;

use Dairectiv\SharedKernel\Domain\Object\Event\DomainEvent;

final readonly class {Aggregate}Created implements DomainEvent
{
    public function __construct(
        public {Aggregate}Id $aggregateId,
        // Only include immutable identity/data needed by listeners
    ) {}
}

Key rules:

  • Always final readonly
  • Implement DomainEvent interface
  • Include only aggregate identity and essential data
  • Use past tense naming

Recording Events in Aggregates

Aggregates extend AggregateRoot and use recordEvent():

abstract class AggregateRoot
{
    protected function recordEvent(DomainEvent $event): void
    {
        DomainEventQueue::recordEvent($event);
    }
}

Usage in aggregate:

public static function draft(DirectiveId $id, string $name): static
{
    $entity = new self();
    $entity->id = $id;
    $entity->name = $name;

    $entity->recordEvent(new DirectiveDrafted($entity->id));

    return $entity;
}

public function publish(): void
{
    Assert::eq($this->state, DirectiveState::Draft, 'Only drafts can be published.');

    $this->state = DirectiveState::Published;
    $this->recordEvent(new DirectivePublished($this->id));
}

Event Listeners

Listeners live in UserInterface/ layer (like controllers, they are entry points to the application layer).

namespace Dairectiv\{Context}\UserInterface\Listener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(DirectivePublished::class)]
final readonly class DirectivePublishedListener
{
    public function __construct(
        private CommandBus $commandBus,
    ) {}

    public function __invoke(DirectivePublished $event): void
    {
        // NO LOGIC HERE - only dispatch commands
        $this->commandBus->execute(new NotifySubscribersCommand($event->directiveId));
        $this->commandBus->execute(new UpdateSearchIndexCommand($event->directiveId));
    }
}

Naming Convention

Listeners must be suffixed with Listener:

  • DirectivePublishedListener
  • RuleArchivedListener
  • WorkflowCreatedListener

Critical Rules for Listeners

  1. No business logic - Listeners are entry points, not orchestrators
  2. Only dispatch commands - Via CommandBus
  3. One listener per event - Or use multiple #[AsEventListener] attributes
  4. Fail gracefully - Don't break the main flow
  5. Suffix with Listener - Always use {Event}Listener naming

Event Dispatch Flow

Aggregate::publish()
    └─→ recordEvent(DirectivePublished)
            └─→ DomainEventQueue::recordEvent()
                    └─→ [After transaction commits via Middleware]
                            └─→ Symfony Messenger dispatches event
                                    └─→ DirectivePublishedListener::__invoke()
                                            └─→ CommandBus::execute(...)

Testing Events

In Unit Tests (Aggregate)

public function testItShouldPublishDirective(): void
{
    $directive = Directive::draft($id, 'Name', 'Desc');
    $this->resetDomainEvents();

    $directive->publish();

    $this->assertDomainEventRecorded(DirectivePublished::class);
}

In Integration Tests (Listener)

public function testItShouldDispatchNotificationOnPublish(): void
{
    $directive = $this->createDirective();

    $this->execute(new PublishDirectiveCommand($directive->id));

    self::assertDomainEventHasBeenDispatched(DirectivePublished::class);
}

Checklist

When creating a Domain Event:

  • Past tense naming ({Aggregate}{Action}ed)
  • Implements DomainEvent interface
  • final readonly class
  • Contains only aggregate identity + essential data
  • Recorded in aggregate via recordEvent()

When creating a Listener:

  • Lives in UserInterface/Listener/
  • Named {Event}Listener (always suffixed with Listener)
  • Uses #[AsEventListener({Event}::class)]
  • Injects CommandBus in constructor
  • Contains NO business logic
  • Only dispatches commands

Reference Files

  • api/src/SharedKernel/Domain/Object/Event/DomainEvent.php
  • api/src/SharedKernel/Domain/Object/Event/DomainEventQueue.php
  • api/src/SharedKernel/Domain/Object/AggregateRoot.php
  • api/src/Authoring/Domain/Object/Directive/Event/DirectiveDrafted.php

Score

Total Score

50/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon