
Entities vs Components vs Systems in ECS: What Goes Where?
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
Inventorycomponent contain methods for adding items? - Should a
Healthcomponent 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:
1Entity = identity2Component = data3System = behaviourAnother way to think about it is:
1Entity2"What is this thing?"3
4Component5"What does this thing have?"6
7System8"What happens to things that have these components?"For example, imagine a player character.
The player might have:
1Transform2Velocity3Health4Inventory5Equipment6PlayerControlledNone 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:
1class Player {2 constructor() {3 this.health = 1004 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:
1const player = world.createEntity()That entity could internally just be an integer:
11or:
142The number itself does not contain any behaviour.
Instead, we compose the entity by attaching components.
For example:
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:
1class Health {2 constructor(max = 100) {3 this.current = max4 this.max = max5 }6}A Transform component might look something like:
1class Transform {2 constructor(x = 0, y = 0, z = 0) {3 this.position = { x, y, z }4 }5}An Inventory component could contain:
1class Inventory {2 constructor() {3 this.items = []4 }5}Notice what isn't inside these components.
We don't have:
1health.takeDamage()or:
1inventory.usePotion()or:
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:
1class Health {2 constructor(max = 100) {3 this.current = max4 this.max = max5 }6
7 takeDamage(amount) {8 this.current -= amount9 }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:
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:
1Healthas data and let something like:
1DamageSystemdecide what damage actually means.
That system can inspect other components too.
For example:
1Health2FinalStats3Dead4StatusEffectsThe 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:
1Transform2Velocityand update their positions.
Conceptually:
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 * deltaTime6 transform.position.y += velocity.y * deltaTime7 transform.position.z += velocity.z * deltaTime8}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:
1Transform + VelocityFor player control:
1PlayerControlled + Input + VelocityFor combat:
1Health + FinalStatsFor equipment:
1Equipment + InventoryThis makes entities extremely composable.
You don't need:
1Player2Enemy3FlyingEnemy4BossEnemy5ArmouredBossEnemywith 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:
1PlayerSystemthat:
- 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:
1InputSystem2 ↓3PlayerControllerSystem4 ↓5GravitySystem6 ↓7MovementSystem8 ↓9CollisionSystemEach 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:
1AISystem2 ↓3Velocity4 ↓5MovementSystemThe 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:
1class Equipment {2 constructor() {3 this.weapon = null4 this.head = null5 this.body = null6 this.accessory = null7 }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:
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:
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:
1PlayerControlled + Input + VelocityMarker components are extremely useful because they let us describe behaviour without hard-coding entity types.
Other examples could include:
1Dead2StaticBody3DynamicBody4InteractableThe 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:
- Has a clearly different responsibility.
- Operates on a different set of components.
- Could be reused independently.
- Needs a specific place in the update order.
- Communicates naturally through state or events.
- 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:
1Player presses quickbar slot 12 ↓3EVT_USE_ITEM_REQUESTThe 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:
1F pressedis input.
1Player requests melee attackis gameplay intent.
1Attack is validis gameplay validation.
1Target receives 12 damageis 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:
1Should the inventory be open?The other determines:
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:
1damageSystem.damageNumberSystem.showNumber(...)or:
1itemSystem.healSystem.heal(...)Now one system needs a reference to another system.
That creates dependencies.
Instead, we can communicate through events.
For example:
1DamageSystem2 ↓3EVT_DAMAGE_RESOLVED4 ↓5DamageNumberUISystem6HitFlashSystem7HealthBarVisibilityThe 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:
1AttackSystem2 ├─ reduce health3 ├─ update health bar4 ├─ display damage number5 ├─ flash enemy red6 ├─ play sound7 ├─ check death8 └─ spawn lootwe can build a flow more like:
1Attack2 ↓3Damage Request4 ↓5DamageSystem6 ↓7Damage Resolved Event8 ├─ DamageNumberUISystem9 ├─ HitFlashSystem10 └─ HealthBarVisibility11 ↓12Health reaches zero13 ↓14DeathSystemEach 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:
1ReadWKeySystem2ReadAKeySystem3ReadSKeySystem4ReadDKeySystem5CalculateForwardSystem6CalculateRightSystem7ApplyForwardSystem8ApplyRightSystemThat 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:
1InputSystem2Captures player input.3
4StatsSystem5Calculates final stats from base stats and modifiers.6
7DamageSystem8Resolves damage against an entity.9
10TargetingSystem11Determines the currently selected target.12
13RespawnSystem14Restores 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:
1PlayerControllerSystem2MovementSystem3GravitySystemversus:
1PlayerControllerSystem2GravitySystem3MovementSystemThose 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:
1Input2↓3Gameplay Requests4↓5Player Controller6↓7Gravity8↓9Movement10↓11Collision12↓13Camera14↓15Combat / Stats16↓17UI18↓19RenderThis 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
1class BaseStats {2 constructor() {3 this.attack = 54 this.defense = 25 }6}Component
1class FinalStats {2 constructor() {3 this.attack = 04 this.defense = 05 }6}System
A StatsSystem can:
- Copy base stats.
- Look at equipped items.
- Apply modifiers.
- 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:
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:
1equipment.weapon = 'bronzeSword'Then StatsSystem looks up the definition and applies:
1attack +5This 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:
1Entity 42Is it state or data?
Use a Component.
Examples:
1Health.current2Transform.position3Inventory.items4Equipment.weapon5Velocity.xIs it behaviour or a game rule?
Use a System.
Examples:
1Apply damage2Calculate final stats3Move entities4Resolve collisions5Find targets6Respawn dead entitiesIs it static reusable configuration?
Use a definition/database/config object.
Examples:
1ItemDatabase2Ability definitions3Enemy archetypes4Loot tablesIs it something that happened?
Consider an Event.
Examples:
1Damage resolved2Item picked up3Stats changed4Interaction requested5Item use requestedThat 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:
1health.applyPoison()2health.updatePoison()3health.removePoison()Now Health needs to understand status effects.
Instead, we might have:
1Health2StatusEffectsThe StatusEffects component stores data such as:
1type2durationRemaining3tickInterval4damageA system processes those effects:
1StatusEffectSystemWhen 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:
1Transform2Health3ColliderAdd:
1Velocityand now it can move.
Add:
1Interactableand now the player can interact with it.
Add:
1HealthBarVisibilityand it can participate in the health-bar UI.
Add:
1Deadand 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:
- Start with a clear responsibility.
- Keep components data-focused.
- Avoid unnecessary direct dependencies.
- Split systems when responsibilities clearly diverge.
- Use events when several systems need to react to the same gameplay outcome.
- 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:
1ENTITY2Who is it?3
4COMPONENT5What does it have?6
7SYSTEM8What happens?9
10EVENT11What happened?12
13DEFINITION14What is this type of thing configured to be?For a sword attack, that might become:
1Entity2Player3
4Components5Transform6Equipment7FinalStats8
9Definition10Bronze Sword11+5 Attack12
13Systems14AttackSystem15DamageSystem16
17Events18Attack Requested19Damage ResolvedEach piece has a clear responsibility.
And as the project grows, that clarity becomes increasingly valuable.
Conclusion
The basic ECS rule is easy to remember:
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.

Learn how to decide whether game logic belongs in an Entity, Component, or System when building an Entity Component System in JavaScript.