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