Consistency Patterns: Domain vs. Integration Events
In high-scale distributed systems, the distinction between internal state transitions and external data contracts is the primary defense against architectural decay. Mismanaging the boundary between **Domain Events** and **Integration Events** leads to "distributed big balls of mud" and catastrophic consistency failures.
1. Tactical Implementation: Domain Events (DE)
Domain Events capture occurrences within a single **Bounded Context**. They facilitate side effects within the same aggregate or across multiple aggregates in the same transactional boundary.
1.1 The In-Memory Dispatcher
DEs are typically handled synchronously or asynchronously within the same process. Using an in-memory bus (e.g., MediatR in .NET, Spring Events in Java), the domain model remains decoupled from the side effects (e.g., updating a read model, triggering a secondary aggregate).
**Key Constraint:** DEs should be emitted *before* the transaction commits to allow side-effect handlers to participate in the same ACID transaction if required, though DDD purists often advocate for eventual consistency even within a context.
2. Strategic Contract: Integration Events (IE)
Integration Events are the public API of a service. They represent committed facts that external services must consume. Unlike DEs, which often contain rich domain objects, IEs must be lean, versioned, and stable.
2.1 Schema Stability and Versions
* **IE Payload:** Should contain the event type, version, timestamp, and the minimal state required for downstream processing (usually just IDs and changed fields).
* **Contract:** Use Avro or Protobuf with a Schema Registry to enforce backward compatibility.
3. Solving the Atomicity Problem: The Transactional Outbox Pattern
The most critical failure mode in event-driven systems is the "dual write" problem: updating the database and sending a message to a broker (Kafka/RabbitMQ) are not atomic.
3.1 The Pattern Mechanics
To achieve atomicity without 2PC (Two-Phase Commit), use the **Transactional Outbox**:
1. **Atomicity:** Within the business transaction, insert the event payload into a dedicated `OUTBOX` table in the same database.
2. **Relay:** A separate process (the "Message Relay" or "Change Data Capture" agent) polls the `OUTBOX` table or tails the database transaction log.
3. **Dispatch:** The relay publishes the message to the broker and marks the outbox entry as dispatched.
3.2 Exactly-Once Semantics
Strict "Exactly-Once" is a theoretical ideal; in practice, we achieve it via **At-Least-Once Delivery + Idempotent Consumption**.
1. **Producer (At-Least-Once):** The Outbox Relay ensures the message reaches the broker. If the broker ACK fails, it retries.
2. **Consumer (Idempotency):** The consumer must track processed `EventID`s.
```sql
-- Consumer side idempotency check
BEGIN TRANSACTION;
IF NOT EXISTS (SELECT 1 FROM ProcessedEvents WHERE EventID = :eventId) THEN
UPDATE AggregateTable SET ...;
INSERT INTO ProcessedEvents (EventID) VALUES (:eventId);
END IF;
COMMIT;
```
4. Technical Comparison
| Feature | Domain Event (DE) | Integration Event (IE) |
| :--- | :--- | :--- |
| **Scope** | Internal (Bounded Context) | External (Cross-Service) |
| **Transaction** | Part of the local ACID txn | Outbox Pattern (Eventual Consistency) |
| **Transport** | In-memory bus / local DB | Message Broker (Kafka, SNS/SQS) |
| **Format** | Domain Classes | DTO / Schema-bound (Avro/JSON) |
| **Failure Mode** | Local Txn Rollback | Retry / Dead Letter Queue (DLQ) |
5. Synthesis: The Event Lifecycle
An expert implementation follows this flow:
1. **Command Execution:** Aggregate processes a command, emits DE.
2. **Local Side Effects:** Local handlers react to DE (e.g., update local search index).
3. **State Commitment:** Local transaction commits, including the **Outbox Entry**.
4. **Asynchronous Relay:** Outbox relay detects the entry, publishes IE to Kafka.
5. **External Consumption:** Downstream service consumes IE, ensuring idempotency.
By decoupling the *fact of change* (Domain) from the *notification of change* (Integration), we preserve the integrity of the microservice boundary while ensuring system-wide reliability.