Domain Events in DDD — Java Implementation
Fast recall
Quick revision
Core answer
OrderPlaced, PaymentCaptured, UserRegistered). Aggregates raise events when state changes; the application layer publishes them after persistence; other parts of the system subscribe to react. Events make cross-aggregate consistency, integration, and audit possible without coupling components. In Java + Spring, raise events inside aggregate methods, drain them after save, publish via ApplicationEventPublisher or a transactional outbox for reliable cross-service delivery.Events as past-tense business facts (not commands), the publish-after-commit problem, the transactional outbox pattern, and idempotent consumption.
Publishing events inside a transaction (consumer sees event before DB committed), or modeling 'events' as CRUD diffs (`CustomerUpdated{name, email}`).
Discuss in-process Spring events vs Kafka for cross-service, the dual-write problem and how outbox solves it, idempotent consumers, and event versioning.
Interview-ready guide
Interview answer
What a domain event is
A fact: something happened that the business cares about. Past tense. Immutable once raised. Describes what occurred, not what should happen.
Good: OrderPlaced, PaymentCaptured, EmailVerified, InventoryReserved, RefundIssued. Bad: PlaceOrder (command), CustomerUpdated (CRUD diff), RowChanged (technical).
Good events make the system expressive, integratable, auditable. Multiple subscribers react without producer knowing. Event log becomes audit log for free. Eventual consistency between aggregates and services is natural.
Raising events in the aggregate
Order root has internal events list. create() adds OrderPlacedEvent. cancel() adds OrderCancelledEvent. drainEvents() returns and clears.
Aggregate just records events; doesn't publish. Application layer drains and publishes after persistence. Decoupling lets aggregates stay framework-free and unit-testable.
Defining events
Records. Immutable, value equality, succinct. OrderPlacedEvent(orderId, customerId, total, occurredAt) implements DomainEvent.
Guidelines: past tense in name, carry only what subscribers need (IDs + key value objects + timestamp), avoid carrying entire aggregates — events are facts not snapshots.
Publishing after commit
Wrong: publish inside transaction. Consumer might query for order before transaction commits — doesn't find it. Or transaction rolls back, you've published phantom event.
Right: publish after commit. Either explicitly drain + publish after the @Transactional method returns, or use Spring's @TransactionalEventListener(phase = AFTER_COMMIT) so listeners only fire when transaction has committed.
In-process vs cross-service
In-process: ApplicationEventPublisher enough. @TransactionalEventListener(AFTER_COMMIT). Synchronous default or @Async. Listener exceptions don't roll back producer's transaction. Good for same-bounded-context reactions.
Cross-service: publish to Kafka / RabbitMQ. Hit the dual-write problem — what if DB save succeeds but Kafka publish fails or vice versa? Inconsistent state.
Transactional outbox — reliable cross-service
In one transaction: save aggregate + insert event rows into outbox table.
Separate worker polls outbox, publishes to Kafka, marks rows as published. Kafka down? Worker retries. Outbox guarantees no event lost.
Schema: outbox(id, aggregate_id, event_type, payload, occurred_at, published_at).
Modern alternatives: Debezium CDC streams outbox table directly to Kafka — no separate worker. Kafka transactions also work but outbox simpler and more portable.
Idempotent consumers — at-least-once reality
Kafka and most brokers guarantee at-least-once. Your consumer will see same event twice eventually.
Make consumers idempotent: dedupe by event ID (processedEventRepo.existsById), or design side effect harmless on duplicate (UPSERT keyed on aggregate ID, use event ID as natural key for new entities).
Never assume exactly-once without specific broker + consumer guarantees.
Common mistakes
- Commands disguised as events — PlaceOrder isn't a fact
- CRUD events — CustomerUpdated says what changed but not why
- Publishing inside transaction — stale data, phantom events
- Entire aggregates in events — bloat, coupling, serialization overhead
- Dual writes — inevitable inconsistency
- Non-idempotent consumers — duplicate side effects (double-charge)
- No event versioning strategy — schema change breaks consumers
- Synchronous chains across services — distributed monolith
Recommendation
Model domain events as past-tense business facts (OrderPlaced, not PlaceOrder); raise them inside aggregate methods on an internal list and drain after persistence. Use Spring's @TransactionalEventListener(AFTER_COMMIT) for in-process listeners; for cross-service publication, use the transactional outbox pattern (or Debezium CDC) to avoid the dual-write problem. Make all consumers idempotent because at-least-once delivery is the realistic guarantee. Use records as event types; carry only IDs and key value objects; plan additive schema evolution from day one. Events are your system's public language — name them with care.
Domain Events in DDD — Java Implementation
Direct Answer
A Domain Event is a named, past-tense business fact that something important happened in the domain (OrderPlaced, PaymentCaptured, UserRegistered). Aggregates raise events when state changes; the application layer publishes them after persistence; other parts of the system subscribe to react. Events make cross-aggregate consistency, integration, and audit possible without coupling components. In Java + Spring, raise events inside aggregate methods, drain them after save, publish via ApplicationEventPublisher or a transactional outbox for reliable cross-service delivery.
Common Mistakes & Pitfalls
Publishing events inside a transaction (consumer sees event before DB committed), or modeling 'events' as CRUD diffs (CustomerUpdated{name, email}).
Check your understanding
Answer these without looking back at the article.
- Why is publishing events inside the transaction dangerous?
- Walk through the transactional outbox pattern end-to-end.
- How do you make a Kafka consumer idempotent?
- How do you evolve event schemas without breaking consumers?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.