Introduction
In the first article, I wrote about why event-driven architecture should not be used everywhere.
The main idea was simple: events are powerful, but they are not magic. They are useful when something meaningful has happened and other parts of the system may need to react. But they can become overkill when the flow is really just a simple request-response interaction.
In this second part, I want to make the discussion more practical.
Let's say we already decided that event-driven design makes sense for a specific flow. The next question is not "Should we use events?" anymore.
The next question is: how do we use them without creating future pain?
Because event-driven systems can be elegant. They can also become very confusing. The difference is usually not the broker. It is the design around it.
A message broker like Apache Kafka does not automatically create good architecture. It only creates movement. And movement is not always progress. Sometimes it is just confusion traveling faster.
Orchestration vs Choreography in Microservices
One of the most important design choices in event-driven microservices is whether a process should be orchestrated or choreographed.
Choreography means each service reacts to events independently. Something happens. An event is published. One service reacts. It publishes another event. Another service reacts.
For example, an order is placed. The payment service reacts and authorizes payment. Then the inventory service reacts and reserves stock. Then the shipping service reacts and creates a shipment. Then the notification service reacts and sends an email.
This can work very well when the process is simple and each service clearly owns its own reaction. It is a little like birds moving together in the sky. There is no visible conductor, but the movement still makes sense.
That is the good side of choreography.
But choreography also has a shadow side. As the process grows, it can become hard to see who owns the full business flow. Who knows whether the order is really complete? Who retries a failed step? Who compensates if payment succeeds but inventory fails? Who explains the current status to customer support?
Somewhere in every architecture, there is always one person who becomes the human workflow engine. This person usually has access to logs, tribal knowledge, and strong coffee.
Orchestration takes a different approach.
In orchestration, one workflow, process manager, or coordinating service owns the flow. It may authorize payment, reserve inventory, create shipment, send confirmation, and handle compensation if something fails.
This is more explicit. There is a place where the process lives. There is a place to check status. There is a place to handle retries, failures, timeouts, and compensation.
It may feel less "pure" from a decoupling perspective, but it can be much easier to understand and operate.
When Choreography Makes Sense
Choreography is a good fit when reactions are independent, the process is simple, and no single component needs to own the full business outcome.
Good examples include notifications, analytics, audit events, search indexing, cache invalidation, or independent integrations.
In these cases, one service publishes a fact, and other services react if they care.
When Orchestration Makes Sense
Orchestration is usually safer when the process has ordering, status, compensation, approvals, timeouts, or customer-facing visibility.
A good question is: if this process gets stuck, who is responsible for knowing where and why?
If the answer is "everyone a little bit," that usually means "nobody clearly enough."
That may be a sign that orchestration is needed.
Good Event-Driven Patterns
If we decide that events are the right fit, we need to treat them seriously.
An event is not just a message. It is a contract. It is a small public statement from one part of the system to the rest of the platform.
Publish events only after the state has really changed. If we publish CustomerRegistered, then the customer should actually be registered. Not almost registered. Not "we are about to register the customer if the next three things go well." An event should not be a hope wearing a nice name.
Use the outbox pattern for reliability. A classic problem is when a service updates its database successfully, then tries to publish an event, and publishing fails. Now the database says one thing, but the event stream says another. The outbox pattern solves this: write the business change and the event record together in the same database transaction, then let another process publish the event from the outbox. Not magical - just a careful way to avoid the gap between "the thing happened" and "the world was told that the thing happened."
Build consumers to be idempotent. If the same event arrives twice, the consumer should not do the wrong thing twice. If PaymentCaptured is processed twice, we should not ship two products, send two invoices, or charge twice. Unless we are trying to speedrun bankruptcy.
Version your schemas. Events live longer than we think. Someone publishes an event today. Another team builds a consumer next month. A reporting system starts using it six months later. A data lake stores it forever, because data lakes are where old JSON goes to retire. Events need ownership, schemas, versioning, compatibility rules, and someone responsible for approving changes. Tools like Faust for Python stream processing help manage this, but the discipline has to come from the team.
Separate business events from technical events. OrderPlaced, ClaimApproved, and SubscriptionCancelled are business events. RowUpdated, CacheInvalidated, and FileWritten are technical events. Both can be useful, but they are not the same. Business events are usually better for communication between domains. Technical events are useful for replication, auditing, indexing, and infrastructure work.
Bad Event-Driven Patterns to Avoid
Event-driven architecture can make a system more flexible. It can also make it harder to understand.
Event-driven CRUD. This happens when every small database field change becomes a business event: CustomerNameUpdated, CustomerPhoneUpdated, CustomerAddressLine2Changed, and so on. Sometimes this level of detail is needed for audit, compliance, data synchronization, or CDC. But as business events, these can become noisy. Not every field update deserves to become part of the domain language.
Events as hidden commands. A service publishes OrderCreated, but what it really means is: "Payment service, please charge the customer. Inventory service, please reserve stock. Shipping service, please prepare shipment. And please all do it in the correct order while pretending this is loosely coupled." That is not really just an event anymore. That is a business process hiding inside an event name.
The distributed monolith over Kafka. This happens when services are technically separated but still tightly dependent. Service B must consume Service A's event immediately. Service C expects Service B to publish another event right after that. Service D breaks if the event is late. On paper, this looks asynchronous. In reality, it is a synchronous chain wearing an event-driven costume. A little hat, maybe. Some nice YAML. But still a chain.
Unclear ownership. Every event should have an owner. Someone should know what the event means, who publishes it, when it is published, what schema it follows, who consumes it, whether it can be replayed, and whether it contains sensitive data. If nobody owns the event, the event becomes platform folklore. People know it exists. People are afraid to change it. Nobody remembers why one field is called statusCode2. And so the field lives forever, like a small haunted column in the event stream.
A Practical Decision Guide
When choosing between APIs, events, orchestration, and choreography, start with the shape of the flow.
| If the flow is... | Usually consider... | Why |
|---|---|---|
| Asking for information now | Synchronous API or read model | The caller needs a clear answer |
| Asking something to happen | Command or API | The system must accept, reject, or process intent |
| Telling others something happened | Event | Independent consumers may react |
| A simple chain of independent reactions | Choreography | Each service can own its reaction |
| A complex business process | Orchestration | The flow needs ownership, status, ordering, or compensation |
This is not a perfect formula. Architecture does not usually give us perfect formulas. If it did, half of our meetings would disappear, and we would not know what to do with all the free time.
But the table helps move the discussion away from slogans and toward the actual shape of the problem.
The most useful questions are often simple: Does the caller need an answer now? Can consumers react independently? Is eventual consistency acceptable? Can we handle duplicate events? Do we have clear ownership of the event schema? If the process gets stuck, who is responsible for knowing where and why?
These questions do not remove judgment. They help us use judgment better.
A Better Rule Than "Use Events Everywhere"
A better platform rule is this: use the communication pattern that fits the flow.
That is not as catchy as "events everywhere." It probably will not impress anyone on a conference slide. But it is much more useful when teams actually need to build and operate systems.
A healthy platform should support event-driven architecture. It should make events easy to publish, consume, document, version, observe, and evolve safely. It should provide good patterns, templates, examples, libraries, and guardrails.
But it should not force every service and every flow into the same shape.
For simple queries, use request-response. For direct actions, use commands or APIs. For business facts, publish events. For independent reactions, use choreography. For complex business processes, use orchestration.
That kind of platform rule is more mature because it accepts reality. And reality, as usual, refuses to fit nicely into one diagram.
Read the Full Article
I wrote a longer version of this article with more detail on orchestration vs choreography, good event-driven patterns, anti-patterns, and platform decision rules.