0% прочитано

When an Event Is Actually the Right Choice

An event is not useful after every state change. I show how I decide when the main operation is already complete and audit, notifications, projections, or other reactions may safely happen later.

3 жовтня 2026 р. 8 хв читанняLaravel

Events are easy to overuse.

A status changes, so an event is created. A row is stored, so another event appears. A method completes, and perhaps that needs an event too.

But the existence of a state change does not automatically mean the system needs an event.

I find a different question more useful: is the main operation already correct and complete without the future handler?

If the answer is yes and later work only reacts to an already completed fact, an event is often a good boundary.

I use an event to react to an already completed fact, not to hide a second mandatory half of the same operation.

First I Define Where the Main Operation Ends

The main change and reactions that follow
Main operation    |    +--> validate rules    +--> change state    +--> persist result    |    voperation is complete  After that:     +--> audit    +--> notification    +--> projection    +--> analytics

If the authoritative state is already correct and the system can consider the operation successful, later work does not necessarily need to happen in the same call.

For example, after a successful change the system may need to:

  • create an audit record;
  • send a notification;
  • update a separate read projection;
  • send data to analytics;
  • start secondary background work.

If a short delay in these reactions does not make the main state invalid, they can be good event consumers.

A Good Event Describes a Fact That Already Happened

PHP - The event describes a completed fact
final readonly class OrderConfirmed{    public function __construct(        public string $orderId,        public DateTimeImmutable $confirmedAt,    ) {}}

I prefer naming events in the past tense: OrderConfirmed, ReportGenerated, ProfileUpdated.

The name makes the semantics clear: this is not a command to perform work, but a notification that a fact has already occurred.

Handlers may then decide whether they need to react to that fact.

An Event Reduces Coupling to the Number of Reactions

PHP - The main use case knows every reaction
$order->confirm(); $auditService->recordConfirmation($order);$notificationService->sendConfirmation($order);$analyticsService->trackConfirmation($order);$projectionService->refresh($order);
PHP - The main use case only publishes the fact
$order->confirm(); $repository->save($order); event(new OrderConfirmed(    $order->id(),    $order->confirmedAt(),));

If the main use case directly calls every secondary action, it gradually becomes aware of more and more parts of the system.

Add another notification channel and the main use case changes. Add another projection and it changes again.

An event lets the primary operation remain stable: it publishes the completed fact while new reactions can be attached separately.

My Main Test: Is a Delay Acceptable?

Testing whether delay is acceptable
Main change completed        |        vevent published        |        +--> handler after 10 ms        |        +--> handler after 2 s        |        +--> retry after 30 s  Is the authoritative state still correct? YES -> an event may be appropriate 

This is one of the most useful checks for me.

I imagine that the event handler does not run immediately. The queue may be temporarily unavailable. The job may retry thirty seconds later.

If the authoritative state remains correct during that time, asynchronous reaction may be perfectly acceptable.

If the system becomes invalid without the second action, I do not use an event merely for architectural style.

Audit, Notifications, and Projections Often Fit Well

Several kinds of reactions naturally fit event-driven processing.

Audit. The authoritative change has already happened. Another component may record the fact for history.

Notifications. A successful business operation should not necessarily fail merely because an email provider responds slowly.

Read projections. A separate representation can be refreshed after authoritative data changes when a small delay is acceptable.

Analytics and metrics. They react to the fact but do not decide whether the fact itself was allowed.

An Event Is a Poor Fit for the Second Mandatory Half of an Invariant

Not every second action belongs in a handler
Step A persisted    |    vevent    |    vStep B has not happened yet  If the state between A and B is invalid,this is a dangerous boundary.

If two changes together form one invariant and the intermediate state must never exist even briefly, an event creates a dangerous gap.

In that case, one transaction or another strong-consistency mechanism may be more appropriate.

I do not use events simply to split an operation that must naturally complete as one unit.

An Event Should Not Announce Something That May Still Roll Back

PHP - The event may escape before the transaction commits
DB::transaction(function () use ($order): void {    $order->confirm();     event(new OrderConfirmed(        $order->id(),        $order->confirmedAt(),    ));     $repository->save($order);});

There is another important boundary: an event should describe a fact that actually happened.

If a handler runs before the surrounding transaction commits, it may observe an event for a change that is rolled back a moment later.

For reactions to committed work, I therefore use an after-commit mechanism or another approach that guarantees the correct ordering.

PHP - Publish after a successful commit
DB::transaction(function () use ($order): void {    $order->confirm();     $repository->save($order);     DB::afterCommit(        static fn () => event(            new OrderConfirmed(                $order->id(),                $order->confirmedAt(),            )        )    );});

afterCommit Fixes Ordering, Not Every Delivery Problem

afterCommit helps prevent publishing an event before data is successfully persisted.

But it does not magically make delivery failure-proof.

For example, the transaction may commit and the process may terminate before the message is actually handed to a queue.

If losing that event is unacceptable, a stronger delivery guarantee is required.

For Important Delivery I Consider an Outbox

The authoritative fact and event record commit together
One transaction:     authoritative change          +    outbox event record          |        COMMIT After commit:     worker reads outbox          |          v    publishes event          |          v

If an event must not be lost between the main transaction commit and queue delivery, I consider the outbox pattern.

The idea is that the authoritative change and a record describing the event are persisted in the same database transaction.

A separate process later reads those records and publishes the messages.

This is more complex than a simple event(), so I do not add an outbox everywhere automatically. For important integration events, however, the stronger guarantee may be justified.

An Event Handler Should Be Ready for Retries

Once an event moves through a queue or another delivery system, I do not assume that every message will be processed exactly once.

A job may retry after failure. A message may be delivered more than once. A worker may complete the side effect and terminate before acknowledging success.

For important handlers, I therefore aim for idempotent behavior: processing the same event again should not create another identical side effect.

I Put the Fact in the Event, Not the Whole Eloquent Model

PHP - A small explicit payload
final readonly class OrderConfirmed{    public function __construct(        public string $orderId,        public DateTimeImmutable $confirmedAt,    ) {}}

Events can also become a hidden mechanism for passing large object graphs around the application.

I try to avoid that.

A handler usually needs a small set of stable facts: an identifier, occurrence time, and data without which the event would lose its meaning.

This makes the event contract explicit and reduces coupling to the current shape of an ORM model.

Not Every Internal Event Should Become an Integration Event

There is another boundary I keep separate: an internal application event and a contract for other systems.

Internally, an event may help separate responsibilities between components.

But once a message leaves the system, it becomes an external contract whose name, payload, and semantics require much stronger stability.

I therefore do not publish every internal event externally by default.

What I Check Before Adding an Event

Before introducing a new event, I ask several questions:

  • Is the main operation complete without this handler?
  • Is delayed reaction acceptable?
  • Does the system remain correct if the handler is temporarily unavailable?
  • Does the event describe a completed fact rather than hide a command?
  • Should it be published only after commit?
  • How critical is reliable delivery?
  • Is retrying the handler safe?
  • Does the event expose more internal data than necessary?

An Event Fits Where the Fact Is Already Complete

For me, an event is not a way to make code look more architectural.

It is useful when it correctly separates an authoritative fact from reactions to that fact.

The main operation should complete its own rules and persist valid state.

After that, audit, notifications, projections, analytics, and other independent reactions may happen separately when delay does not violate correctness.

For simple cases, publishing after commit may be enough. Important delivery may require an outbox. Handlers should be designed with retries in mind.

My main criterion is simple: if the event is delayed, the authoritative fact should still remain correct and complete.

An event is useful not merely because something changed, but because a completed fact may now have independent listeners.