Event-Driven Microservices: Lessons from the Field
June 15, 2026 · 3 min read
Event-driven architecture gets pitched as an unambiguous upgrade over synchronous request/response systems: better decoupling, better scalability, better resilience. All true, in the right context. What doesn't get discussed enough is how much operational discipline it demands in exchange for those benefits. A few lessons from building order management and resource-tracking systems on EventBridge and SQS.
Idempotency isn't optional, it's the foundation
The first hard lesson: at-least-once delivery is the default assumption you have to design around, not an edge case. Every consumer needs to treat duplicate delivery as normal. That means idempotency keys on write operations, and it means being deliberate about which operations are naturally idempotent (setting a status) versus which aren't (incrementing a counter). Retrofitting idempotency into a service that wasn't designed for it is far more painful than building it in from day one.
Schema evolution needs a contract, not a convention
When ten services are all consuming the same event stream, "we'll just remember to coordinate schema changes" stops working around the time the team grows past a handful of people. What works:
- Versioned event schemas, with new fields always additive and optional.
- A registry or shared package that's the single source of truth for event shapes, not copy-pasted types across services.
- Consumers built to ignore fields they don't recognize, rather than failing on unexpected data.
This sounds like process overhead until the first time a schema change silently breaks a downstream consumer in production — after that, it's an easy sell.
Observability has to be designed in, not bolted on
Synchronous systems give you a natural trace: a request comes in, you can follow it through the call stack. Event-driven systems don't hand you that for free. Without deliberate effort, "why didn't this order get processed" turns into archaeology across five services and a dead-letter queue. The fix is boring but essential: propagate a correlation ID through every event from the moment it originates, and make sure every consumer logs against it. Pair that with dead-letter queue alerting from day one — a message stuck in a DLQ that nobody notices is a silent failure, and those are the worst kind.
Not everything needs to be async
The most common overcorrection I've seen is treating "event-driven" as a mandate to make everything async, including operations where the caller genuinely needs a synchronous answer. If a user is waiting on a confirmation, forcing that through an async event chain just to be architecturally consistent adds latency and failure modes for no real benefit. Reserve the event-driven pattern for what it's actually good at: decoupling services that don't need to know about each other, absorbing bursty load, and enabling independent scaling — not as a blanket architectural philosophy applied everywhere out of habit.
The payoff, when it's earned
Done well, this pattern is genuinely worth the investment. Legacy batch integrations that used to run on nightly cron jobs become near-real-time. Services scale independently based on their own load characteristics instead of the whole system moving in lockstep. New consumers can be added to an existing event stream without ever touching the producer. That's a real architectural win — it's just not a free one, and the teams that get burned by event-driven systems are usually the ones that adopted the pattern without adopting the discipline that has to come with it.