WierdX — Programming Reference All tutorials →
Developer reference · Practical tutorials · CS fundamentals
Software Engineering

Event-Driven Architecture: Callbacks, Pub/Sub, and Reactive Patterns

In a request-response system, the caller waits for the result. In an event-driven system, the caller publishes a fact — "an order was placed" — and any interested party responds independently. This inversion of control is what makes event-driven architecture appealing for loosely coupled systems, and what makes it harder to trace and debug.

Published June 30, 2026

Event-driven design appears at multiple scales: within a single process (callbacks, event emitters), between services in the same runtime (in-process pub/sub), and across distributed systems (message queues, event streaming platforms). The core idea is the same at every scale: producers emit events, consumers react to them, and neither party holds a direct reference to the other.

Callbacks

The simplest form of event-driven code is a callback: a function you pass to another function, to be called when something happens. JavaScript's DOM event API and most I/O libraries are built on this model.

// Browser DOM: register a callback for the click event
document.querySelector('#submit-btn').addEventListener('click', function(event) {
    event.preventDefault();
    console.log('Form submitted');
});

// Node.js: callback-based file read
const fs = require('fs');
fs.readFile('/etc/hosts', 'utf8', function(err, data) {
    if (err) { console.error(err); return; }
    console.log(data);
});

Callbacks are low-overhead and straightforward for a single async operation. They become unwieldy when multiple async steps depend on each other — the "callback pyramid" or "callback hell" problem — which is why Promises and async/await exist as higher-level abstractions over the same event-driven mechanism.

Event emitters

An event emitter is an object that maintains a list of listeners for named events. Any code that holds a reference to the emitter can register a listener or emit an event. Node.js's EventEmitter is the canonical example; Python's asyncio and most UI frameworks use equivalent mechanisms.

// Node.js EventEmitter
const EventEmitter = require('events');

class OrderProcessor extends EventEmitter {
    processOrder(order) {
        if (order.total > 500) {
            this.emit('large-order', order);
        }
        this.emit('order-processed', { id: order.id, status: 'done' });
    }
}

const processor = new OrderProcessor();

processor.on('order-processed', (data) => {
    console.log(`Order ${data.id} is ${data.status}`);
});

processor.on('large-order', (order) => {
    console.log(`High-value order flagged: $${order.total}`);
});

processor.processOrder({ id: 'ORD-001', total: 750 });

The emitter does not know how many listeners are registered, or what they do. Adding a new behavior — sending a notification for large orders — requires registering a new listener, not modifying processOrder. This satisfies the open/closed principle: the emitter is open for extension without modification.

Pub/Sub pattern

Pub/Sub (publish/subscribe) generalizes the emitter idea with a broker in between. Publishers send messages to named topics; subscribers register interest in topics. Neither knows about the other — they communicate only through the broker.

class EventBus:
    def __init__(self):
        self._subscribers = {}

    def subscribe(self, topic, handler):
        self._subscribers.setdefault(topic, []).append(handler)

    def publish(self, topic, payload):
        for handler in self._subscribers.get(topic, []):
            handler(payload)

bus = EventBus()

def send_confirmation_email(order):
    print(f"Sending email for order {order['id']}")

def update_inventory(order):
    print(f"Decrementing stock for order {order['id']}")

bus.subscribe('order.placed', send_confirmation_email)
bus.subscribe('order.placed', update_inventory)

bus.publish('order.placed', {'id': 'ORD-002', 'items': ['A', 'B']})

The in-process bus above is synchronous and single-threaded. Production pub/sub systems — Redis Pub/Sub, Google Cloud Pub/Sub, Apache Kafka — distribute messages across processes and machines, adding network latency but enabling consumers to run independently and at different scales.

Message queues vs event streams

Two common infrastructure patterns implement pub/sub at scale:

  • Message queues (RabbitMQ, SQS) deliver each message to exactly one consumer. The message is removed from the queue once acknowledged. This is correct for work distribution: N workers compete to process jobs.
  • Event streams (Kafka, Kinesis) retain events in an ordered, durable log. Multiple consumer groups can each read the entire log at their own offset. This is correct for fan-out: multiple services each need every event.

Choosing between them depends on whether every consumer needs every event (stream) or whether messages represent work to be done once (queue).

Trade-offs and challenges

Event-driven systems decouple components, which is both the appeal and the danger. The benefits are real: producers and consumers can be deployed, scaled, and updated independently. The costs are equally real:

  • Traceability. A single business operation may trigger a chain of events across several services. Tracing that chain requires correlation IDs propagated through every event and a distributed tracing system to reconstruct the flow.
  • Ordering. Events from the same producer may arrive out of order at a consumer, especially across distributed systems. Designing for out-of-order arrival is harder than designing for sequential processing.
  • At-least-once delivery. Most message systems guarantee delivery but not exactly-once: a consumer may receive the same event twice if it fails before acknowledging. Consumers must be idempotent — processing the same event twice must produce the same result as processing it once.

Event-driven architecture works best when components genuinely need to be decoupled and when the team is prepared to invest in observability tooling. For systems where services call each other directly and ordering matters, synchronous request-response may be simpler and easier to reason about.