AR
AgentRuss
ECS diagram showing Entity as an ID, Components as data, and Systems as logic.
AgentRuss Guide

Entities vs Components vs Systems in ECS: What Goes Where?

Written by
AgentRuss
Published

Entities vs Components vs Systems in ECS: What Goes Where?

One of the first things you learn about an Entity Component System is usually something like this:

An Entity is an ID. A Component contains data. A System contains logic.

That definition is correct.

The problem is that it sounds much easier than it actually is.

Once you start building a real game, questions quickly appear:

  • Should an Inventory component contain methods for adding items?
  • Should a Health component know how to take damage?
  • Should an item execute its own effect?
  • Should input handling and player movement be one system?
  • Should one system open an inventory panel and also render everything inside it?
  • When should two systems communicate through an event instead of calling each other directly?

These decisions matter because they determine whether your ECS remains clean as the project grows or slowly turns into a collection of tightly coupled systems.

In this guide, we'll look at how I decide what belongs in an Entity, Component, or System, using practical examples from a JavaScript ECS game engine.


The Simple Rule

A useful starting point is:

JavaScript
1Entity = identity
2Component = data
3System = behaviour

Another way to think about it is:

JavaScript
1Entity
2"What is this thing?"
3
4Component
5"What does this thing have?"
6
7System
8"What happens to things that have these components?"

For example, imagine a player character.

The player might have:

JavaScript
1Transform
2Velocity
3Health
4Inventory
5Equipment
6PlayerControlled

None of those components need to know that they belong to a player.

They simply describe pieces of data associated with an entity.

Systems then look for combinations of those components and perform behaviour.


Entities: Identity Without Behaviour

In a traditional object-oriented game architecture, you might create something like:

JavaScript
1class Player {
2 constructor() {
3 this.health = 100
4 this.position = { x: 0, y: 0, z: 0 }
5 }
6
7 move() {}
8 attack() {}
9 takeDamage() {}
10 heal() {}
11}

The player owns both its data and its behaviour.

An ECS approaches this differently.

An entity might simply be:

JavaScript
1const player = world.createEntity()

That entity could internally just be an integer:

JavaScript
11

or:

JavaScript
142

The number itself does not contain any behaviour.

Instead, we compose the entity by attaching components.

For example:

JavaScript
1world.addComponent(player, new Transform())
2world.addComponent(player, new Velocity())
3world.addComponent(player, new Health(100))
4world.addComponent(player, new PlayerControlled())

The entity now behaves like a player because of the components attached to it and the systems that process those components.

That distinction is important.

The entity is not inherently a player.

It is an ID representing a collection of components.


Components: Describe What an Entity Has

Components should primarily contain data.

For example:

JavaScript
1class Health {
2 constructor(max = 100) {
3 this.current = max
4 this.max = max
5 }
6}

A Transform component might look something like:

JavaScript
1class Transform {
2 constructor(x = 0, y = 0, z = 0) {
3 this.position = { x, y, z }
4 }
5}

An Inventory component could contain:

JavaScript
1class Inventory {
2 constructor() {
3 this.items = []
4 }
5}

Notice what isn't inside these components.

We don't have:

JavaScript
1health.takeDamage()

or:

JavaScript
1inventory.usePotion()

or:

JavaScript
1transform.moveForward()

Those operations are behaviours.

In a clean ECS architecture, behaviour belongs in systems.


Why Keep Components Data-Only?

At first, putting logic directly into a component can feel convenient.

For example:

JavaScript
1class Health {
2 constructor(max = 100) {
3 this.current = max
4 this.max = max
5 }
6
7 takeDamage(amount) {
8 this.current -= amount
9 }
10}

That seems harmless.

But what happens later when damage needs to consider:

  • armour
  • defense stats
  • critical hits
  • shields
  • buffs
  • debuffs
  • resistances
  • invulnerability
  • death events
  • damage numbers
  • hit flashes
  • multiplayer authority

Suddenly:

JavaScript
1health.takeDamage(10)

is no longer enough.

The component starts accumulating gameplay rules.

Now your data container is becoming a gameplay object.

Instead, we can keep:

JavaScript
1Health

as data and let something like:

JavaScript
1DamageSystem

decide what damage actually means.

That system can inspect other components too.

For example:

JavaScript
1Health
2FinalStats
3Dead
4StatusEffects

The damage calculation can evolve without turning Health into a giant class.


Systems: Behaviour Over Matching Entities

Systems contain the logic of the game.

A MovementSystem might query entities that have:

JavaScript
1Transform
2Velocity

and update their positions.

Conceptually:

JavaScript
1for (const entity of entities) {
2 const transform = world.getComponent(entity, Transform)
3 const velocity = world.getComponent(entity, Velocity)
4
5 transform.position.x += velocity.x * deltaTime
6 transform.position.y += velocity.y * deltaTime
7 transform.position.z += velocity.z * deltaTime
8}

The important part is that the system does not care whether the entity is:

  • a player
  • an enemy
  • an NPC
  • a projectile
  • a moving platform

If it has the required components, the system can process it.

That is one of the major advantages of ECS.

Behaviour becomes based on capabilities, not class inheritance.


Think in Terms of Capabilities

Instead of asking:

Is this entity a Player?

an ECS often asks:

Does this entity have the components required for this behaviour?

For movement:

JavaScript
1Transform + Velocity

For player control:

JavaScript
1PlayerControlled + Input + Velocity

For combat:

JavaScript
1Health + FinalStats

For equipment:

JavaScript
1Equipment + Inventory

This makes entities extremely composable.

You don't need:

JavaScript
1Player
2Enemy
3FlyingEnemy
4BossEnemy
5ArmouredBossEnemy

with increasingly complicated inheritance trees.

Instead, behaviour emerges from component combinations.


Example: Player Input and Movement

This is a good example of where system boundaries become important.

You could create one giant system:

JavaScript
1PlayerSystem

that:

  • reads keyboard input
  • checks running
  • handles jumping
  • rotates the player
  • applies acceleration
  • changes velocity
  • moves the transform
  • resolves collisions
  • updates animations

It might work initially.

But now everything related to the player is coupled together.

A cleaner structure might be:

JavaScript
1InputSystem
2
3PlayerControllerSystem
4
5GravitySystem
6
7MovementSystem
8
9CollisionSystem

Each system answers a different question.

InputSystem

What buttons did the player press?

PlayerControllerSystem

What movement should those inputs produce?

GravitySystem

How should gravity affect velocity?

MovementSystem

How should velocity change position?

CollisionSystem

Is the resulting movement physically valid?

This separation means you can later reuse parts of the pipeline.

For example, an AI-controlled enemy doesn't need InputSystem.

It could have:

JavaScript
1AISystem
2
3Velocity
4
5MovementSystem

The MovementSystem doesn't care where the velocity came from.

That is exactly what we want.


Components Should Represent State, Not Actions

A useful test when designing a component is to look at its fields.

Good component fields tend to describe state:

JavaScript
1class Equipment {
2 constructor() {
3 this.weapon = null
4 this.head = null
5 this.body = null
6 this.accessory = null
7 }
8}

This tells us what is equipped.

It does not decide:

  • whether an item can be equipped
  • what stats the item provides
  • whether equipping should remove another item
  • whether the UI should update

Those are behaviours handled elsewhere.

Similarly:

JavaScript
1class Quickbar {
2 constructor() {
3 this.slots = []
4 }
5}

describes quickbar state.

A system decides what happens when slot 1 is pressed.


What About Marker Components?

Not every component needs complex data.

Sometimes a component exists simply to mark an entity as having a particular capability or role.

For example:

JavaScript
1class PlayerControlled {}

This component might contain no fields at all.

Its presence means:

This entity can be controlled by player input.

A system can query:

JavaScript
1PlayerControlled + Input + Velocity

Marker components are extremely useful because they let us describe behaviour without hard-coding entity types.

Other examples could include:

JavaScript
1Dead
2StaticBody
3DynamicBody
4Interactable

The presence of the component itself carries meaning.


When Should Something Become Its Own System?

This is where ECS design becomes more subjective.

There is no rule saying every tiny action needs a separate system.

Creating hundreds of microscopic systems can be just as difficult to maintain as creating giant systems.

A useful question is:

Does this logic represent a separate responsibility?

Consider using a separate system when the behaviour:

  1. Has a clearly different responsibility.
  2. Operates on a different set of components.
  3. Could be reused independently.
  4. Needs a specific place in the update order.
  5. Communicates naturally through state or events.
  6. Is likely to grow independently later.

Let's look at a practical example.


Why Have ItemUseRequestSystem and ItemUseSystem?

This is a question that can come up when looking at a separated ECS architecture.

Why not make one system that detects the button press and immediately uses the item?

You absolutely could for a very small project.

But separating them gives us two different responsibilities.

ItemUseRequestSystem

This system answers:

Did the player request to use something?

For example:

JavaScript
1Player presses quickbar slot 1
2
3EVT_USE_ITEM_REQUEST

The system translates input into intent.

It does not need to know exactly what the item will do.

ItemUseSystem

This system answers:

Is the requested item valid, and what does using it mean?

It might:

  • check whether the item exists
  • check the inventory amount
  • determine the item type
  • consume the item
  • emit a healing event
  • trigger another gameplay effect

Now the actual use logic is not tied to keyboard input.

That becomes valuable later.

An item could potentially be used by:

  • keyboard input
  • clicking the inventory
  • controller input
  • AI
  • a scripted event
  • multiplayer commands

All of those can eventually produce the same request.

The item system only cares about the request itself.


Input Is Not Gameplay Logic

This idea is worth emphasizing.

Input tells us what the player wants.

It should not necessarily perform the entire gameplay operation.

For example:

JavaScript
1F pressed

is input.

JavaScript
1Player requests melee attack

is gameplay intent.

JavaScript
1Attack is valid

is gameplay validation.

JavaScript
1Target receives 12 damage

is gameplay resolution.

Those are different stages.

Separating them makes the architecture easier to extend.


UI Systems Benefit From Separation Too

Another common question is:

Why separate InventoryToggleSystem from InventoryPanelUISystem if both affect the inventory UI?

Because they have different responsibilities.

One might determine:

JavaScript
1Should the inventory be open?

The other determines:

JavaScript
1What should the inventory currently display?

Those sound similar, but they are not the same job.

Imagine later adding:

  • controller navigation
  • inventory search
  • sorting
  • equipment comparison
  • item tooltips
  • drag and drop
  • crafting
  • vendor windows

A single InventoryUISystem responsible for everything could become enormous.

Separating visibility/state from rendering keeps each system understandable.


Use Events When Systems Shouldn't Know About Each Other

One of the easiest ways to accidentally recreate tightly coupled object-oriented code inside an ECS is to make systems call each other directly.

For example:

JavaScript
1damageSystem.damageNumberSystem.showNumber(...)

or:

JavaScript
1itemSystem.healSystem.heal(...)

Now one system needs a reference to another system.

That creates dependencies.

Instead, we can communicate through events.

For example:

JavaScript
1DamageSystem
2
3EVT_DAMAGE_RESOLVED
4
5DamageNumberUISystem
6HitFlashSystem
7HealthBarVisibility

The DamageSystem does not need to know how many systems respond to the event.

It simply announces:

Damage happened.

Other systems can react independently.

This makes adding new behaviour much easier.


A Damage Example

Imagine an attack hits an enemy.

Instead of:

JavaScript
1AttackSystem
2 ├─ reduce health
3 ├─ update health bar
4 ├─ display damage number
5 ├─ flash enemy red
6 ├─ play sound
7 ├─ check death
8 └─ spawn loot

we can build a flow more like:

JavaScript
1Attack
2
3Damage Request
4
5DamageSystem
6
7Damage Resolved Event
8 ├─ DamageNumberUISystem
9 ├─ HitFlashSystem
10 └─ HealthBarVisibility
11
12Health reaches zero
13
14DeathSystem

Each system owns one part of the process.

Later, adding a combat log does not require modifying DamageSystem.

We simply add another listener.


Don't Split Systems Just Because You Can

There is a danger in the opposite direction too.

You don't need:

JavaScript
1ReadWKeySystem
2ReadAKeySystem
3ReadSKeySystem
4ReadDKeySystem
5CalculateForwardSystem
6CalculateRightSystem
7ApplyForwardSystem
8ApplyRightSystem

That is technically separated, but not necessarily useful.

The goal is not maximum system count.

The goal is clear responsibilities.

A good system should usually be easy to describe in one sentence.

For example:

JavaScript
1InputSystem
2Captures player input.
3
4StatsSystem
5Calculates final stats from base stats and modifiers.
6
7DamageSystem
8Resolves damage against an entity.
9
10TargetingSystem
11Determines the currently selected target.
12
13RespawnSystem
14Restores dead entities at their respawn point.

If describing a system requires a paragraph full of unrelated responsibilities, it may be doing too much.


System Order Matters

Once behaviour is split across systems, update order becomes important.

Imagine:

JavaScript
1PlayerControllerSystem
2MovementSystem
3GravitySystem

versus:

JavaScript
1PlayerControllerSystem
2GravitySystem
3MovementSystem

Those can produce different results.

If gravity modifies velocity, it probably needs to happen before movement applies velocity to the transform.

Similarly, collision usually needs to happen after movement has produced something to resolve.

A simplified pipeline might be:

JavaScript
1Input
2
3Gameplay Requests
4
5Player Controller
6
7Gravity
8
9Movement
10
11Collision
12
13Camera
14
15Combat / Stats
16
17UI
18
19Render

This is another reason separate systems can be useful.

Their order becomes explicit.


Where Should Game Rules Live?

A good rule is:

If it describes what something currently is, it probably belongs in a component.
If it describes what should happen, it probably belongs in a system.

For example:

Component

JavaScript
1class BaseStats {
2 constructor() {
3 this.attack = 5
4 this.defense = 2
5 }
6}

Component

JavaScript
1class FinalStats {
2 constructor() {
3 this.attack = 0
4 this.defense = 0
5 }
6}

System

A StatsSystem can:

  1. Copy base stats.
  2. Look at equipped items.
  3. Apply modifiers.
  4. Store the result in FinalStats.

The components contain numbers.

The system contains the rule for calculating those numbers.


What About Item Definitions?

Not everything in an ECS game has to be a component.

Static definitions can live in databases or configuration objects.

For example:

JavaScript
1const ItemDatabase = {
2 bronzeSword: {
3 name: 'Bronze Sword',
4 modifiers: {
5 attack: 5,
6 },
7 },
8}

This does not represent the mutable state of one entity.

It describes a reusable item definition.

An Equipment component might store:

JavaScript
1equipment.weapon = 'bronzeSword'

Then StatsSystem looks up the definition and applies:

JavaScript
1attack +5

This keeps configuration separate from runtime state.


A Useful Decision Checklist

When you're unsure where something belongs, ask these questions.

Is it identity?

Use an Entity.

Example:

JavaScript
1Entity 42

Is it state or data?

Use a Component.

Examples:

JavaScript
1Health.current
2Transform.position
3Inventory.items
4Equipment.weapon
5Velocity.x

Is it behaviour or a game rule?

Use a System.

Examples:

JavaScript
1Apply damage
2Calculate final stats
3Move entities
4Resolve collisions
5Find targets
6Respawn dead entities

Is it static reusable configuration?

Use a definition/database/config object.

Examples:

JavaScript
1ItemDatabase
2Ability definitions
3Enemy archetypes
4Loot tables

Is it something that happened?

Consider an Event.

Examples:

JavaScript
1Damage resolved
2Item picked up
3Stats changed
4Interaction requested
5Item use requested

That simple checklist resolves a surprising number of architecture questions.


A Practical Example: Poison

Suppose we want to add poison.

A less modular approach might add this to Health:

JavaScript
1health.applyPoison()
2health.updatePoison()
3health.removePoison()

Now Health needs to understand status effects.

Instead, we might have:

JavaScript
1Health
2StatusEffects

The StatusEffects component stores data such as:

JavaScript
1type
2durationRemaining
3tickInterval
4damage

A system processes those effects:

JavaScript
1StatusEffectSystem

When poison ticks, it can generate a damage request.

Then the existing damage pipeline handles the actual damage.

That means poison does not need its own separate version of:

  • defense handling
  • damage numbers
  • death detection
  • hit reactions

It reuses systems that already exist.

This is one of the biggest architectural benefits of ECS.


The Real Goal: Composability

The purpose of ECS isn't just to avoid classes.

It is to make behaviour composable.

Imagine an entity with:

JavaScript
1Transform
2Health
3Collider

Add:

JavaScript
1Velocity

and now it can move.

Add:

JavaScript
1Interactable

and now the player can interact with it.

Add:

JavaScript
1HealthBarVisibility

and it can participate in the health-bar UI.

Add:

JavaScript
1Dead

and death-related systems can react to it.

You are building behaviour from combinations rather than creating a new inheritance hierarchy every time something changes.


Don't Design the Perfect ECS Up Front

One final lesson is that system boundaries will evolve.

Sometimes two responsibilities look separate but turn out to belong together.

Other times a simple system grows until splitting it becomes obvious.

That is normal.

You do not need to predict the final architecture before writing the game.

A useful approach is:

  1. Start with a clear responsibility.
  2. Keep components data-focused.
  3. Avoid unnecessary direct dependencies.
  4. Split systems when responsibilities clearly diverge.
  5. Use events when several systems need to react to the same gameplay outcome.
  6. Refactor when the architecture tells you it is becoming uncomfortable.

ECS is not about following rigid rules.

It is about making change easier.


Final Mental Model

When designing something new, I usually come back to this:

JavaScript
1ENTITY
2Who is it?
3
4COMPONENT
5What does it have?
6
7SYSTEM
8What happens?
9
10EVENT
11What happened?
12
13DEFINITION
14What is this type of thing configured to be?

For a sword attack, that might become:

JavaScript
1Entity
2Player
3
4Components
5Transform
6Equipment
7FinalStats
8
9Definition
10Bronze Sword
11+5 Attack
12
13Systems
14AttackSystem
15DamageSystem
16
17Events
18Attack Requested
19Damage Resolved

Each piece has a clear responsibility.

And as the project grows, that clarity becomes increasingly valuable.


Conclusion

The basic ECS rule is easy to remember:

JavaScript
1Entities are IDs.
2Components are data.
3Systems are logic.

The harder part is learning where to draw the boundaries.

A good ECS architecture doesn't try to place every tiny behaviour into its own system. Instead, it aims for systems with clear responsibilities, components that describe state, and communication patterns that avoid unnecessary coupling.

When you're deciding where new functionality belongs, ask:

Is this data, behaviour, identity, configuration, or something that happened?

That question will usually point you in the right direction.

Once those responsibilities are separated cleanly, adding features such as abilities, equipment, status effects, AI, multiplayer, and more complex combat becomes much easier because you're extending existing pipelines instead of rewriting them.