Event Guidelines

Event Guidelines

Domain Events

  • Emitted from aggregate methods when state changes
  • Immutable value objects
  • JSON-serializable (no objects, no closures)
  • Always include a version field (integer, starting at 1)
  • Named past tense: MemberCreated, GroupMemberJoined

Event Structure

final readonly class MemberCreated implements DomainEventInterface
{
    public function __construct(
        public string $memberId,
        public string $tenantId,
        public string $firstName,
        public string $lastName,
        public string $email,
        public \DateTimeImmutable $occurredAt,
        public int $version = 1,
    ) {}
}

Naming Convention

  • Class: {Entity}{Action} in PascalCase (MemberCreated, GroupMemberJoined)
  • Topic: {module}.{entity}.{action} in dot notation (people.member.created)

Versioning

  • Start at version 1
  • Bump version when the payload structure changes
  • Consumers must handle all known versions
  • Never remove fields — only add new ones (backward compatible)
  • Document version changes in EVENT_CATALOG.md

Dispatch

  • Events are collected by the aggregate during method execution
  • Application layer (command handler) dispatches events after persisting
  • Use Symfony Messenger for async dispatch via Redis transport
  • Synchronous dispatch only for critical same-transaction needs

Consumption

  • Event handlers are in the consuming module’s Application layer
  • Handlers must be idempotent (same event processed twice = same result)
  • Use message stamps for deduplication
  • Failed events go to the failure transport for retry

Catalog

  • Every event must be registered in .ai/context/EVENT_CATALOG.md
  • Include: event name, payload schema, emitter, known consumers, version