Skip to content
Leo Jung
Go back

[DDD Intro #6] Domain Event: Expressing what happened in the domain

Edit page

A domain model is not made only of state. Things keep happening in the domain. Orders are placed, payments are completed, shipments are requested, subscriptions are renewed, refunds are approved, and entitlements expire.

These are not simple data changes. They are meaningful events in the domain.

A Domain Event is an object that represents an important thing that has already happened in the domain. An event is not a command. It is not a request. An event is a fact that occurred in the past.

Understanding this difference is important.

Events are in the past tense

Event names are usually written in the past tense.

SubscriptionRenewed
PaymentCompleted
SubscriptionCanceled
AccessGranted
RefundApproved

These names do not say “do something.” They say “something happened.”

RenewSubscription is closer to a command. It is a request to renew a subscription. SubscriptionRenewed, on the other hand, is an event. It is the fact that a subscription was renewed.

This distinction is not merely a naming preference. It is a modeling distinction.

A command can fail. A renewal may be requested, but if payment fails, the subscription may not be renewed. An event is a fact that has already happened, so it cannot be undone. A compensating event may happen later, but the fact that the original event occurred does not disappear.

Command: RenewSubscription
Result: SubscriptionRenewed or SubscriptionRenewalFailed

This distinction makes domain flow clearer.

Why Domain Event is needed

There are several reasons to use domain events. The most important reason is to explicitly express what happened in the domain.

When a subscription is renewed, several follow-up actions may be needed.

Extend the entitlement to the next period.
Send a receipt email.
Record the renewal event in marketing analytics.
Reflect active subscriber information in the recommendation system.
Add history to the customer support timeline.

What happens if we put all of these actions directly inside the Subscription Aggregate or Application Service?

subscription.renew(paymentResult);
entitlementService.extend(subscription);
emailService.sendReceipt(subscription);
analyticsService.recordRenewal(subscription);
supportTimelineService.addRenewalHistory(subscription);

This may look fine at first. But as follow-up actions increase, the subscription renewal use case becomes heavier. The subscription model ends up knowing too much about entitlement, email, analytics, and customer support.

With Domain Event, the subscription Aggregate can express only the important fact for itself.

subscription.renew(paymentResult);

// internally records a SubscriptionRenewed event

Other concerns then react to the event.

SubscriptionRenewed
  -> extend entitlement
  -> send receipt email
  -> append analytics event
  -> add customer support history

An event connects concerns around a meaningful fact that happened in the domain.

Events reduce coupling

An important effect of Domain Event is reducing coupling. Subscription renewal logic no longer needs to know directly about sending email, analytics, or customer support history.

The subscription context publishes the fact that “a subscription was renewed.” Other components or contexts that care about that fact react in their own ways.

This structure is especially useful for collaboration between Bounded Contexts.

When payment is completed in the billing context, the subscription context may need to know. When a subscription is renewed in the subscription context, the entitlement context must extend access. If one context directly uses another context’s internal model, the model becomes contaminated.

Events can become the language of collaboration between contexts.

Billing Context: publishes PaymentCompleted
Subscription Context: receives PaymentCompleted and renews subscription
Subscription Context: publishes SubscriptionRenewed
Entitlement Context: receives SubscriptionRenewed and extends access

Of course, using events does not automatically create good design. Event names and data must carry domain meaning well. If events are used only as technical messages, they become another point of coupling.

Domain Event and Integration Event

In practice, it is useful to distinguish Domain Event from Integration Event.

A Domain Event is a meaningful event that occurred inside the domain model. It is mainly used inside the same Bounded Context to express domain logic and connect follow-up handling.

An Integration Event is closer to a contract published externally to communicate with another system or another Bounded Context.

This does not mean they must always be separate objects. But we must understand that their purposes differ.

Suppose there is a domain event named SubscriptionRenewed. Internally, it may have this data:

public record SubscriptionRenewed(
    SubscriptionId subscriptionId,
    SubscriberId subscriberId,
    PlanId planId,
    Period renewedPeriod
) {}

An integration event published to external systems may need a more stable contract.

{
  "eventType": "subscription.renewed.v1",
  "subscriptionId": "sub_123",
  "subscriberId": "mem_456",
  "startsAt": "2026-07-01T00:00:00",
  "endsAt": "2026-08-01T00:00:00"
}

A Domain Event is close to the language of the model, while an Integration Event is close to a contract between systems. They do not always need to be separated, but if we do not distinguish them, internal model changes can break external contracts, or external contracts can make the internal model rigid.

Events are not byproducts of state changes

It is not enough to understand events as “the state changed, so leave a log.” A Domain Event must be a meaningful event in the domain.

For example, this event is too technical:

SubscriptionStatusChanged

The state change itself may be important in some cases. But usually we can find a more domain-oriented name.

SubscriptionRenewed
SubscriptionCanceled
SubscriptionExpired
SubscriptionSuspendedDueToPaymentFailure

SubscriptionStatusChanged does not say what happened. It only says a status value changed. SubscriptionSuspendedDueToPaymentFailure, on the other hand, contains the domain meaning that the subscription was suspended because payment failed.

A good event name should support conversation with domain experts.

“A subscription status change event occurred” is much less domain-oriented than “The subscription was suspended due to payment failure.”

Aggregate and Domain Event

Domain Events usually occur inside Aggregate behavior. As an Aggregate changes its own state, it can record that something important happened from the domain perspective.

Consider subscription renewal.

public class Subscription {
    private SubscriptionId id;
    private SubscriberId subscriberId;
    private SubscriptionStatus status;
    private Period currentPeriod;
    private List<DomainEvent> domainEvents = new ArrayList<>();

    public void renew(PaymentResult paymentResult) {
        if (!paymentResult.isSuccess()) {
            recordPaymentFailure(paymentResult);
            return;
        }

        this.currentPeriod = currentPeriod.next();
        this.status = SubscriptionStatus.ACTIVE;

        domainEvents.add(new SubscriptionRenewed(
            id,
            subscriberId,
            currentPeriod
        ));
    }
}

This is not a complete implementation, but it shows the core idea. Subscription performs the renewal rule and leaves the fact that it was renewed as an event.

The Application Service can publish events after saving the Aggregate.

Subscription subscription = repository.findById(subscriptionId);
subscription.renew(paymentResult);
repository.save(subscription);
eventPublisher.publish(subscription.pullDomainEvents());

In this flow, the event is not a separate task detached from the Aggregate’s state change. It is a fact that occurred as the result of a domain behavior.

Overusing events hides flow

Domain Event is powerful, but overusing it can make a system harder to understand. If every method call becomes an event, explicit flow disappears. It becomes hard to trace where something is executed.

It is especially risky to scatter core domain rules that must be processed in order within one transaction into event handlers.

For example, if subscription renewal must always extend the period and activate the status, that should be handled directly inside the Subscription Aggregate. If it is postponed to an event handler, the system can temporarily enter an incomplete state.

Events are well suited for important follow-up reactions that can be loosely connected. In contrast, rules that protect invariants inside an Aggregate should not be scattered through events.

Useful questions are:

Is this work part of the original behavior?
Or can another interested party react after that behavior occurred?
Is immediate consistency required?
Is eventual consistency enough?

What should an event contain?

A Domain Event should contain the information it needs. But if it contains too much, the event becomes a copy of another model. If it contains too little, receivers must query the source Aggregate again and coupling may appear.

What does a SubscriptionRenewed event need?

At minimum, it may need the subscription ID and renewed period. The entitlement context may need the subscriber ID and period to extend access. But it may not need payment method details or internal policy objects.

Event design is a balance between receiver convenience and publisher encapsulation.

For a Domain Event, include data meaningful inside the domain model. For an Integration Event published externally, design a long-term stable contract more carefully.

Closing

Domain Event is a way to express important things that happened in the domain. An event is not a command. It is a fact that has already occurred. That is why the name should be in the past tense of the domain language.

A good Domain Event makes the model clearer. It reveals the reason for state changes, loosely connects concerns, and enables collaboration between Bounded Contexts. SubscriptionSuspendedDueToPaymentFailure is better than SubscriptionStatusChanged because it says what actually happened in the domain.

But events are not the answer to every problem. If core rules that protect invariants are scattered into event handlers, the model becomes weaker. Events shine most when they express important facts that occurred as the result of domain behavior.

In the next post, we will look at how the concepts discussed so far fail in reality. DDD does not fail only because it is difficult. In many cases, it fails because it is misunderstood as a way to apply patterns.


Edit page
Share this post:

Previous Post
[DDD Intro #7] Why DDD fails
Next Post
[DDD Intro #5] Aggregate: The boundary of consistency