Skip to main content

Stop Using Event Buses

Why Declarative Dependency Graphs Win

Updated 15 min readMarkdown
Part of seriesUnderstanding .me
Stop Using Event Buses

Stop Using Event Buses: Why Declarative Dependency Graphs Win

Event buses make it easy to connect components. Dependency graphs make it easier to understand what those connections mean.

Event-driven architectures are everywhere in JavaScript.

You emit an event. Something listens. Another component reacts. The system works.

Until it doesn’t.

As applications grow, event listeners accumulate, state updates trigger unexpected cascades, and debugging becomes an exercise in reconstructing invisible relationships between components.

The problem isn’t necessarily the event bus itself.

The problem begins when events become the primary mechanism for maintaining relationships between application state.

What if those relationships were declared instead of manually coordinated?

That’s the idea behind declarative dependency graphs.

Let’s explore why they matter, how they compare to traditional event buses, and how a reactive kernel can automatically maintain derived state without requiring developers to orchestrate every update.

What Is the Problem With Event Buses in JavaScript?

An event bus provides a communication mechanism between otherwise independent components.

A typical implementation looks like this:

const bus = new EventTarget();

bus.addEventListener("priceChanged", (event) => {
  updateSubtotal(event.detail);
});

bus.addEventListener("subtotalChanged", (event) => {
  updateTax(event.detail);
});

bus.addEventListener("taxChanged", (event) => {
  updateTotal(event.detail);
});

Each listener performs a specific operation.

At first glance, the architecture appears modular. But consider what happens when the application grows. The total depends on the tax. The tax depends on the subtotal. The subtotal depends on the price and quantity.

Those dependencies exist, but they’re scattered across event handlers.

The event bus knows which listeners receive an event.

It doesn’t necessarily know why one value depends on another.

That distinction matters.

Event delivery is not the same thing as dependency management.

An event bus answers:

Who should receive this event?

A dependency graph answers:

Which values depend on this state, and what must be recomputed when it changes?

These are different architectural responsibilities.

Why Do Event-Driven Applications Become Difficult to Debug?

Consider a simple shopping cart.

bus.addEventListener("priceChanged", updateSubtotal);
bus.addEventListener("quantityChanged", updateSubtotal);

bus.addEventListener("subtotalChanged", updateTax);
bus.addEventListener("subtotalChanged", updateDiscount);

bus.addEventListener("taxChanged", updateTotal);
bus.addEventListener("discountChanged", updateTotal);

Now imagine that updateSubtotal() emits another event.

function updateSubtotal() {
  const subtotal = price * quantity;

  bus.dispatchEvent(
    new CustomEvent("subtotalChanged", {
      detail: subtotal
    })
  );
}

A single price update can trigger several handlers.

Some may execute synchronously.

Others may schedule asynchronous operations.

Some may produce the same values they already held.

And multiple branches may eventually update the same destination.

For example:

price
  |
  v
subtotal
  |       |
  v       v
 tax   discount
  |       |
  +---+---+
      |
      v
     total

This is a dependency graph.

But the application is implementing it indirectly through events.

When something goes wrong, developers must reconstruct the graph from the code.

Which listener triggered the update?

Which handler emitted the next event?

Why was the total calculated twice?

What happens when one handler completes before another?

The event bus doesn’t automatically provide those answers.

The relationships are implicit.

A declarative dependency graph makes those relationships explicit.

How Do Declarative Dependency Graphs Work?

Instead of describing what should happen after an event, a declarative system describes how values depend on other values.

Consider:

const subtotal = price * quantity;
const tax = subtotal * taxRate;
const total = subtotal + tax;

These expressions define a mathematical relationship.

If price changes, subtotal must be reevaluated.

If subtotal changes, tax and total may need reevaluation.

The developer shouldn’t need to manually emit a sequence of events to maintain those relationships.

A reactive dependency graph can represent them directly.

price ─────┐
           ├── subtotal ──┬── tax
quantity ──┘              │    │
                          └────┴── total

The graph captures the relationships between values.

When an input changes, the runtime can identify the affected computations and update them in dependency order.

This is the fundamental idea behind fine-grained reactivity.

Rather than coordinating updates through event names, the application declares dependencies and lets the runtime maintain them.

How Can You Replace Manual Event Listeners With Reactive State?

One way to implement this model is through a path-based reactive kernel.

In .me, paths represent values and can participate in declarative expressions.

For example:

me.cart.price(100);
me.cart.quantity(2);
me.cart.taxRate(0.16);

These paths represent application state.

Now consider the relationships:

me.cart["="](
  "subtotal",
  "price * quantity"
);

me.cart["="](
  "tax",
  "subtotal * taxRate"
);

me.cart["="](
  "total",
  "subtotal + tax"
);

The expressions describe how derived values relate to their inputs.

Conceptually, the kernel maintains:

cart.price ───────┐
                  ├── cart.subtotal
cart.quantity ────┘         |
                            v
cart.taxRate ──────────── cart.tax
                            |
                            v
                        cart.total

The important difference is architectural.

We aren’t manually subscribing subtotal to a price event.

We aren’t manually emitting a tax event.

We aren’t writing an event handler to recalculate the total.

We’re declaring the relationships.

The reactive kernel can use those relationships to determine which derived values require recomputation.

This is the difference between imperative event coordination and declarative state dependencies.

What Happens When a Value Changes?

Suppose the quantity changes:

me.cart.quantity(3);

The dependency graph identifies the affected computations.

Conceptually:

quantity
   |
   v
subtotal
   |
   v
  tax
   |
   v
 total

Other independent paths don’t need to participate in this update.

For example:

cart.customer.name
cart.shipping.address
cart.preferences.currency
cart.history.previousOrders

None of these values depend on quantity.

A dependency-aware runtime has no reason to reevaluate them as part of this calculation.

This is where fine-grained reactivity becomes useful.

Instead of broadcasting a general state-change event and asking subscribers to determine whether they care, the kernel can use dependency information to identify affected computations.

However, there is an important distinction.

Fine-grained reactivity doesn’t mean every update is automatically O(k) under every possible implementation.

The actual cost depends on how dependencies are indexed, how affected nodes are discovered, how computations are scheduled, and how changes are propagated.

A useful performance model separates:

  • The cost of identifying affected dependencies.
  • The cost of evaluating affected expressions.
  • The cost of comparing previous and current values.
  • The cost of notifying external consumers.

The advantage of an explicit dependency graph is that these operations can be organized around the affected region rather than blindly processing unrelated application state.

How Do You Prevent Cascading Updates in Reactive Systems?

One of the most common problems in event-driven applications is redundant propagation.

Imagine a derived value:

const eligible = age >= 18;

The age changes from 25 to 26.

The input changed.

But the derived value didn’t.

Before:

age = 25
eligible = true

After:

age = 26
eligible = true

Should every downstream computation depending on eligible run again?

Not necessarily.

If downstream computations depend only on the boolean result, and that result remains unchanged, propagating the update may be unnecessary.

This optimization is commonly called equality-based propagation or early cutoff.

Conceptually:

function recompute(node) {
  const previous = node.value;
  const next = evaluate(node.expression);

  if (Object.is(previous, next)) {
    return;
  }

  node.value = next;
  propagateToDependents(node);
}

This is illustrative pseudocode, not the internal implementation of .me.

The principle is simple.

An input changing doesn’t necessarily mean every derived output changes.

In an event-driven architecture, developers often implement this behavior manually.

For example:

if (previousValue !== nextValue) {
  bus.dispatchEvent(
    new CustomEvent("eligibilityChanged")
  );
}

As applications grow, those checks become scattered throughout event handlers.

A reactive dependency system can centralize equality checks and propagation decisions.

That doesn’t eliminate every unnecessary computation.

The immediate dependent may still need to be evaluated to discover that its result is unchanged.

But it can prevent further propagation along that branch.

For large dependency graphs, avoiding downstream work can be significant.

Can Dependency Graphs Prevent JavaScript Memory Leaks?

Memory leaks are another common concern in event-driven applications.

Consider:

function mountComponent() {
  bus.addEventListener("dataChanged", updateUI);
}

If the component is repeatedly mounted without removing the listener, subscriptions may accumulate.

A safer implementation manages cleanup:

function mountComponent() {
  bus.addEventListener("dataChanged", updateUI);

  return () => {
    bus.removeEventListener("dataChanged", updateUI);
  };
}

This works.

The difficulty comes from maintaining subscription lifecycles across large applications.

Reactive dependency graphs can reduce the need for manually maintained listeners between derived values.

If a value depends on another value, that relationship belongs to the graph rather than an application-level event handler.

But dependency graphs don’t automatically eliminate memory leaks.

They still require lifecycle management.

A reactive runtime must handle questions such as:

  • What happens when a derived expression is replaced?

  • Are obsolete dependency edges removed?

  • Can unused nodes be garbage-collected?

  • How are external subscriptions disposed of?

  • What happens when a component stops observing a value?

The benefit isn’t that cleanup becomes unnecessary.

It’s that dependency lifecycle management can be centralized rather than duplicated across application code.

How Do You Debug a Reactive Dependency Graph?

One of the hardest questions in state management is deceptively simple:

Why does this value have its current value?

Traditional debugging often starts with logs.

console.log("price changed", price);
console.log("subtotal updated", subtotal);
console.log("tax calculated", tax);
console.log("total updated", total);

Logs tell us what happened at particular execution points.

But they don’t necessarily provide a structured explanation of the dependencies behind a value.

An explicit dependency graph offers another possibility.

In .me, the kernel exposes an introspection interface:

me.explain("cart.total");

The purpose of this interface is to inspect information associated with a derived path, including its expression and dependency metadata.

That makes it possible to investigate questions such as:

What expression defines this value?

Which paths does it depend on?

What other values participate in the calculation?

Which relationships should be examined when an input changes?

This is especially useful when the application contains hundreds or thousands of derived values.

Instead of searching for every event handler that might influence a result, developers can inspect the declared dependency structure.

It’s important to distinguish dependency introspection from a complete historical audit trail.

Explaining the current dependencies of a value isn’t necessarily the same as recording every mutation that occurred in the past.

A complete audit system also needs historical records, timestamps, identities, and appropriate persistence guarantees.

Nevertheless, explicit dependency metadata provides a useful foundation for understanding derived application state.

Are Dependency Graphs Faster Than Event Buses?

Not automatically.

Event buses can be extremely efficient at delivering notifications.

A direct event dispatch may be cheaper than maintaining a dependency graph for a trivial operation.

The performance question depends on the workload.

Consider two different systems.

System A: Event notifications

User clicked button
       |
       v
    Event bus
       |
       v
   Analytics handler

A dependency graph adds little value here.

The application simply needs to communicate that something happened.

System B: Derived application state

10,000 source values
         |
         v
Dependent calculations
         |
         v
Aggregations
         |
         v
Derived application state

Here, dependency tracking becomes more interesting.

If a small number of source values change, the system can identify which derived computations depend on them.

The performance advantage comes from avoiding unnecessary work, not from making individual function calls magically faster.

A meaningful benchmark should measure:

  1. Graph construction time.

  2. Memory overhead per node and dependency edge.

  3. Affected-node discovery time.

  4. Recomputation latency.

  5. Propagation behavior when derived values remain unchanged.

  6. Update throughput under different dependency structures.

Comparing only the execution time of emit() against a reactive state update would miss the architectural difference.

The relevant question is:

How much total work does the application perform to maintain consistent derived state?

When Should You Use an Event Bus Instead of a Dependency Graph?

Event buses aren’t inherently bad.

They solve a different problem.

An event bus is useful when communicating that something occurred.

Examples include:

bus.emit("userLoggedIn");
bus.emit("paymentCompleted");
bus.emit("notificationReceived");

These are events.

They represent occurrences in the system.

A dependency graph is useful when maintaining relationships between values.

Examples include:

total = subtotal + tax;
availableStock = inventory - reserved;
remainingFuel = fuel - estimatedConsumption;

These are derived relationships.

They describe what values mean relative to other values.

The distinction is important:

Events describe what happened. Dependencies describe what follows from the current state.

Many applications need both.

An event may update source state.

That state change may trigger a reactive dependency graph.

The graph may then update derived values.

An external notification can communicate the resulting transition to other systems.

These mechanisms can coexist without competing for the same responsibility.

How Do Declarative Dependency Graphs Scale to Complex Applications?

Consider a fleet management application.

Each vehicle contains state:

me.fleet.trucks[184].fuel.liters(312);
me.fleet.trucks[184].fuel.consumption(2.7);
me.fleet.trucks[184].engine.temperature(91);

The application also needs derived information.

For example, estimated remaining range:

me.fleet.trucks[184].fuel["="](
  "range",
  "liters * consumption"
);

Now imagine thousands of vehicles.

Each vehicle may have calculations for:

  • Fuel consumption.

  • Estimated range.

  • Maintenance status.

  • Route progress.

  • Operational alerts.

  • Fleet-level aggregations.

In an event-driven implementation, developers might coordinate these relationships using dozens of event types and subscription handlers.

In a declarative dependency system, those relationships can be represented directly.

When the fuel level of one truck changes, the kernel can identify the computations that depend on that value.

Other vehicles don’t need to participate unless a shared aggregation or cross-vehicle dependency requires it.

This is particularly useful for applications involving:

Real-time dashboards, logistics systems, financial calculations, interactive simulations, knowledge graphs, and other workloads with substantial derived state.

The architecture becomes more valuable as the number and complexity of relationships increase.

What Are the Trade-Offs of Declarative Dependency Graphs?

Dependency graphs introduce their own engineering challenges.

They aren’t a universal replacement for events.

A runtime must handle dependency cycles, scheduling order, dynamic dependencies, error propagation, and cleanup.

For example:

A depends on B
B depends on C
C depends on A

This creates a cycle.

A reactive kernel needs a defined policy for handling it.

Possible approaches include rejecting cycles, supporting explicit feedback mechanisms, or applying specialized evaluation strategies.

Dynamic dependencies introduce another challenge.

Consider:

result = condition ? valueA : valueB;

Depending on the runtime, the active dependency set may change when condition changes.

Maintaining accurate dependency information requires more than simply recording a static list of variables.

There are also questions about asynchronous computations, external side effects, transactions, and consistency guarantees.

These concerns are precisely why a dependency graph benefits from being managed by a dedicated runtime rather than reconstructed through application-specific event handlers.

How Can You Start Replacing Event Buses With Declarative Dependencies?

You don’t need to rewrite an entire application.

Start by identifying event handlers whose primary responsibility is recalculating state.

For example:

bus.on("priceChanged", updateSubtotal);
bus.on("quantityChanged", updateSubtotal);
bus.on("subtotalChanged", updateTax);
bus.on("taxChanged", updateTotal);

Ask a simple question:

Are these handlers communicating events, or maintaining mathematical relationships?

If they’re maintaining relationships, they may be candidates for declarative dependencies.

The migration process can be incremental.

First, identify the source values.

Next, define the derived expressions.

Then, make the dependency relationships explicit.

Finally, measure whether the new architecture reduces redundant calculations, manual subscriptions, and debugging complexity.

Keep events where they represent actual occurrences.

Use dependencies where the application needs to maintain derived state.

The objective isn’t to eliminate every event listener.

It’s to stop using event delivery as a substitute for dependency management.

Final Thoughts: Events Tell You What Happened. Dependencies Tell You Why Values Change.

Event buses are useful communication tools.

But when an application uses events to maintain thousands of relationships between values, the architecture can become difficult to reason about.

A declarative dependency graph changes the model.

Instead of manually coordinating updates, developers describe the relationships between values.

The runtime becomes responsible for tracking dependencies, scheduling recomputations, and propagating relevant changes.

This creates opportunities for fine-grained reactivity, early cutoff, structured introspection, and more predictable state management.

That’s the direction we’re exploring with .me, an open-source, path-based reactive kernel.

The underlying idea is simple:

Declare the relationships. Let the kernel maintain them.

And perhaps more importantly:

Stop asking which listeners should run. Start asking which values actually depend on what changed.


Explore .me

GitHub: https://github.com/neurons-me/.me

Documentation: https://neurons-me.github.io/.me/

Project: https://neurons.me


Frequently Asked Questions

What is the difference between an event bus and a dependency graph?

An event bus distributes notifications to registered listeners. A dependency graph represents relationships between values or computations, allowing a runtime to identify which derived values may need updating when an input changes.

How can I avoid event listener memory leaks in JavaScript?

Remove listeners when they are no longer needed, use lifecycle-aware subscription patterns, and avoid unnecessary manual subscriptions for derived state. Declarative dependency graphs can centralize dependency management, but they still require proper cleanup.

What is fine-grained reactivity?

https://neurons-me.github.io/.me/docs/Extreme-Fan-Out.html

Fine-grained reactivity tracks dependencies at a relatively precise level, allowing updates to target affected computations rather than reevaluating broad sections of application state.

How do you prevent cascading updates in reactive applications?

Use dependency-aware scheduling and equality checks. If a derived value is recomputed but remains unchanged, an early-cutoff mechanism can prevent unnecessary downstream propagation.

Can a dependency graph replace Redux, RxJS, or an event bus?

Not universally. Redux provides state management patterns, RxJS provides reactive stream composition, and event buses distribute notifications. Dependency graphs are particularly useful for maintaining derived relationships between values. These approaches can complement one another.

When should I use a declarative dependency graph?

Consider one when your application has many interconnected derived values, frequent state changes, redundant recalculations, or difficulty tracing how updates propagate through the system.

About the author

  • ○ universe → ⊙ identity → ● meaning

No comments yet. Be the first to comment.

Understanding .me

Part 11 of 14

A foundational series exploring the architecture, core axioms, and runtime primitives behind .me

Up nextBuilding Management System (BMS)A Reactive Approach to Smart Buildings