Knowledge Guide
HomeSystem DesignMicroservices Patterns

Event-Driven Architecture Pattern An Example

The producer hands each event to a bus that looks up exactly one channel by the event's type, and that channel drops the event onto its own in-memory queue so a background worker delivers it to subscribers — decoupling the producer's thread from the consumers'. Those two moves — type-based routing and asynchronous hand-off — are what make this code event-driven rather than a chain of method calls. We build a traffic-management example in Java: sensors produce VehicleEvents, a SimpleEventBus routes them, per-type channels queue them, and TrafficLight consumers react.

The event

An event is an immutable record of something that already happened. It carries a type (the routing key the bus keys on) and a payload.

public final class VehicleEvent {
    private final String vehicleId;
    private final String type;      // "entering" or "leaving" — the routing key

    public VehicleEvent(String vehicleId, String type) {
        this.vehicleId = vehicleId;
        this.type = type;
    }

    public String getVehicleId() { return vehicleId; }
    public String getType()      { return type; }
}

The contracts and the producer

Three small interfaces pin down the roles. Note that the bus routes by type, and a channel is bound to one topic — this is what 064/065 called a Datatype Channel.

public interface EventBus {
    void publish(VehicleEvent event);
    void register(EventChannel channel);
}

public interface EventChannel {
    String topic();                       // the single event type this channel carries
    void publish(VehicleEvent event);     // enqueue for asynchronous delivery
    void subscribe(EventConsumer consumer);
}

public interface EventConsumer {
    void onEvent(VehicleEvent event);
}

// A sensor at a signal. It knows the bus, nothing about consumers.
public class Sensor {
    private final EventBus bus;
    public Sensor(EventBus bus) { this.bus = bus; }

    public void vehicleDetected(String vehicleId, String action) {
        bus.publish(new VehicleEvent(vehicleId, action));
    }
}

Why the naive version is wrong

The commonly-posted version of this example has two defects that quietly cancel the architecture it claims to demonstrate.

  1. It broadcasts to every channel. Its publish did for (EventChannel c : channels) c.add(event); — every event lands in every channel. That erases the type→channel routing 064/065 taught: a "leaving"-only channel now also receives "entering" events, so every consumer must re-filter by type, re-coupling all consumers to all event types. Routing is the whole point of a channel; broadcasting removes it.
  2. It is fully synchronous and in-process. Its channel's add called consumer.onEvent(event) directly, so the entire pipeline ran on the sensor's thread. There is no temporal decoupling — the defining property of EDA. A slow consumer blocks the sensor; a consumer that throws propagates straight back and can crash the producer. That is a plain method call wearing an EDA costume.

The versions below fix both: the bus resolves one channel by type, and the channel hands off through a queue to its own worker thread.

The bus: route by type, not broadcast

The bus is a lookup table from event type to channel. Publishing resolves exactly one channel (or none). Using a concurrent map lets multiple producer threads publish safely.

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SimpleEventBus implements EventBus {
    private final Map<String, EventChannel> channels = new ConcurrentHashMap<>();

    @Override
    public void register(EventChannel channel) {
        channels.put(channel.topic(), channel);   // one channel per event type
    }

    @Override
    public void publish(VehicleEvent event) {
        EventChannel channel = channels.get(event.getType());
        if (channel == null) {
            // No subscriber for this type. In production: dead-letter it.
            System.err.println("dropped unroutable event type=" + event.getType());
            return;
        }
        channel.publish(event);   // delivered to the matching channel ONLY
    }
}

The channel: hand off to a worker thread

This is where synchronous code becomes event-driven. publish only enqueues and returns immediately; a dedicated worker thread drains the queue and fans out to subscribers. The producer never waits on a consumer, and one failing consumer cannot kill the channel.

import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;

public class AsyncEventChannel implements EventChannel {
    private final String topic;
    private final List<EventConsumer> consumers = new CopyOnWriteArrayList<>();
    private final BlockingQueue<VehicleEvent> queue = new LinkedBlockingQueue<>();

    public AsyncEventChannel(String topic) {
        this.topic = topic;
        Thread worker = new Thread(this::drain, "channel-" + topic);
        worker.setDaemon(true);
        worker.start();
    }

    @Override public String topic() { return topic; }
    @Override public void subscribe(EventConsumer c) { consumers.add(c); }

    @Override
    public void publish(VehicleEvent event) {
        queue.offer(event);        // returns at once — producer thread is freed here
    }

    private void drain() {
        while (true) {
            VehicleEvent event;
            try {
                event = queue.take();   // blocks until an event is available
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                return;
            }
            for (EventConsumer c : consumers) {
                try {
                    c.onEvent(event);
                } catch (RuntimeException consumerError) {
                    // one bad consumer must not kill the worker or block the queue
                    System.err.println("consumer failed: " + consumerError);
                }
            }
        }
    }
}

The consumer and the wiring

The consumer just reacts. Wiring registers one channel per type and subscribes the traffic lights.

public class TrafficLight implements EventConsumer {
    @Override
    public void onEvent(VehicleEvent event) {
        if ("entering".equals(event.getType())) {
            System.out.println("Vehicle " + event.getVehicleId()
                + " is entering. Lengthening green.");
        } else if ("leaving".equals(event.getType())) {
            System.out.println("Vehicle " + event.getVehicleId()
                + " has left. Resetting timings.");
        }
    }
}

// --- bootstrap ---
EventChannel entering = new AsyncEventChannel("entering");
EventChannel leaving  = new AsyncEventChannel("leaving");
entering.subscribe(new TrafficLight());
leaving.subscribe(new TrafficLight());

SimpleEventBus bus = new SimpleEventBus();
bus.register(entering);
bus.register(leaving);

Sensor sensor = new Sensor(bus);
sensor.vehicleDetected("KA01-4321", "entering");
sensor.vehicleDetected("KA05-8890", "leaving");
sensor.vehicleDetected("KA09-1200", "exiting");   // unknown type
diagram
diagram

Worked trace

Three calls from the bootstrap above. Watch the Thread column: publish completes on main, but onEvent runs later on a channel worker — that gap is the decoupling. Watch routing: an event reaches exactly the channel whose topic() matches its type.

#CallThreadEffect
1vehicleDetected("KA01-4321","entering")mainbuilds VehicleEvent{KA01-4321, entering}
2bus.publish(evt)mainchannels.get("entering") → ch#1 (ch#2 not touched)
3ch1.publish(evt)mainqueue.offer(evt) returns immediately; main is free to continue
4queue.take() → onEvent(evt)channel-enteringprints "Vehicle KA01-4321 is entering. Lengthening green."
5vehicleDetected("KA05-8890","leaving")mainpublish → channels.get("leaving") → ch#2 only
6queue.take() → onEvent(evt)channel-leavingprints "Vehicle KA05-8890 has left. Resetting timings."
7vehicleDetected("KA09-1200","exiting")mainchannels.get("exiting") → null → dropped (dead-letter candidate)

Because steps 3 and 4 run on different threads, print order across channels is not guaranteed — events are ordered only within a single channel served by one worker. That non-determinism is a feature of async delivery, not a bug in the code.

Pitfalls

When to use it, when not

Reach for an event bus / async pub-sub when the shape of the problem is one-producer-to-many-reactors and the producer must not wait for the reaction.

Concrete signals that point here: multiple, independent consumers react to the same fact; the set of consumers grows over time and you want to add them without editing the producer; the producer's latency budget can't absorb the consumers' work; load is spiky and a queue can buffer it; consumers can tolerate eventual consistency.

Trade-offs versus a direct synchronous call (request/response). A direct call is the natural alternative: sensor.trafficLight.adjust(vehicle). You gain immediate results, a stack trace that spans the whole operation, trivial transactions, and simple ordering. You lose extensibility (every new reactor edits the caller) and isolation (the caller waits and inherits every failure). The event bus flips both: you gain decoupling, buffering and easy fan-out; you pay in indirection (three interfaces plus threads to trace), harder debugging (the effect happens on another thread, later), no end-to-end transaction, and only per-channel ordering.

Versus a Mediator/orchestrator. The bus here is a Broker-style topology (064): dumb pipe, smart endpoints, maximum scalability, minimum central control. If you instead need to coordinate a multi-step workflow with conditional routing, aggregation, or compensation, a Mediator/Saga orchestrator earns its central bottleneck by giving you one place that owns the flow.

Versus a real broker (Kafka/RabbitMQ). This in-memory channel is fine inside one process for teaching or for cheap intra-service fan-out. The moment you need durability, redelivery, cross-service delivery, or consumer groups, replace AsyncEventChannel with a broker — the interfaces stay; the transport changes.

Choose the event bus when reactors are many/unknown and the producer must stay fast and ignorant of them. Prefer a direct synchronous call when you need the result back, there is exactly one collaborator, and strong consistency matters more than extensibility.

Takeaways


Re-authored and deepened for this guide. Draws on Gregor Hohpe & Bobby Woolf, Enterprise Integration Patterns (Publish-Subscribe Channel, Datatype Channel, Dead Letter Channel); Martin Fowler, "What do you mean by Event-Driven?" (martinfowler.com); Chris Richardson, Microservices Patterns (event-driven collaboration); and Sam Newman, Building Microservices. Java concurrency (BlockingQueue, CopyOnWriteArrayList, ConcurrentHashMap) follows the java.util.concurrent documentation. Corrects the original page's broadcast-to-all-channels routing bug and its synchronous, in-process dispatch, aligning the example with the type-routed, asynchronous model taught in sections 064 and 065.

🤖 Don't fully get this? Learn it with Claude

Stuck on Event-Driven Architecture Pattern An Example? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Event-Driven Architecture Pattern An Example** (System Design) and want to truly understand it. Explain Event-Driven Architecture Pattern An Example from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Event-Driven Architecture Pattern An Example** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Event-Driven Architecture Pattern An Example** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Event-Driven Architecture Pattern An Example** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes