Systeric / Docs
Open App →

Think in Events

There are two ways to wire a system together. One is a chain of commands: do this, then call that, then wait for the other. The other is events: something happened, and whatever cares reacts on its own. Most engineers only ever reach for the first. Knowing when a problem actually wants the second, and when it doesn’t, is a real step up in how you design.

This is the design counterpart to Pure Functions & Immutability. Pure functions are about the shape of your logic; thinking in events is about the shape of your system: how the pieces find out about each other, and how tightly they’re bound. The mechanics of the three kinds of “event” live in Working with Events; this doc is about the judgment of when to think this way at all.


The Shift: From Commands to Facts#

What’s expected: You can look at a flow and see it either as a sequence of commands or as a fact that other things react to, and choose deliberately.

Take checkout. The command way: when an order is placed, the checkout code itself sends the confirmation email, then decrements inventory, then pushes to the analytics pipeline, then notifies fulfillment, one function, calling four services, in order, waiting on each.

The event way: checkout does one thing, records the fact that an order was placed, and returns. The email service, the inventory service, analytics, and fulfillment each subscribe to that fact and react on their own. Checkout doesn’t know they exist.

Commands: checkout does it all
placeOrder() {
  save(order)
  email.send(...)
  inventory.decrement(...)
  analytics.track(...)
  fulfillment.notify(...)
}
knows all four; fails if any is down
Events: checkout states a fact
placeOrder() {
  save(order)
  emit("OrderPlaced", order)
}

knows nothing downstream;
each service reacts on its own

Neither is “correct.” They buy different things, and the skill is knowing which trade you want.


What Events Buy You#

What’s expected: You can name the specific benefit you’re getting from an event, not reach for one by reflex.

  • Decoupling. The producer doesn’t know its consumers. You can add a fifth reaction (a loyalty-points update) without touching, or even redeploying, checkout. The blast radius of a new feature shrinks to the new consumer.
  • Resilience. If the email service is down, a command-style checkout fails the whole order. An event-style checkout already recorded the order and emitted the fact; the email retries later. A non-critical dependency can’t take down the critical path.
  • Responsiveness. The user gets their “order confirmed” the moment the order is saved, not after four downstream calls finish. Slow work happens after the response, not inside it.
  • A natural audit trail. A stream of past-tense facts, OrderPlaced, OrderShipped, OrderRefunded, is the history of what happened. It’s often the cleanest way to reconstruct how something got into its current state.

What Events Cost You#

What’s expected: You can state the price of going event-driven, and you don’t pay it where it isn’t worth it.

Events are not free, and treating them as a default is its own failure. The costs are real:

  • Eventual consistency. For a moment after the order is placed, inventory hasn’t decremented and the email hasn’t sent. The world is briefly out of sync. If your UI or your logic assumes everything is done the instant checkout returns, that assumption is now a bug.
  • Harder to follow. A command flow you can read top to bottom in one function. An event flow is scattered across producers and consumers that don’t reference each other, you can’t grep your way from cause to effect. This is where observability stops being optional: the trace is how you follow a flow you can’t read.
  • Delivery is a contract. Once work is async, you inherit at-least-once delivery, out-of-order arrival, and retries, so consumers must be idempotent and must not assume order. That discipline is the price of admission, and it’s covered in Working with Events.

When to Reach for It#

What’s expected: You use an event when work can happen later and elsewhere, and a direct call when you need the answer now.

The test is two questions about the reacting work:

Can it happen…Then
Later (not before you respond) and elsewhere (a different concern)Emit an event; let it react
You need its result to answer the user right nowJust call the function directly

Sending the confirmation email? Later and elsewhere, event. Charging the card and confirming the order before you show a success page? You need the result now, direct call. A good rule of thumb: the critical path that the user is waiting on stays synchronous and direct; the side effects that fan out from it become events. Don’t make a user wait on work they don’t need done to get their answer, and don’t make a simple, must-happen-now step into an async flow you then have to chase across the system.


The Middle Ground Is the Usual Answer#

Most real features are a small synchronous core with a fan-out of events around it: checkout synchronously saves the order and takes payment (the user waits on exactly that), then emits OrderPlaced and lets email, inventory, analytics, and fulfillment react. That shape, a thin critical path plus decoupled reactions, is also the functional-core / imperative-shell idea at the system level: keep what must be correct and immediate small and direct, and let everything else react to the fact. Reach for events where they earn their keep, not everywhere, and not nowhere.


Next: Prove It Works