ECS in practice: EventBus, testability, and the bridge to enterprise

— 5 min read

ECS in practice: EventBus, testability, and the bridge to enterprise

Lire en français


In the first article, I laid out the foundations of the Entity Component System through a 4X space strategy game I’m building in the browser with PixiJS and TypeScript.

The starting point: OOP class hierarchies don’t hold up when hundreds of different entities interact every frame. ECS proposes a radical alternative: entities reduced to a simple ID, components that only carry data, and systems that contain all the logic. We saw how a 30-line VelocitySystem handles physics for the entire game, and how the BehaviorSystem plugs in AI behaviors without modifying the existing code.

But one question remained open: if systems are independent and don’t know about each other, how do they communicate? And more importantly: what does this pattern say to enterprise architects familiar with message brokers, event-driven architectures, and microservices?

That’s what we explore here.

In this article:

The EventBus

ECS, in theory, is three concepts: Entity, Component, System. Period. But in practice, as soon as the game outgrows its prototype stage, a question arises: if systems are independent and don’t know about each other, how do they communicate?

An EventBus is an internal communication channel. A system can emit an event (“this ship was just destroyed”) without knowing who’s listening. Other systems subscribe to events they care about and react. Nobody imports anybody. The contract is the event, not the module.

Without it, coupling sneaks in through the back door. If the CombatSystem has to set a “flag” component that the FactionSystem knows to read, they share implicit knowledge. The EventBus makes this contract explicit and typed.

In enterprise, it’s exactly the same reality: you can have services communicate through direct calls, but at some point you need a broker (Kafka, SNS, RabbitMQ). Except here, everything runs in a single process in the browser, bringing processing times down to the microsecond range, where a network broker is more in the millisecond or even tens of milliseconds range.

In its current state, the game defines about a hundred typed events. Here are two:

"entity:destroyed": {
  entityId: number;
  victimFaction: string;
  killerFaction: string;
  sectorId: string;
};

"trade:completed": {
  shipEntityId: number;
  stationEntityId: number;
  resourceId: string;
  qty: number;
  totalPrice: number;
  action: "buy" | "sell";
};

The CombatSystem emits entity:destroyed. The FactionSystem listens to this event to update diplomatic relations. The RenderBridge also listens to it to display an explosion. None of these modules directly import the others.

graph LR
  CS["CombatSystem"] -- "emit" --> EB{{"EventBus"}}
  EB -- "entity:destroyed" --> FS["FactionSystem"]
  EB -- "entity:destroyed" --> RB["RenderBridge"]

  style EB stroke-width:2px
Systems communicate through typed events without importing each other — like a Kafka broker, but in microseconds.

This is exactly the pattern found in enterprise architecture with message brokers (Kafka, SNS, RabbitMQ).

Pure rules

Calculation formulas (damage, pricing, physics) are extracted into pure functions, in a dedicated folder:

// movement.ts
export function applyThrust(vx, vy, rotation, acceleration, dt) {
  return {
    vx: vx + Math.cos(rotation) * acceleration * dt,
    vy: vy + Math.sin(rotation) * acceleration * dt,
  };
}

No dependencies, no side effects. Input → output. These functions are reusable and unit-testable without any setup.

In fact, all domain tests run without a browser. We instantiate a real World, a real EventBus, a real system. We emit an event, run an update, check the state. No mocks, no DOM.

ECS beyond this project

My implementation is a homemade TypeScript prototype. But the pattern is validated at much larger scale.

In gaming, the major engines are converging on data-oriented design:

  • Unity DOTS — optional ECS framework for high-performance scenarios
  • Unreal Engine 5 Mass Entity — data-oriented architecture for managing tens of thousands of autonomous agents
  • Bevy (Rust) — engine built entirely on ECS, with automatic system parallelization

Beyond gaming, the Gazebo robotics simulator uses an ECS to model autonomous agents. Its systems declare which phase of the cycle they run in (PreUpdate, Update, PostUpdate), a discipline found in most game engines.

Bridging to enterprise architecture

If you work on distributed systems, ECS will resonate with you. Here are the parallels I’ve identified:

Entity composition resembles service composition. In ECS, an entity is an ID with attached components. In microservices, a business domain is a set of services composed around a responsibility.

The in-process EventBus is a local message broker. Systems communicate through typed events. Replace “in-process” with “inter-service” and you have Kafka or SNS.

The data/logic separation naturally structures the flow. Components hold state, systems mutate it, queries read it. A read/write separation that echoes event-driven architectures.

Testability comes from the same principle. In ECS, each system is testable in isolation because it only depends on the World and the EventBus. In microservices, each service is testable in isolation because it only depends on its ports (API, message queue).

System independence enables selective scaling, exactly like service independence enables horizontal scaling: each part of the system evolves at its own pace.

What this changes for an architect

After a few weeks coding this game, I have a reinforced conviction: composition beats inheritance, in game dev just as in enterprise.

ECS enforces a design discipline. You can’t cheat with a quick inheritance shortcut. Every feature follows the same pattern: a component (the data), a system (the logic), events (the communication). It’s constraining at first, liberating afterwards.

This discipline produces readable code. A new developer opening the project understands VelocitySystem in 30 seconds. They don’t need to trace five levels of inheritance to understand where a behavior comes from.

And most importantly, it scales. Not just in performance (the game currently handles over 200,000 entities spread across 154 sectors without breaking a sweat), but in organizational complexity. Adding a new behavior doesn’t require understanding the existing codebase — just knowing which components to read and which events to emit.

This is exactly the promise of event-driven and microservice architectures. ECS let me experience it at a small scale, in one evening, in a browser.

Going further

Now that the foundations are laid, the next question: what happens when 200,000 entities start trading resources with no central orchestrator? The next article dives into emergent economics — and the parallels with distributed enterprise systems.