Author: Mohammad Shakhtour

Status: Accepted

Context

The Problem

In ADR-001, we established Domain Events as the primary pattern for cross-aggregate coordination. However, not all events have the same consistency and delivery requirements. Consider two scenarios in our CMMS system:

Scenario 1: Work Order Creation

  1. User creates a work order for an asset
  2. Asset status must immediately change to "Under Maintenance"
  3. This status change must happen in the same transaction
  4. If the work order creation fails, the asset status must not change

Scenario 2: Work Order Completion Email

  1. Work order is completed successfully
  2. System sends notification email to stakeholders
  3. Email delivery can happen asynchronously
  4. Email failure should not rollback the work order completion

The Challenge/Need: How do we handle these fundamentally different event processing requirements while maintaining clean architecture and preparing for future distributed system evolution?

Why This Matters

The pattern extends to event processing strategies:

---

Decision Drivers

  1. Transaction Boundaries - Some operations require ACID guarantees, others don't
  2. Consistency Requirements - Critical state changes need strong consistency, notifications can be eventual
  3. System Resilience - Non-critical operations shouldn't block or fail critical transactions
  4. Performance - Asynchronous processing for long-running operations (emails, external APIs)
  5. Microservices Readiness - Clear separation enables future distributed architecture
  6. Failure Isolation - External system failures shouldn't affect core business operations

---

Options Considered

Option 1: Single Event Type (All Synchronous)

Pattern:

All events execute synchronously within the transaction. Handlers modify state, send emails, call external APIs - all in one transaction.

Pros:

Cons:

Verdict: Rejected - External failures affect core business operations

---

Option 2: Single Event Type (All Asynchronous)

Pattern:

All events execute asynchronously outside the transaction. Even critical state changes happen via eventual consistency.

Pros:

Cons:

Verdict: Rejected - Cannot guarantee critical business invariants

---

Option 3: Two Distinct Event Handler Types (Chosen)

Pattern:

Separate interfaces for different consistency requirements:

Decision Criteria:

Pros:

Cons:

Verdict: Accepted - Balances consistency, performance, and future evolution

---

Option 4: Attribute-Based Distinction

Pattern:

Single handler interface with attributes marking execution strategy:

[Transactional]
public class AssetStatusEventHandler : IEventHandler<WorkOrderCreated> { }

[Asynchronous]
public class EmailEventHandler : IEventHandler<WorkOrderCompleted> { }

Pros:

Cons:

Verdict: Rejected

---

Decision

Implement two distinct event handler interfaces:

IDomainEventHandler<TEvent>

Purpose: Handle events that require strong consistency and immediate state changes within the same transaction.

Characteristics:

Use Cases:

Example:

// WorkOrder created → Asset must be set to "Under Maintenance"
public class WorkOrderCreatedEventHandler : IDomainEventHandler<WorkOrderCreatedEvent>
{
    public async Task Handle(WorkOrderCreatedEvent @event, CancellationToken ct)
    {
        var asset = await _assetRepository.GetByIdAsync(@event.AssetId, ct);
        asset.SetUnderMaintenance();
        // Saved in same transaction
        //if any domain exceptions , it will be auto rollback
    }
}

IIntegrationEventHandler<TEvent>

Purpose: Handle events that can execute asynchronously with eventual consistency, typically for cross-boundary communication.

Characteristics:

Use Cases:

Example:

// WorkOrder completed → Send notification email
public class EmailWorkOrderCompletedHandler : IIntegrationEventHandler<WorkOrderCompletedEvent>
{
    public async Task Handle(WorkOrderCompletedEvent @event, CancellationToken ct)
    {
        await _emailService.SendCompletionNotification(@event.WorkOrderId);
        // Email failure won't rollback work order completion
    }
}

---

Key Design Points

Transactional Consistency:

Guaranteed Delivery:

Failure Isolation:

---

Future Evolution

Default Implementation (Single-Process Setup)

Domain Events:

Integration Events:

Microservices Deployment

Domain Events:

Integration Events:

No Business Code Changes Required: Handler interfaces remain identical—only infrastructure configuration changes. When migrating to fully distributed async architectures, compensation events or Saga may be required for distributed transaction handling.

---

Guidelines

When to Use IDomainEventHandler

Use when the event handler:

When to Use IIntegrationEventHandler

Use when the event handler:

Decision Flowchart

Does the handler modify aggregate state?
├─ Yes → Does it require immediate consistency?
│         ├─ Yes → IDomainEventHandler
│         └─ No  → IIntegrationEventHandler
│
└─ No  → Is it a notification or external call?
          └─ Yes → IIntegrationEventHandler

---

Notes

This decision reflects the reality that different operations have different consistency requirements. By making this explicit at the type system level, we provide clear guidance to developers and enable the system to evolve naturally toward a distributed architecture.

---