Framing these as rivals is a category error. Microservices is a decision about how you split a system into separately deployable pieces. Event-driven architecture is a decision about how those pieces talk to each other. One is about structure, the other about communication, so asking which to pick is like asking whether to use rooms or doors. You can have microservices that only ever make direct synchronous calls, and you can have a single deployable that is heavily event-driven inside. The useful questions are when each split is worth its cost, and when events are the right way for parts to communicate.
What microservices actually solve
Microservices break a system into units that deploy, scale, and fail on their own. The payoff is organisational as much as technical: separate teams own separate services, ship on their own cadence, and pick the runtime that suits their workload without coordinating a single release. A service under heavy load can scale independently of the rest, and a bug in one is less likely to take down everything else.
At Motive Partners we built the payments, authentication, and webhook handling as distinct services, with payments going through Plaid. Those are genuinely different concerns with different security postures and different reasons to change, so splitting them let each evolve without dragging the others along. You can read more in the Motive Partners case study.
The costs nobody mentions in the diagram
The boxes-and-arrows picture hides the operational bill. Every service boundary is now a network call that can be slow, fail, or time out, so you inherit retries, timeouts, and partial failures across the whole system. A request that was once a function call is now a distributed transaction with no easy rollback. You need service discovery, versioned contracts, per-service deployment pipelines, distributed tracing, and a way to reason about a request that touches six services. Most teams that regret microservices did not regret the idea. They regretted splitting before they had the operational maturity to run a fleet, and ended up with a distributed monolith: all the network cost, none of the independence.
What event-driven architecture actually solves
Event-driven architecture changes how parts communicate. Instead of one component calling another and waiting for an answer, a producer emits an event (something happened) and any number of consumers react to it on their own time. The producer does not know or care who is listening. That single property buys you three things that synchronous calls struggle with.
Decoupling
Producers and consumers do not call each other directly, so you can add, remove, or change a consumer without touching the producer. The producer's job ends when the event is published.
Fan-out
One event can drive many reactions. A single 'order placed' event can update inventory, trigger a receipt, and warm a cache, with each consumer added independently of the others.
Absorbing spikes
A queue or log sits between producer and consumer, so a burst of work is buffered rather than dropped. Consumers drain at their own rate instead of the producer overwhelming a downstream system.
At Bauer Media we moved 34 sites onto Next.js with an event-driven backend using RabbitMQ, ElasticSearch, and GraphQL. Content changes were published as events, and the systems that needed to react (search indexing, cache invalidation, the sites themselves) consumed them independently. No single component had to know the full list of everything that cared about a content update, which is what let that many sites share a backend without each new consumer becoming a change to the publisher. The Bauer Media case study goes into the detail.
The costs events bring with them
Asynchronous communication is harder to reason about than a function call, and the difficulty is not evenly spread. Ordering is the first surprise: events can arrive out of sequence, so any consumer that assumes a strict order needs to handle that explicitly. Then there is idempotency. Most messaging systems deliver at least once, which means the same event can arrive twice, so every consumer has to be safe to run more than once on the same message. Debugging is genuinely harder, because a single business action becomes a chain of events across components with no call stack tying them together, which is why distributed tracing stops being optional.
The deepest cost is eventual consistency. Once parts react to events on their own time, there is a window where the system is internally inconsistent: the order exists but the inventory has not yet been decremented. For some workflows that window is harmless. For others it is a correctness bug or a compliance problem, and you have to design around it rather than pretend it is not there. That is a real tradeoff, not a detail to wave away.
Before you make something event-driven, you owe it
- Idempotent consumers, because at-least-once delivery means duplicates will happen.
- A plan for out-of-order events, even if the plan is to enforce ordering at a cost.
- A dead-letter path for messages that keep failing, so a poison message does not stall a queue.
- Distributed tracing, so one business action can be followed across consumers.
- An explicit answer to where eventual consistency is acceptable and where it is not.
How they compose
Because they answer different questions, they fit together rather than compete. The common pattern is microservices that publish events for the things other services need to react to, while still making direct synchronous calls where a request genuinely needs an immediate answer. The Motive Partners work used both: events drove the parts of the platform migration where one change needed several downstream reactions, and the migration ran on BullMQ as the job and message backbone, which is what carried the system through the work that led to the sale to NCR. Where a request needed a value back right now, a direct call was still the right tool.
The mistake is treating events as the default for everything. If a caller needs the result before it can continue, asking it to publish an event and wait for a reply event is a synchronous call wearing a costume: you have taken on ordering, idempotency, and a correlation mechanism to rebuild something a plain request-response gives you for free. Use events when a producer should not have to know who reacts, or when reactions can happen later. Use a synchronous call when the caller needs the answer to proceed. Most production backends are a mix, and the skill is knowing which interaction is which. Picking that split well is a large part of what we do in platform and backend engineering.
When to adopt each, and when to keep a monolith
Both splits earn their cost in specific conditions, and neither is a starting point you reach for by default. The honest answer for a lot of systems is that you need neither yet.
Adopt microservices when you have multiple teams that genuinely need to ship independently, when parts of the system have sharply different scaling or security needs, or when one component changes far more often than the rest. Adopt event-driven communication when producers should not be coupled to consumers, when one change needs to fan out to several reactions, or when you need to absorb spikes that a synchronous call would drop. Notice that neither trigger is "the codebase feels big".
For most teams starting out, a modular monolith is the better call. Keep one deployable, but enforce clean module boundaries inside it, with modules talking through well-defined interfaces rather than reaching into each other's internals. You get most of the design benefit of clear separation without the network cost, the distributed-debugging tax, or the eventual-consistency burden. When a module starts to need its own deployment cadence or scaling profile, you already have the seam to extract it into a service, and you can introduce events on exactly the boundary that needs them. Splitting early, before the boundaries have proven themselves, almost always means redrawing them later across a network, which is the expensive version of a decision you could have made in code.
A decision guide
Hold the two questions apart and most of the confusion goes away. Decide how to split the system first, then decide how the parts should communicate, and let the costs above tell you when each is worth paying.
How to decide
- Split into services for team independence, divergent scaling, or differing change rates. Not for size alone.
- Use events when a producer should not know its consumers, when one change fans out, or when you need to absorb spikes.
- Keep synchronous calls for simple request-response, where the caller needs the answer to continue.
- Compose them: services that publish events where reactions are independent, and call directly where they are not.
- Start with a modular monolith and clean boundaries. Extract a service, and add events on that seam, when a part earns it.
- If you adopt events, pay the idempotency, ordering, tracing, and eventual-consistency costs up front, not after an incident.
The teams that get this right are not the ones with the most services or the most events. They are the ones who can still explain, a year later, why each boundary exists and why each interaction is asynchronous or not. If you want a second opinion on where your own system should be split and where events actually pay off, that is the kind of question we work through in platform and backend engineering.