Why events are attractive
Events let one business action serve several consumers without making the original request wait for every downstream effect. That is valuable when notifications, search indexing, analytics, and workflows evolve at different speeds.
The cost is delayed understanding
A synchronous call makes the dependency visible. An event hides it until an operator needs to trace why a customer-facing state did not change. Event names, payload versions, correlation IDs, and replay tools are therefore part of the product, not optional infrastructure.
The boundary I use
I reserve events for facts that have happened and use commands for requests to change state. That distinction prevents a message bus from becoming an untyped remote procedure call system.
Decoupling is useful when it buys independent change. Otherwise, a direct call is often the more honest design.
The contract is the architecture
An event should describe a fact that has already happened: InvoiceIssued, AssetLocated, or UploadCompleted. It should include a stable event ID, an aggregate or resource ID, the occurred-at timestamp, a schema version, and a correlation ID. Those fields make duplicate handling and incident tracing possible.
flowchart TD
A[Command] --> B[Transactional state change]
B --> C[Outbox record]
C --> D[Publisher]
D --> E[Event broker]
E --> F[Consumer: notifications]
E --> G[Consumer: search]
E --> H[Consumer: analytics]
The outbox pattern is useful when the state change and event publication must not drift apart. The application commits the business change and an outbox row in one transaction; a publisher can safely retry delivery later.
await database.transaction(async (tx) => {
const invoice = await tx.invoices.issue(input);
await tx.outbox.insert({
type: "InvoiceIssued",
aggregateId: invoice.id,
payload: invoice,
});
});
Ordering is local, not magical
Most systems do not need global ordering. They need ordering for one account, asset, or aggregate. Partitioning around that key gives consumers a meaningful sequence without forcing unrelated work through one bottleneck.
Operational questions
Before introducing a broker, I ask how a consumer is replayed, how a poison message is isolated, how payload versions are retired, and how an operator can find every downstream effect of one command. If those answers are missing, the event-driven design has moved complexity rather than removed it.
