# Event-Driven Architecture: What It Solves and Hides

> The price of loose coupling through events: balancing the flexibility it buys against the risk of making your system's flow impossible to trace

- Type: Notes
- Published: 2026-06-20
- Updated: 2026-09-06
- Tags: architecture, event-driven, messaging
- Reading time: 3 min read
- Source: https://muhammetsafak.com/engineering/notes/when-event-driven-architecture/
- Language: en-US
- Author: Muhammet Şafak

---
> **TL;DR:** Events buy loose coupling by letting the producer stay unaware of its consumers, and the same move makes the flow invisible: no compiler verifies an event contract the way it verifies a method call. Two costs are paid up front or not at all — versioned, schema-validated payloads, and a correlation id carried into every log and every downstream event. A single-listener event is a method call in disguise.

I once watched a team where no one could give a straight answer to "what happens when an order is created?" In the codebase, eight separate listeners were subscribed to the `OrderCreated` event; you couldn't read from any single place which one ran in which order, under which condition. The flow didn't live in the code — it lived in people's heads, and incompletely at that.

Event-driven architecture loosens coupling. This post is a reminder that something else loosens along with it: causality.

## What do events solve?

Code that publishes an event doesn't know who listens to it. `OrderCreated` is published; billing, notifications, and analytics each listen to it separately. The order code is unaware of all three.

The benefit is real: adding a new consumer happens without touching the producer. Independent development, independent deployment, fan-out. Parts that don't know each other.

## What do events hide?

The flip side of the same coin: as coupling loosens, **the flow becomes invisible**.

In a direct method call, the flow *is* the code — you go to the definition and read on. Not so with an event. To see what happens when `OrderCreated` is published, you have to find every listener by hand. The compiler won't help you: a compiler verifies the contract of a method call, but no one verifies the contract of an event.

The line from [the modular monolith post](/engineering/essays/why-i-start-with-a-modular-monolith/) applies here too — like a network contract, an event contract sits in the compiler's blind spot.

## Don't choose it before paying: two prerequisites

Before moving to an event-driven architecture, you pay two costs up front.

**Schema discipline.** An event is a contract. When the payload of `OrderCreated` changes, the eight places listening to it won't know — not until it breaks in production. Events must be versioned, fields must be added in a backward-compatible way, [payloads must be validated against a schema](/engineering/notes/validate-schema-at-the-edge/). Without this discipline, loose coupling means "silent breakage."

**Traceability.** To be able to see a request travel through the system, [every event must carry a correlation ID](/engineering/systems/polyglot-queue-distributed-tracing/):

```php
event(new OrderCreated(
    orderId: $order->id,
    correlationId: request()->header('X-Correlation-Id') ?? (string) Str::uuid(),
));
```

If this ID isn't carried into the logs and into subsequent events, then in a multi-listener flow there is no answer to "why did this job run?" Tracing isn't an optional add-on to event-driven; it's a prerequisite.

## In Laravel: in-process events ≠ event-driven architecture

Laravel's `event()`/listener mechanism, when it runs synchronously, is really an organized method call — same process, same transaction, same stack trace. This is safe and traceable; use it freely.

The difficulties event-driven architecture brings start when an event crosses a **boundary**: into a queue, into a message broker, into another process. The moment you add `ShouldQueue` to a listener and send the event to RabbitMQ, the schema and tracing debt kicks in. Don't conflate the two: using in-process listeners does not move you into "event-driven architecture."

## When should you move to event-driven?

Actually carrying an event across a boundary is justified in these cases:

- **There's a real fan-out.** A single event is listened to by many independent consumers that shouldn't know about each other.
- **The consumer must be asynchronous.** The work shouldn't keep the producer waiting for a response; "accepted" is enough.
- **The consumers must scale or be distributed independently.** This also brings us to [the microservices decision](/engineering/essays/when-i-move-to-microservices/) — the same measured-signal threshold.

Without these, a direct service call is both more readable and safer. An event with a single listener is just a method call in disguise — and a harder one to trace, at that.

---

Event-driven architecture loosens coupling; in doing so, it loosens causality too. If you want the first, pay the bill for the second — schema and tracing — up front.

Loose coupling isn't free; you pay for it with invisible flow.

## Sources

- [Enterprise Integration Patterns: Publish-Subscribe Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html) — Hohpe and Woolf
- [Enterprise Integration Patterns: Correlation Identifier](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CorrelationIdentifier.html) — Hohpe and Woolf
- [Events: queued event listeners and ShouldQueue](https://laravel.com/docs/12.x/events) — Laravel

## Frequently asked

### Does using in-process Laravel events mean you have an event-driven architecture?

No. A synchronous listener is an organized method call — same process, same transaction, same stack trace — and it is safe and traceable. The difficulties start when the event crosses a boundary into a queue, a broker or another process; that is where the schema and tracing debt kicks in.

### What has to be paid for before moving to event-driven?

Two things, up front. Schema discipline: an event is a contract, so it must be versioned, extended in a backward-compatible way and validated against a schema, or loose coupling just means silent breakage. And traceability: every event carries a correlation id into the logs and into the events it triggers, otherwise there is no answer to why a job ran.
