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.
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.
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.
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.
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.
To achieve atomicity without 2PC (Two-Phase Commit), use the Transactional Outbox:
OUTBOX table in the same database.OUTBOX table or tails the database transaction log.Strict "Exactly-Once" is a theoretical ideal; in practice, we achieve it via At-Least-Once Delivery + Idempotent Consumption.
EventIDs.
-- 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;
| 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) |
An expert implementation follows this flow:
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.