
How to Build an Entity Component System (ECS) in JavaScript
If you're new to ECS, I recommend reading Understanding Entity Component Systems (ECS): A Beginner-Friendly Guide first. If you already understand the basics but are unsure where responsibilities belong, see Entities vs Components vs Systems in ECS: What Goes Where?.
In this guide, we're going to build a small but functional Entity Component System in plain JavaScript.
The goal is not to create the fastest ECS ever made. Instead, we want an implementation that clearly demonstrates the core architecture:
- entities are IDs
- components contain data
- systems contain behaviour
- queries find entities with the required components
- the world coordinates everything
By the end, we'll have multiple entities moving through the same MovementSystem without needing Player, Enemy, or GameObject classes.
What We're Building
Our minimal ECS will contain five main pieces:
- World — owns entities, components, and systems
- Entity — a unique numeric ID
- Component — data attached to an entity
- Query — finds entities with a required set of components
- System — runs logic on matching entities
The flow looks like this:
1World2 ├── Entities3 ├── Components4 └── Systems5 │6 └── Query matching entities7 │8 └── Update component dataFor our example, an entity with both Transform and Velocity will automatically be processed by MovementSystem.
Project Structure
A simple folder structure could look like this:
1src/2├── ecs/3│ ├── Component.js4│ ├── System.js5│ └── World.js6│7├── components/8│ ├── Transform.js9│ └── Velocity.js10│11├── systems/12│ └── MovementSystem.js13│14└── main.jsThis is deliberately small.
As an engine grows, you might later add things such as Query.js, EventBus.js, InputSystem.js, DamageSystem.js, collision systems, rendering systems, and many more components.
For now, we only need enough structure to understand the pattern.
1. Create a Base Component
Create src/ecs/Component.js:
1export class Component {}That's all we need.
The base class gives our components a common structure, but it contains no gameplay logic.
A component's job is to store state.
For example, a Health component might eventually contain current and max values, while a Transform component stores position.
The important part is that components do not decide what should happen to that data.
2. Create a Base System
Create src/ecs/System.js:
1export class System {2 constructor(world) {3 this.world = world4 }5
6 update(deltaTime) {}7}Every system receives a reference to the World.
That lets systems query entities and retrieve their components.
Each concrete system will override update(deltaTime) with its own behaviour.
For example:
MovementSystemchanges positionsDamageSystemresolves damageStatsSystemcalculates final statsRenderSystemdraws the scene
3. Build the World
The World is the central container for our ECS.
Create src/ecs/World.js and start with:
1export class World {2 constructor() {3 this.nextEntityId = 14 this.entities = new Set()5 this.componentStores = new Map()6 this.systems = []7 }8}Each property has a specific job.
nextEntityId gives every new entity a unique ID.
entities tracks which entities currently exist.
componentStores contains component data grouped by component type.
systems contains the systems that should run each update.
The component storage will conceptually look something like this:
1Transform2 Entity 1 -> Transform data3 Entity 2 -> Transform data4 Entity 5 -> Transform data5
6Velocity7 Entity 1 -> Velocity data8 Entity 5 -> Velocity dataNotice that the entity itself does not own those components directly.
The world manages the relationship between entity IDs and component data.
4. Create and Destroy Entities
Add these methods to World:
1createEntity() {2 const entity = this.nextEntityId++3
4 this.entities.add(entity)5
6 return entity7}8
9destroyEntity(entity) {10 if (!this.entities.has(entity)) {11 return false12 }13
14 for (const store of this.componentStores.values()) {15 store.delete(entity)16 }17
18 this.entities.delete(entity)19
20 return true21}Now we can create an entity with:
1const player = world.createEntity()If it is the first entity, player will simply contain the number 1.
There is no position, health, inventory, mesh, or movement behaviour inside that value.
It is only an identity.
When we destroy an entity, we remove both its ID and all component data associated with it.
5. Add Component Storage
Now the world needs methods for attaching, retrieving, checking, and removing components.
Add these methods:
1addComponent(entity, component) {2 const componentType = component.constructor3
4 if (!this.componentStores.has(componentType)) {5 this.componentStores.set(componentType, new Map())6 }7
8 const store = this.componentStores.get(componentType)9
10 store.set(entity, component)11
12 return component13}14
15getComponent(entity, componentType) {16 const store = this.componentStores.get(componentType)17
18 if (!store) {19 return undefined20 }21
22 return store.get(entity)23}24
25hasComponent(entity, componentType) {26 const store = this.componentStores.get(componentType)27
28 return store?.has(entity) ?? false29}30
31removeComponent(entity, componentType) {32 const store = this.componentStores.get(componentType)33
34 if (!store) {35 return false36 }37
38 return store.delete(entity)39}The component class itself becomes the key in componentStores.
If we attach new Transform(), the Transform class identifies the store containing Transform components.
This lets us retrieve data later with:
world.getComponent(entity, Transform)
and check composition with:
world.hasComponent(entity, Transform)
6. Create Transform and Velocity Components
Now let's create some actual game data.
Create src/components/Transform.js:
1import { Component } from '../ecs/Component.js'2
3export class Transform extends Component {4 constructor(x = 0, y = 0, z = 0) {5 super()6
7 this.position = {8 x,9 y,10 z,11 }12 }13}Then create src/components/Velocity.js:
1import { Component } from '../ecs/Component.js'2
3export class Velocity extends Component {4 constructor(x = 0, y = 0, z = 0) {5 super()6
7 this.x = x8 this.y = y9 this.z = z10 }11}Both components contain data only.
Transform describes where an entity is.
Velocity describes how quickly it is moving.
Neither component contains a move() method.
Movement behaviour will belong to a system.
7. Compose Our First Entity
In main.js, create a world and an entity:
1import { World } from './ecs/World.js'2
3import { Transform } from './components/Transform.js'4import { Velocity } from './components/Velocity.js'5
6const world = new World()7
8const player = world.createEntity()9
10world.addComponent(11 player,12 new Transform(0, 0, 0),13)14
15world.addComponent(16 player,17 new Velocity(2, 0, 0),18)We can now think of the entity like this:
1Entity 12 ├── Transform3 │ position: 0, 0, 04 │5 └── Velocity6 x: 27 y: 08 z: 0The entity has no Player class.
It behaves like something that can move because it has the data required by a movement system.
That is the composition-based nature of ECS.
8. Add Queries
Systems need a way to find entities with particular combinations of components.
Add this method to World:
1query(componentTypes) {2 const results = []3
4 for (const entity of this.entities) {5 const matches = componentTypes.every(6 (componentType) =>7 this.hasComponent(entity, componentType),8 )9
10 if (matches) {11 results.push(entity)12 }13 }14
15 return results16}Now we can ask:
1const movingEntities = world.query([2 Transform,3 Velocity,4])That means:
Find every entity that has both Transform and Velocity.
Imagine the world contains:
1Entity 12 Transform3 Velocity4
5Entity 26 Transform7
8Entity 39 Transform10 Velocity11 HealthA query for Transform + Velocity would return Entity 1 and Entity 3.
Entity 2 would not match because it has no Velocity.
This is a key ECS concept: systems operate on capabilities rather than entity types.
9. Create MovementSystem
Now we can finally add behaviour.
Create src/systems/MovementSystem.js:
1import { System } from '../ecs/System.js'2
3import { Transform } from '../components/Transform.js'4import { Velocity } from '../components/Velocity.js'5
6export class MovementSystem extends System {7 update(deltaTime) {8 const entities = this.world.query([9 Transform,10 Velocity,11 ])12
13 for (const entity of entities) {14 const transform =15 this.world.getComponent(entity, Transform)16
17 const velocity =18 this.world.getComponent(entity, Velocity)19
20 transform.position.x += velocity.x * deltaTime21 transform.position.y += velocity.y * deltaTime22 transform.position.z += velocity.z * deltaTime23 }24 }25}The system asks for every entity with Transform and Velocity.
It then updates their position using their velocity.
Notice what the system does not check.
It never asks whether the entity is a player, enemy, projectile, NPC, or moving platform.
If an entity has the required components, it can move.
10. Register and Update Systems
Add these methods to World:
1addSystem(system) {2 this.systems.push(system)3
4 return system5}6
7update(deltaTime) {8 for (const system of this.systems) {9 system.update(deltaTime)10 }11}Then register our movement system:
1import { MovementSystem } from './systems/MovementSystem.js'2
3world.addSystem(4 new MovementSystem(world),5)The world can now run every registered system with:
world.update(deltaTime)
This also introduces an important architectural detail: system order matters.
Systems execute in the order they are registered.
Later, a game might use an order such as:
1InputSystem2PlayerControllerSystem3GravitySystem4MovementSystem5CollisionSystem6CameraSystem7DamageSystem8StatsSystem9RenderSystemFor example, gravity should normally change velocity before movement uses that velocity to update position.
11. Complete World Class
At this point, our minimal World.js looks like this:
1export class World {2 constructor() {3 this.nextEntityId = 14 this.entities = new Set()5 this.componentStores = new Map()6 this.systems = []7 }8
9 createEntity() {10 const entity = this.nextEntityId++11
12 this.entities.add(entity)13
14 return entity15 }16
17 destroyEntity(entity) {18 if (!this.entities.has(entity)) {19 return false20 }21
22 for (const store of this.componentStores.values()) {23 store.delete(entity)24 }25
26 this.entities.delete(entity)27
28 return true29 }30
31 addComponent(entity, component) {32 const componentType = component.constructor33
34 if (!this.componentStores.has(componentType)) {35 this.componentStores.set(36 componentType,37 new Map(),38 )39 }40
41 const store =42 this.componentStores.get(componentType)43
44 store.set(entity, component)45
46 return component47 }48
49 getComponent(entity, componentType) {50 const store =51 this.componentStores.get(componentType)52
53 if (!store) {54 return undefined55 }56
57 return store.get(entity)58 }59
60 hasComponent(entity, componentType) {61 const store =62 this.componentStores.get(componentType)63
64 return store?.has(entity) ?? false65 }66
67 removeComponent(entity, componentType) {68 const store =69 this.componentStores.get(componentType)70
71 if (!store) {72 return false73 }74
75 return store.delete(entity)76 }77
78 query(componentTypes) {79 const results = []80
81 for (const entity of this.entities) {82 const matches = componentTypes.every(83 (componentType) =>84 this.hasComponent(85 entity,86 componentType,87 ),88 )89
90 if (matches) {91 results.push(entity)92 }93 }94
95 return results96 }97
98 addSystem(system) {99 this.systems.push(system)100
101 return system102 }103
104 update(deltaTime) {105 for (const system of this.systems) {106 system.update(deltaTime)107 }108 }109}This implementation is intentionally straightforward.
A production ECS can optimize almost every part of it, but all of the important architectural ideas are already present.
12. Run the ECS
Now let's create a simple game loop.
Our main.js can look like this:
1import { World } from './ecs/World.js'2
3import { Transform } from './components/Transform.js'4import { Velocity } from './components/Velocity.js'5
6import { MovementSystem } from './systems/MovementSystem.js'7
8const world = new World()9
10const player = world.createEntity()11
12world.addComponent(13 player,14 new Transform(0, 0, 0),15)16
17world.addComponent(18 player,19 new Velocity(2, 0, 0),20)21
22world.addSystem(23 new MovementSystem(world),24)25
26let lastTime = performance.now()27
28function update(currentTime) {29 const deltaTime =30 (currentTime - lastTime) / 100031
32 lastTime = currentTime33
34 world.update(deltaTime)35
36 const transform =37 world.getComponent(player, Transform)38
39 console.log(transform.position.x)40
41 requestAnimationFrame(update)42}43
44requestAnimationFrame(update)The entity's velocity is two units per second along the X axis.
Every frame:
- the world updates
MovementSystemfinds the entity- the system reads its
TransformandVelocity - position changes
There is no player.move() method anywhere.
Instead:
1Entity2+3Transform4+5Velocity6+7MovementSystem8=9Movement13. Add Another Moving Entity
The benefit becomes clearer when we add another entity.
1const enemy = world.createEntity()2
3world.addComponent(4 enemy,5 new Transform(10, 0, 0),6)7
8world.addComponent(9 enemy,10 new Velocity(-1, 0, 0),11)We don't need to change MovementSystem.
It automatically discovers both entities because both have Transform and Velocity.
The same system could eventually move:
- players
- enemies
- NPCs
- projectiles
- moving platforms
The behaviour is reusable because it depends on component composition rather than inheritance.
14. Use Marker Components for Roles
Sometimes a component doesn't need any data at all.
Suppose we want to distinguish an entity controlled by the player.
We can create a marker component:
1import { Component } from '../ecs/Component.js'2
3export class PlayerControlled extends Component {}Then attach it:
1world.addComponent(2 player,3 new PlayerControlled(),4)A future PlayerControllerSystem could query for:
PlayerControlled + Transform + Velocity
while an AI system could process entities with a different marker.
The MovementSystem still doesn't care where movement came from.
That gives us a useful pipeline:
1InputSystem2 ↓3PlayerControllerSystem4 ↓5Velocity6 ↓7MovementSystemAn enemy could instead use:
1AISystem2 ↓3Velocity4 ↓5MovementSystemBoth reuse the same movement logic.
15. Components Can Change Behaviour Dynamically
One powerful property of ECS is that behaviour can change when components are added or removed.
Suppose we remove Velocity:
1world.removeComponent(2 player,3 Velocity,4)The entity still exists.
Its Transform still exists.
But it no longer matches the query used by MovementSystem.
The movement behaviour effectively disappears because the entity no longer has the required composition.
We could later add Velocity again and the existing system would automatically begin processing the entity.
That is very different from designing a rigid inheritance hierarchy where behaviour is permanently defined by an object's class.
16. Why Queries Become Important
Our current query implementation scans all entities every time it runs.
That is perfectly fine for learning the architecture, but it is not necessarily how we would build a highly optimized production ECS.
As the engine grows, we can introduce dedicated query objects that cache matching entities.
Instead of repeatedly scanning:
1Entity 12Entity 23Entity 34Entity 45...a query can maintain a set such as:
1Transform + Velocity2 Entity 13 Entity 34 Entity 125 Entity 28Then MovementSystem can operate directly on those entities.
The simple version in this guide is useful because it makes the concept obvious before introducing optimization.
17. Where Events Fit In
As we add gameplay systems, not every interaction should happen through direct component changes.
Sometimes several systems need to know that something happened.
For example:
1DamageSystem2 ↓3Damage Resolved4 ↓5DamageNumberUISystem6HitFlashSystem7DeathSystemInstead of making DamageSystem directly call every other system, we can introduce an EventBus.
That allows one system to announce an event while other systems react independently.
A useful distinction is:
- components represent state
- events represent something that happened
For example:
Health is state.
DamageResolved is an event.
Equipment is state.
ItemEquipped could be an event.
This becomes increasingly useful as the ECS grows.
18. Don't Put Everything Into ECS
ECS is a powerful architecture, but that doesn't mean every piece of game data needs to become an entity or component.
Static definitions are often better kept as ordinary JavaScript objects.
For example:
1const ItemDatabase = {2 bronzeSword: {3 name: 'Bronze Sword',4
5 modifiers: {6 attack: 5,7 },8 },9
10 healthPotion: {11 name: 'Health Potion',12 healAmount: 25,13 },14}This data describes reusable item definitions.
An Inventory or Equipment component can reference those definitions without turning every configuration object into an ECS entity.
The same applies to things like:
- ability definitions
- loot tables
- enemy archetypes
- level configuration
- static game settings
Use ECS where it helps organize runtime state and behaviour.
19. What a Larger ECS Can Become
Our example only contains two components and one system, but the architecture can grow naturally.
Components might eventually include:
1Transform2Velocity3Health4Input5PlayerControlled6Inventory7Equipment8BaseStats9FinalStats10Quickbar11StatusEffects12Collider13Interactable14CameraFollowSystems might include:
1InputSystem2PlayerControllerSystem3GravitySystem4MovementSystem5CollisionSystem6InteractionSystem7InventorySystem8ItemUseSystem9HealSystem10StatsSystem11TargetingSystem12DamageSystem13DeathSystem14RespawnSystem15RenderSystemThe important thing is that the basic model does not change.
Entities remain IDs.
Components remain state.
Systems remain behaviour.
Queries connect them together.
A Useful ECS Checklist
When adding a new feature, ask a few simple questions.
Does this need its own identity?
Create an Entity.
Does it describe current state?
Use a Component.
Examples include position, health, velocity, inventory contents, or equipped items.
Does it describe behaviour or a game rule?
Use a System.
Examples include movement, damage resolution, stat calculation, targeting, or collision.
Does a system need entities with a particular capability?
Use a Query.
For example, Transform + Velocity means an entity can participate in movement.
Did something happen that several systems may care about?
Consider an Event.
Is it reusable static configuration?
Use an ordinary definition or database object.
This mental model can resolve a surprising number of architecture decisions.
Where to Go Next
The ECS we've built here is intentionally small, but it gives us a foundation for more advanced features.
Natural next steps include:
- cached Query objects
- an EventBus
- input components and input systems
- fixed-timestep updates
- collision systems
- health and damage
- inventory and equipment
- stats and modifiers
- abilities and cooldowns
- status effects
- targeting
- death and respawning
- rendering with Three.js
Each feature can build on the same foundation rather than requiring us to redesign the entire engine.
Conclusion
We built a functional Entity Component System in JavaScript using only a few core ideas:
1World2Entities3Components4Queries5SystemsThe World coordinates the ECS.
Entities provide identity.
Components provide state.
Queries find entities with the required capabilities.
Systems apply behaviour to those entities.
Most importantly, gameplay emerges from composition.
An entity with Transform and Velocity can move because MovementSystem knows how to process that combination.
Another entity can use exactly the same system without sharing a parent class or duplicating movement code.
As the engine grows, we can make queries faster, add events, introduce fixed-timestep simulation, build combat and inventory systems, and eventually support much more complex gameplay.
But all of those features can grow from the same small architecture we built here.

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

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