Event Driven Architecture: Powerful Pattern or Distributed Mess

by Arif Ikhsanudin, Backend Developer

When It Works

Event-driven architecture (EDA) is a pattern where system components communicate by producing and consuming events rather than by direct calls. A service publishes OrderPlaced to an event bus. Inventory, billing, and notification services each subscribe to that event and react independently. No service calls another directly.

This pattern genuinely shines in specific conditions: when you have multiple independent consumers for the same state change, when those consumers have different reliability and scaling requirements, and when you want to add new consumers without modifying the producer.

E-commerce checkout is the textbook example: OrderPlaced triggers inventory reservation, payment charging, warehouse notification, customer email, analytics recording — all independently, all able to fail and retry without blocking each other. The producer does not need to know the consumers exist.

When It Becomes a Mess

EDA applied without discipline creates systems where behavior is impossible to trace. A request enters, events cascade through N services in a partially-ordered graph, and the final state emerges from an interaction that no single service fully understands. Debugging requires reconstructing event flows across service logs and distributed trace spans.

Three specific failure patterns:

Event chains. Service A publishes event 1. Service B consumes event 1 and publishes event 2. Service C consumes event 2 and publishes event 3. A failure in step 3 requires understanding the chain from step 1 to debug. The further the chain, the harder the debugging. Long event chains are often a sign that EDA is being used where an orchestrator (a process that explicitly sequences steps) would be clearer.

Implicit ordering dependencies. Events are processed asynchronously and in arbitrary order unless you design explicitly for ordering. A PaymentProcessed event consumed before the corresponding OrderCreated event is an edge case that will occur eventually at scale. Every consumer must handle out-of-order delivery, which means every consumer needs to query state rather than assume state from the event alone.

# Fragile: consumer assumes event arrives in order and trusts event data
def handle_payment_processed(event):
    order = event.data["order"]  # Assume this is current
    send_receipt(order)          # What if order was modified after payment?

# Robust: consumer fetches current state
def handle_payment_processed(event):
    order = db.get_order(event.data["order_id"])  # Fetch current state
    if order.status != "payment_complete":
        # Unexpected state -- handle it
        return
    send_receipt(order)

Schema evolution. An event schema change — renaming a field, changing a type — requires coordinating all consumers simultaneously or maintaining backward compatibility. With two consumers, this is manageable. With fifteen consumers, one of which is an analytics pipeline owned by a different team, this is a coordination problem that can block the producer from evolving.

The Choreography vs Orchestration Decision

EDA as described above is choreography — each service reacts independently, no central coordinator. The alternative is orchestration — a central process (a saga orchestrator, a workflow engine) explicitly sequences the steps and handles failures.

Choreography scales better and is more loosely coupled. Orchestration is easier to reason about, debug, and recover from failures. The choice depends on how complex the process is and how important traceability is.

For simple fan-out (one event, multiple independent consumers), choreography is cleaner. For multi-step processes with compensating transactions (place order → reserve inventory → charge payment → if payment fails, release inventory), orchestration with a tool like Temporal or AWS Step Functions is usually more appropriate than a chain of events with implicit compensation logic.

The Practical Test

Before adopting EDA for a new integration, ask: can I draw the complete event flow — every service that produces or consumes each event, and every state transition — in under 10 minutes? If you cannot, the system is already complex enough that adding more event-driven coupling will make it harder, not easier. Consider whether a simpler direct call or an explicit workflow would be clearer.

EDA is not inherently complex. Systems built with EDA without discipline are.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Singapore Backend Developers Are Expensive and Hard to Retain — The Remote Fix

You finally hired that senior backend engineer. Eight months later, they left for a bank offering 40% more. Now you're starting over.

Read more

Distributed Caching With Redis in Spring Boot — Beyond the Basics

Spring Boot's Redis cache integration works with minimal configuration. The decisions that matter — serialization format, key design, eviction policy, and how to handle cache-aside vs read-through patterns — require deliberate choices that affect correctness and performance under load.

Read more

Collectors, flatMap, and Reduce in Java Streams — The Operations That Take More Than a Minute to Learn

filter, map, and forEach are intuitive. The operations that unlock streams as a serious data processing tool — groupingBy, flatMap, reduce, and custom collectors — require more explanation. Here is that explanation.

Read more

Stop Mocking Things You Do Not Own

Mocking third-party libraries and external APIs directly in tests couples your test suite to the library's interface, not to your code's behavior. When the library changes, your tests break — even if your code still works correctly.

Read more