← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Domain Events

architecture PHP 7.0+ Advanced
debt(d7/e7/b5/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). The detection_hints indicate automated detection is 'no' and the code pattern (service directly calling side effects instead of raising domain events) requires a reviewer to understand the intended architecture. PHPStan (the only listed tool) cannot flag the absence of domain event patterns — it would only catch type errors. Missing domain events are silent in normal operation and only discovered during architecture review or when a new side effect requirement reveals the coupling.

e7 Effort Remediation debt — work required to fix once spotted

Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix describes moving event raising into aggregate roots and dispatching after transaction commits — this is not a single-line fix. It requires identifying all state-changing services, refactoring them to raise events from aggregate roots, introducing an event dispatcher, wiring up handlers, and ensuring transactional safety (events dispatched only after commit). This touches multiple layers (domain, application, infrastructure) and multiple files across the codebase.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). Domain events apply across web, cli, and queue-worker contexts. Once adopted (or not adopted), every new side effect added to the system is shaped by whether domain events exist. Without them, every new side effect (Slack notification, analytics, etc.) requires touching existing service code. The burden is significant but not fully architectural — teams can partially adopt or retrofit incrementally without a full rewrite.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception field identifies a well-documented and consequential confusion: developers treat domain events and integration events as the same thing, leading to domain events being published to external message brokers directly. The common_mistakes reinforce additional traps: raising events before the transaction commits (silent data inconsistency), mutable/entity payloads (serialisation failures), present-tense naming (semantic inversion), and missing timestamps (replay/ordering bugs). These contradict intuitions carried from general event-driven patterns elsewhere, warranting t7.

About DEBT scoring →

Also Known As

domain event event publishing raised events

TL;DR

Named, immutable records of something that happened in the domain — 'OrderPlaced', 'PaymentReceived' — enabling decoupled reactions without direct service calls.

Explanation

A domain event captures a past fact: OrderPlaced, UserRegistered, PaymentFailed. They are immutable value objects containing all relevant data at the time of occurrence. The aggregate that raises the event does not know who handles it — handlers subscribe independently. This decouples the raising code from side effects (send email, update analytics, reserve inventory). Domain events are distinct from integration events — domain events are internal, integration events cross service boundaries.

Diagram

sequenceDiagram
    participant CMD as Command
    participant AGG as Aggregate
    participant BUS as Event Bus
    participant H1 as Email Handler
    participant H2 as Analytics Handler
    CMD->>AGG: PlaceOrder(items, customer)
    AGG->>AGG: Validate and update state
    AGG-->>BUS: Publish OrderPlaced event
    BUS-->>H1: OrderPlaced - send confirmation email
    BUS-->>H2: OrderPlaced - track conversion
    Note over AGG,BUS: Aggregate raises event.<br/>Handlers react independently.<br/>No direct coupling.

Common Misconception

Domain events and integration events are the same — domain events are internal to a bounded context; integration events are published to external systems via a message broker.

Why It Matters

Without domain events, adding a new side effect (send Slack notification when an order ships) requires modifying the shipping service — domain events let you add a handler without touching existing code.

Common Mistakes

  • Domain events that contain mutable objects or database entities — they must be serialisable and self-contained.
  • Raising events before the transaction commits — if the transaction rolls back, the events have already fired.
  • Events named in the present tense (OrderPlacing) instead of past tense (OrderPlaced) — events record facts, not intentions.
  • Not recording when the event occurred — timestamp is essential for event ordering and replay.

Code Examples

✗ Vulnerable
// Side effects coupled directly to domain logic:
class Order {
    public function ship(): void {
        $this->status = 'shipped';
        // Direct calls — every new side effect requires modifying this class:
        app(Mailer::class)->sendShippingNotification($this);
        app(Analytics::class)->track('order_shipped', $this->id);
        app(SlackNotifier::class)->notify('Order ' . $this->id . ' shipped');
    }
}
✓ Fixed
// Domain event — decoupled side effects:
class OrderShipped {
    public function __construct(
        public readonly string $orderId,
        public readonly string $trackingNumber,
        public readonly DateTimeImmutable $occurredAt,
    ) {}
}

class Order {
    private array $events = [];
    public function ship(string $trackingNumber): void {
        $this->status = 'shipped';
        $this->events[] = new OrderShipped($this->id, $trackingNumber, new DateTimeImmutable());
    }
    public function releaseEvents(): array {
        $events = $this->events; $this->events = [];
        return $events;
    }
}

Added 15 Mar 2026
Edited 22 Mar 2026
Views 27
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 0 pings S 1 ping M 1 ping T 0 pings W 2 pings T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S
No pings yet today
Amazonbot 7 Perplexity 5 Unknown AI 2 ChatGPT 2 Majestic 1 Google 1 SEMrush 1 Ahrefs 1
crawler 19 crawler_json 1
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Medium
⚡ Quick Fix
Raise domain events from within aggregate roots (not services) — OrderPlaced, PaymentReceived — and dispatch them after the transaction commits to trigger side effects asynchronously
📦 Applies To
PHP 7.0+ web cli queue-worker laravel symfony
🔗 Prerequisites
🔍 Detection Hints
Service class directly calling email/notification/analytics after state change; no domain event raised from aggregate root
Auto-detectable: ✗ No phpstan
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: High Context: File Tests: Update

✓ schema.org compliant