Event-Driven Architecture: Moving Beyond Simple REST APIs
The symptom: a controller that does too much
You've seen this function before. A single POST /orders handler that creates the order, charges the card, sends a confirmation email, updates inventory, notifies the warehouse, and pings an analytics endpoint — all inline, all synchronous, all in one try/catch block that's grown to 200 lines. Every new requirement ("also notify Slack when an order is over $500") means editing a function that already has too many reasons to fail.
Separating "what happened" from "what should happen next"
Event-driven architecture inverts the dependency. The order service's only job is to decide that an order was placed and publish that fact:
await orderRepository.save(order);
await eventBus.publish("order.placed", {
orderId: order.id,
total: order.total,
items: order.items,
});
return order;
Everything downstream — payment capture, inventory reservation, email, warehouse notification — becomes its own consumer, subscribed to order.placed, living in its own module with its own retry policy. The order service doesn't know or care that six other things happen after it. That's the whole point.
What you gain
- Independent failure domains. If the email service is down, orders still get created and inventory still reserves. The email just retries from the queue later.
- Independent scaling. The analytics consumer can run one replica; the payment consumer can run ten.
- Auditability for free. Every state transition is a discrete, loggable event instead of a line buried in a stack trace.
What it costs you
Eventual consistency is the honest trade-off. Between "order placed" and "inventory reserved" there's a window — usually milliseconds, sometimes longer under load — where the two are out of sync. Most teams new to this pattern try to eliminate that window; the ones who succeed instead design for it: idempotent consumers, compensating actions for failures, and UI that reflects "processing" rather than assuming instant consistency.
When REST is still the right call
Not everything needs an event bus. If an action has exactly one consequence and that consequence needs to happen before you can respond to the client (validating a coupon code, say), a direct function call or synchronous REST request is simpler, easier to debug, and has fewer moving parts to operate. Reach for events when an action fans out to multiple independent side effects that don't need to complete before you respond — that's the actual signal, not "microservices are cool."