---
title: "Event-Driven Architecture: What It Solves and Hides"
description: "The price of loose coupling through events: balancing the flexibility it buys against the risk of making your system's flow impossible to trace"
url: https://sade.dev/en/notes/when-event-driven-architecture/
lang: en
author: "Muhammet Şafak"
published: 2026-06-20
updated: 2026-09-06
section: Note
tags: ["architecture","event-driven","messaging"]
summary: "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."
---

# Event-Driven Architecture: What It Solves and Hides

> 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](/en/journal/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. 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:

```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](/en/journal/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.

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


## 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
