In the previous article about DDD and consistency boundaries I wrote that one business operation does not necessarily mean one large transaction.
The next question follows naturally: if some work can happen outside the transaction, should it automatically become a Domain Event?
In practice, I do not use events as the default way to call the next step. I first decide whether the second action is part of valid state or merely a reaction to an already completed fact.
Similar Steps Can Require Different Decisions
Case A activate product | vsave required state If the second step fails,the product must not be active. Case B activate product | vIn the first case, the second action is part of valid system state. In the second case, it merely reacts to an already successful change.
That distinction matters more to me than how elegant an event-driven approach looks.
If the Second Action Is Part of the Invariant, I Prefer a Direct Call
DB::transaction(function () use ( $product, $registration,) { $product->activate(); $registration->confirmFor( $product->id, ); $this->products->save( $product, ); $this->registrations->save( $registration,If Product is not allowed to be active without required confirmed state, I do not want to commit first and then hope a listener completes the second half of the rule.
A direct call honestly shows the dependency. If one step fails, the transaction rolls both back.
An Event Should Not Weaken an Invariant Just to Decouple Code
If the system can enter a domain-invalid state after the first commit, an event only hides the problem.
The code became less direct while the invariant became weaker. This is the kind of case I discussed in the article about when one transaction is better than an event.
A Domain Event Fits When the System Announces an Already Completed Fact
Product activation validate |change state |save |commit | vProductActivated reactions:- notification- analyticsProductActivated does not ask someone else to finish activation. It announces that activation already succeeded.
If notification or analytics temporarily fail, they can be retried without undoing the activation itself.
The Event Name Helps Check Its Meaning
Fact: ProductActivatedAccessGrantedRegistrationConfirmed Command-like name: ActivateProductGrantAccessConfirmRegistrationThis is not an absolute rule, but it is a useful signal.
If an event name sounds like a command saying "do this," I may simply have hidden a direct call behind an event.
A Domain Event reads more naturally as a fact in the past tense.
A Reaction to a Fact Should Start After Commit
DB::transaction(function () use ( $product,) { $product->activate(); $this->products->save( $product, ); DB::afterCommit(function () use ( $product, ) { ProductActivated::dispatch( $product->id, ); });If an event means "this already happened," I do not want it dispatched before a successful commit.
Otherwise a listener may perform an external action while the primary transaction later rolls back. The system would announce a fact that never became true.
When an Event Actually Adds Value
An event is useful to me when the primary fact is already complete, another part of the system merely reacts, the reaction can be retried, and the source should not know every future consequence.
I discussed this type of scenario more broadly in the article about when an event is the right choice.
Why I Avoid Events Everywhere
Method A |event 1 |listener |event 2 |listener |event 3 |listener Question:An event creates an indirect relationship. That is both its strength and its cost.
If understanding one action requires finding several listeners and jobs, the complexity may come from code organization rather than from the domain.
I use an event when that indirection adds real value.
A Direct Method Call Is Not Automatically Bad Coupling
If one action truly cannot be correct without another, a domain dependency already exists.
An event does not remove it. It only makes it less visible.
In those places, a direct call is often more honest because the mandatory sequence is explicit.
An Application Service Can Coordinate Required Steps Directly
In the article about Application Service and Domain Service I wrote that an Application Service coordinates the use case.
If several steps must happen in one transaction, explicit coordination often reads better than a chain of hidden listeners.
The Best Question for Me Is: What Should Happen on Failure?
If second step fails... Should first step roll back? YES-> direct call-> same consistency boundary-> usually same transaction NO-> first fact may commit-> second step can retry-> event may fitThis test works better for me than the abstract question "event or service?"
If failure of the second action makes the first invalid, I want rollback. If the first action remains valid and the second can be retried later, an event starts to look natural.
In Center, I Stopped Choosing Events Just to Separate Code
While working on Center, this distinction became practical for me.
As use cases grew, an event sometimes looked like the cleanest way to separate responsibilities.
But I kept returning to one question: is the second action part of valid state, or is it a reaction to state that is already valid?
That helped me avoid scattering critical rules across listeners while keeping the core model independent from secondary reactions.
My Short Check Before Creating a Domain Event
I ask:
- Is the primary action already valid without the listener?
- Does the second action only react to a fact that already happened?
- If the listener fails, can it be retried safely later?
- Should failure of the second action roll back the first?
- Does the event describe a fact rather than a hidden command?
- Does the event reduce unwanted coupling, or merely hide a mandatory sequence?
I Choose by the Type of Dependency, Not by Style
For me, direct method calls and Domain Events are no longer competitors.
A direct call expresses a mandatory dependency: without the second step, the first result is not valid.
A Domain Event expresses a completed fact: the primary change is already valid and other parts of the system may react.
If the second action protects an invariant, I keep it in the primary flow. If it only reacts to a fact after commit, an event becomes the natural choice.
An event should not hide a mandatory dependency. If the first action becomes invalid without the second, they form one consistent operation rather than a reaction after the fact.



