0% прочитано

Domain Event or Direct Method Call: How I Choose Between Them

I do not use domain events everywhere. If the second action is required to protect an invariant, I keep it as a direct call in one transaction. If another part of the system only reacts to an already committed fact, a domain event is often a better fit.

14 жовтня 2026 р. 10 хв читанняLaravel

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.

My basic check is simple: if skipping the second action could leave an invariant false, it is not merely a reaction after the fact. It belongs to the primary operation.

Similar Steps Can Require Different Decisions

Similar sequence, different consistency requirements
Case A activate product    |    vsave required state If the second step fails,the product must not be active.  Case B activate product    |    v

In 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

Both changes are part of one consistent decision
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

First the fact becomes true, then other parts react
Product activation validate   |change state   |save   |commit   |   vProductActivated reactions:- notification- analytics

ProductActivated 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

An event naturally describes a fact that already happened
Fact: ProductActivatedAccessGrantedRegistrationConfirmed  Command-like name: ActivateProductGrantAccessConfirmRegistration

This 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

A listener does not react to a fact that can still roll back
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

Too many events hide the execution flow
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?

Failure behavior quickly reveals the right relationship
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 fit

This 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.