# RECS: A Reactive ECS Framework for High-Performance Simulations in Julia

**URL:** <https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098>\
**Category:** Package Announcements\
**Tags:** games, ecs\
**Created:** [June 22, 2025, 12:32am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098 "2025-06-22T00:32:32Z")\
**Posts on this page:** 17\
**Page:** 1

<div class="post-metadata">

**Author:** ![Gesee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gesee/32/217235_2.png) [@Gesee](https://discourse.julialang.org/u/Gesee)\
**Post date:** [June 22, 2025, 12:32am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/1 "2025-06-22T00:32:32Z")

</div>

I am thrilled to introduce **RECS** (Reactive Entity-Component-System), a new Julia package designed for building high-performance, modular, and reactive simulations, with a focus on game development and scientific applications. RECS combines the data-oriented efficiency of ECS architectures with a reactive, asynchronous execution model, making it ideal for complex systems requiring dynamic data flows and scalability.

**Repository** : [github.com/Gesee-y/ReactiveECS.jl](https://github.com/Gesee-y/ReactiveECS.jl)  
**Documentation** : [ReactiveECS Guide](https://github.com/Gesee-y/RECS.jl/blob/main/doc/HowToUse.md) | [Reactive Architecture](https://github.com/Gesee-y/ReactiveECS.jl/blob/main/doc/Architecture.md)  
**Get Started** : `]add ReactiveECS` or `]add https://github.com/Gesee-y/ReactiveECS.jl` for the development version

## Why RECS?

RECS is built to address the limitations of traditional ECS frameworks, such as rigid update loops and static system orchestration, while leveraging Julia’s strengths for performance and flexibility. Key features include:

- **Reactive System Chaining** : Use `listen_to` to create dynamic data pipelines between systems, enabling modular workflows without tight coupling.
- **Asynchronous Execution** : Systems run as independent coroutines, supporting parallelism and real-time responsiveness.
- **Cache-Friendly Design** : Components are stored in a Struct of Arrays (SoA) layout, optimized for vectorization and SIMD (via `LoopVectorization.jl`).
- **Dynamic Extensibility** : Add or modify systems at runtime, ideal for iterative scientific experiments.
- **Fast Entity Management** : Entity pooling and precomputed archetypes ensure low overhead (e.g., ~869 ns to add an entity, ~168 ns to remove).

## RECS in Scientific Computing

RECS excels in scientific simulations where large datasets and complex interactions are common. Below is an example of using RECS for a **molecular dynamics simulation** , modeling particles under gravitational forces—a common task in computational physics and chemistry.

### Example: Molecular Dynamics Simulation

This example simulates a system of particles interacting via gravitational forces, with components for position, velocity, and mass, and systems for force computation and motion integration.

```julia
using ReactiveECS
using LinearAlgebra

# Define components
@component Position begin
    x::Float64
    y::Float64
end

@component Velocity begin
    vx::Float64
    vy::Float64
end

@component Mass begin
    m::Float64
end

# Define systems
@system ForceSystem
@system MotionSystem

# Compute gravitational forces between particles
function ReactiveECS.run!(::ForceSystem, ref::WeakRef)
    indices = ref.value
    positions = get_component(world, :Position)
    masses = get_component(world, :Mass)
    forces = zeros(Float64, length(indices), 2) # Store forces [fx, fy]

    @inbounds for i in eachindex(indices)
        for j in i+1:length(indices)
            idx1, idx2 = indices[i], indices[j]
            dx = positions[idx2].x - positions[idx1].x
            dy = positions[idx2].y - positions[idx1].y
            r = sqrt(dx^2 + dy^2)
            if r > 1e-6 # Avoid division by zero
                G = 6.67430e-11 # Gravitational constant
                force = G * masses[idx1].m * masses[idx2].m / r^2
                fx = force * dx / r
                fy = force * dy / r
                forces[i, 1] += fx
                forces[i, 2] += fy
                forces[j, 1] -= fx
                forces[j, 2] -= fy
            end
        end
    end
    return forces
end

# Update velocities and positions
function ReactiveECS.run!(sys::MotionSystem, data)
    indices = get_indices(world, sys) # Returns the indices of the entities matching the subscription of the system
    forces = data # Received from ForceSystem
    velocities = get_component(world, :Velocity)
    positions = get_component(world, :Position)
    masses = get_component(world, :Mass)
    dt = 0.01 # Time step

    @inbounds for i in eachindex(indices)
        idx = indices[i]
        # Update velocity (F = ma => a = F/m)
        velocities[idx].vx += forces[i, 1] / masses[idx].m * dt
        velocities[idx].vy += forces[i, 2] / masses[idx].m * dt
        # Update position
        positions[idx].x += velocities[idx].vx * dt
        positions[idx].y += velocities[idx].vy * dt
    end
end

# Setup
world = ECSManager()
force_sys = ForceSystem()
motion_sys = MotionSystem()

subscribe!(world, force_sys, (PositionComponent, MassComponent))
listen_to(force_sys, motion_sys)

run_system!(force_sys)
run_system!(motion_sys)

# Create 100 particles with random positions and masses
for i in 1:100
    create_entity!(world;
        Position=PositionComponent(rand() * 10, rand() * 10),
        Velocity=VelocityComponent(0.0, 0.0),
        Mass=MassComponent(1.0 + rand() * 10)
    )
end

# Simulate for 10 frames
for frame in 1:10
    println("Frame $frame")
    dispatch_data(world)
    blocker(world)
end

```

### Why RECS for Science?

- **Scalability** : RECS’s SoA layout and vectorization support (e.g., `@inbounds`, `@turbo`) make it ideal for large-scale simulations, like modeling thousands of particles.
- **Reactivity** : The `listen_to` mechanism allows systems to chain computations (e.g., forces → motion), mimicking the iterative workflows of scientific simulations.
- **Flexibility** : Add new systems (e.g., a visualization system) at runtime without modifying existing code, perfect for iterative experiments.
- **Performance** : Benchmarks show RECS handles 10K entities in ~766 μs for updates, outperforming other Julia ECS frameworks like Overseer.jl for large datasets.

For more examples, including Conway’s Game of Life and physics simulations, check our [case studies](https://github.com/Gesee-y/ReactiveECS.jl/blob/main/doc/GameOfLife.md).

## Get Involved

We’re excited to see how the community uses RECS for games, simulations, and beyond! Here’s how you can contribute:

- **Report bugs** or suggest features via GitHub issues.
- **Contribute code** to enhance RECS’s features or add new examples.
- **Share your projects** using RECS in the Discussions tab.

Try RECS today and let us know your feedback! For technical details, see our [reactive architecture article](https://github.com/Gesee-y/ReactiveECS.jl/blob/main/doc/Achitecture.md) or dive into the [user guide](https://github.com/Gesee-y/ReactiveECS.jl/blob/main/doc/HowToUse.md)

---

<div class="post-metadata">

**Author:** ![oschulz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oschulz/32/2998_2.png) [@oschulz](https://discourse.julialang.org/u/oschulz)\
**Post date:** [June 22, 2025, 9:21am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/2 "2025-06-22T09:21:41Z")

</div>

Very nice to see a high-performance ECS implementation!

A question about the design, if I may - I would expect constructs like

```julia
positions = get_component(world, :Position)

```

to make the `ReactiveECS.run!` methods type-unstable. Is this not relevant, performance-wise? Or is there some const-propagation magic under the hood?

---

<div class="post-metadata">

**Author:** ![Gesee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gesee/32/217235_2.png) [@Gesee](https://discourse.julialang.org/u/Gesee)\
**Post date:** [June 22, 2025, 10:30am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/3 "2025-06-22T10:30:24Z")

</div>

There will be no problem but `positions` will contain a StructArray  
`ReactiveECS.run!` is made to run with concrete type, you should try modifying field instead of interacting directly with `positions` and since we are using [StructArray.jl](https://github.com/JuliaArrays/StructArrays.jl), when you will try getting a field (for example `position.x`), it will return a vector of the `position.x` of each component. The problen now is that julia cannot infer the type of the data contained in that vector, leading to unnecessary allocations. so you should annotate the type

```julia
positions = get_component(world, :Position)
x_positions::Vector{Float32} = positions.x # Now you can get the x position of every entity
# without losing performances
```

---

<div class="post-metadata">

**Author:** ![Gesee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gesee/32/217235_2.png) [@Gesee](https://discourse.julialang.org/u/Gesee)\
**Post date:** [June 22, 2025, 12:19pm UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/4 "2025-06-22T12:19:54Z")

</div>

But if you want to be sure that you `run!` function is type stable, you can use [FunctionWrappers.jl](https://github.com/JuliaLang/FunctionWrappers.jl)

```julia

using FunctionWrappers

struct MySystemRun
    # FunctionWrapper will ensure that the function is type stable
    fun::FunctionWrapper{Nothing, Tuple{WeakRef}}
end

function run_my_system(ref) # ref is a weak ref to the vector of indices
    # My process
end

const MyRun = MySystemRun(run_my_system)
ReactiveECS.run!(::MySystem, ref) = MyRun.fun(ref)

```

---

<div class="post-metadata">

**Author:** ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)\
**Post date:** [June 22, 2025, 3:51pm UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/5 "2025-06-22T15:51:10Z")

</div>

Interesting, but I’m curious on what exactly is behind the hood such to make a package like this useful. I explain: in the example you show here, of a particle simulation, most of the time would be spent in computing the forces, which is something that is dealt with using specialized algorithms, and has to be parallelized at that level. The structure of the code, in base Julia, would not be too different, if we define custom structures for the particles, for example.

What benefits would I get from using such a system, in comparison with a bare bones implementation of a particle simulation ([like this, for example](https://github.com/m3g/2021_FortranCon/blob/main/celllistmap_vs_namd%2Fsimulate.jl))?

---

<div class="post-metadata">

**Author:** ![Gesee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gesee/32/217235_2.png) [@Gesee](https://discourse.julialang.org/u/Gesee)\
**Post date:** [June 22, 2025, 5:37pm UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/6 "2025-06-22T17:37:07Z")

</div>

It’s a good question.  
In fact, the main benefit of this package is **structural rather than algorithmic**.  
The point is to fully decouple the logic of your program. In the example above, the logic computing forces is completely separate from the one that integrates motion. This is particularly important when you have dozens of other systems (rendering, collisions, AI, etc.), and you want them to remain easy to compose or swap.

Another design goal is making **parallelization easier**. That’s exactly why the system returns the indices of the relevant entities and why components/entities are internally represented as contiguous arrays. This makes it trivial to split work across threads. For example:

```julia
chunk_size = 100
forces = zeros(Float64, length(indices), 2)
chunks = Iterators.partition(indices, chunk_size)
results = map(chunk -> Threads.@spawn calculate_force!(forces, chunk), chunks)
fetch.(results) # wait for all chunks to finish

```

There’s no merging step needed because all tasks directly update the shared `forces` buffer by index.

Sure, for a one-off particle sim, you could do something similar with plain structs. But as the system scales — with more behaviors, more data types, more interactions — having this ECS backbone means you don’t end up with a single tangled update loop. That’s where the architecture really pays off.

---

<div class="post-metadata">

**Author:** ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)\
**Post date:** [June 22, 2025, 6:47pm UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/7 "2025-06-22T18:47:57Z")

</div>

> [@Gesee](#):
>
> There’s no merging step needed because all tasks directly update the shared `forces` buffer by index

But what would be the function to calculate forces there? (again, maybe the example of the particle simulation above was unfortunate for my understanding)

---

<div class="post-metadata">

**Author:** ![Gesee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gesee/32/217235_2.png) [@Gesee](https://discourse.julialang.org/u/Gesee)\
**Post date:** [June 22, 2025, 8:00pm UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/8 "2025-06-22T20:00:47Z")

</div>

Oh, I see. My bad.  
Yeah, we will need a merge, since there are risk for race conditions. We would need to create buffer for each chunk and merge the results.  
Meaning, the `run!` function is actually just like a `main` function in C.  
You can implement your calculations the way you want, the point is to let you do that without mixing up logic, `MotionSystem`’s logic doesn’t interfer with `ForceSystem`’s logic. And also let you add new system easily, for example if you want to add a new `DragSystem`, you would just configure his logic and then just add it into your existing workflow with little to no change in your existing code.  
And this modularity doesn’t come at the cost of performances. Data (or more precisely indices) are efficiently delivered to the systems in a way that simplify parallelism and scalability.

---

<div class="post-metadata">

**Author:** ![oschulz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oschulz/32/2998_2.png) [@oschulz](https://discourse.julialang.org/u/oschulz)\
**Post date:** [June 23, 2025, 1:19am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/9 "2025-06-23T01:19:08Z")

</div>

Would a construct like

```julia
positions = get_component(world, Val(:Position))

```

be possible to make things type-stable without explicit type-annotations?

---

<div class="post-metadata">

**Author:** ![oschulz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oschulz/32/2998_2.png) [@oschulz](https://discourse.julialang.org/u/oschulz)\
**Post date:** [June 23, 2025, 1:35am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/10 "2025-06-23T01:35:43Z")

</div>

I wonder if, by specializing `getindex` and `getproperty` and so on, one could turn this into something like

```julia
function ReactiveECS.run!(world, sys::MotionSystem, data)
    E = world[sys] # "filtered" entity references, kind of a virtual StructArray

    # via `getproperty` specialization:
    velocities = E.Velocity
    positions = E.Position
    masses = E.mass

    # Virtual StructArray-like behavior:
    # E.Velocity[idx] == E[idx].Velocity

    @inbounds for idx in eachindex(E)
        # ...
    end
end

```

(with the same happening under the hood as before, just nice syntactic sugar on top)?

---

<div class="post-metadata">

**Author:** ![Gesee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gesee/32/217235_2.png) [@Gesee](https://discourse.julialang.org/u/Gesee)\
**Post date:** [June 23, 2025, 6:00am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/11 "2025-06-23T06:00:55Z")

</div>

Nice idea.  
I will work on that.  
It would greatly simplify the syntax

---

<div class="post-metadata">

**Author:** ![oschulz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oschulz/32/2998_2.png) [@oschulz](https://discourse.julialang.org/u/oschulz)\
**Post date:** [June 23, 2025, 6:43am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/12 "2025-06-23T06:43:01Z")

</div>

Cool! I’m excited to see where this goes, I think there’s interesting applications for ECS esp. in scientific simulations (where the approach is often not well know yet).

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [June 23, 2025, 7:22am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/13 "2025-06-23T07:22:42Z")

</div>

> [@lmiq](#):
>
> The structure of the code, in base Julia, would not be too different, if we define custom structures for the particles, for example.

> [@Gesee](#):
>
> The point is to fully decouple the logic of your program. In the example above, the logic computing forces is completely separate from the one that integrates motion.

I think lmiq is trying to say that it would be possible to define a `struct Particle` that contains `Position, Velocity, Mass` fields and for the algorithm to deal with forces and motion separately too. That seems true, could even use StructArray.jl for component-wise storage. The standout difference to me is that ECS doesn’t demand a particular `struct` to implement an entity, only components (at least that’s what `@component` seems to be doing). We only needed to deal with masses and positions in the force system and masses, positions, and velocities in the motion system; the systems have no idea what a particle is, and there are no methods specifically for particles. If there were, the `Particle` methods would just extract the components, do the real work on the components, and rewrap them into `Particle`s for no real benefit. Intuitively, this opens up situations where there isn’t a manageable set of fixed structures. Maybe the game lets players put any number of wheels on a car, not just 4, and the car itself is already made of arbitrary pieces. Making distinct types, even nameless ones like `NamedTuple`s, for every idea of a car players can imagine is a nightmare there and unnecessary because it’s the components’ behaviors and the emergent activity that matter.

> [@Gesee](#):
>
> ```julia
> velocities[idx].vx += ...
> velocities[idx].vy += ...
> positions[idx].x += ...
> positions[idx].y += ...
> 
> ```

Minor question, how are these mutations via field reassignment possible when the macro seems to be making [immutable structs](https://github.com/Gesee-y/ReactiveECS.jl/blob/a8561451b5b9e0ad1dc26f119e6172482b4825dc/src/components.jl#L32)?

```julia
macro component(name, block)
	struct_name = Symbol(string(name)*_default_suffix())
	ex = string(name) # Just to interpolate a symbol

	# Our struct expression
	struct_ex = Expr(:struct, false, :($struct_name <: AbstractComponent), block)
    ...

```

---

<div class="post-metadata">

**Author:** ![Gesee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gesee/32/217235_2.png) [@Gesee](https://discourse.julialang.org/u/Gesee)\
**Post date:** [June 23, 2025, 7:58am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/14 "2025-06-23T07:58:14Z")

</div>

In fact, a `StructArray` doesn’t actually keep the struct in memory, it keep a vector or each field. so if I `push!` a struct in a `StructArray`, each field’s value will go to their respective vector. Meaning whetever the struct is mutable or immutable, since it’s not the struct that’s directly stored, it doesn’t matter (but keeping struct immutable make the `StructArray` reconstruct that struct quicker)

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [June 23, 2025, 8:06am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/15 "2025-06-23T08:06:51Z")

</div>

Just checking if I’m reading that right. It’s not that an entity is deconstructed and stored into struct of arrays of its components, but rather each component is deconstructed and stored into a struct of arrays of its fields?

---

<div class="post-metadata">

**Author:** ![Gesee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gesee/32/217235_2.png) [@Gesee](https://discourse.julialang.org/u/Gesee)\
**Post date:** [June 23, 2025, 8:09am UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/16 "2025-06-23T08:09:36Z")

</div>

Yeah, and each struct of array of component is stored in a dict via their name. In fact, an entity is much just an indice. It indicate a which position the data of the entity are

---

<div class="post-metadata">

**Author:** ![Gesee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gesee/32/217235_2.png) [@Gesee](https://discourse.julialang.org/u/Gesee)\
**Post date:** [June 23, 2025, 5:09pm UTC](https://discourse.julialang.org/t/recs-a-reactive-ecs-framework-for-high-performance-simulations-in-julia/130098/17 "2025-06-23T17:09:00Z")

</div>

If you’re interested, I alredy updated the syntax and optimized performances. Here is now the form of a typical `run!`:

```julia
using RECS

# This will create a new component
# And set boilerplates for us
# The constructor is just the name passed here plus the suffix "Component"
@component Health begin
    hp::Int
end

@component Transform begin
    x::Float32
    y::Float32
end

@component Physic begin
    velocity::Float32
end

# We create a system
@system PhysicSystem begin
    delta::Float32
end
@system RenderSystem

# The system's internal logic
# Each system should have one
function RECS.run!(world, sys::PhysicSystem, data)

    E = world[sys] # First we get some sort of wrapper for ease of use
    indices::Vector{Int} = data.value # The indices of the matching entities
    L = length(indices)

    # Next we get the components
    transforms = E.Transform
    physics = E.Physic

    # We will just work on the x axis
    # Type unstability has been fixed, no more need to specify the types
    x_pos = transforms.x
    velo = physics.velocity
    dt::Float32 = sys.delta

    @inbounds for i in indices
        x_pos[i] += velo[i]*dt
    end
end

function RECS.run!(_, ::RenderSystem, pos) # Here `pos` is the transform_data we returned in the PhysicSystem `run!`
    for i in eachindex(pos)
        t = pos[i]
        println("Rendering entity at position ($(t.x), $(t.y))")
    end
end

ecs = ECSManager()

physic_sys = PhysicSystem(1/60)
render_sys = RenderSystem()

subscribe!(ecs, physic_sys, (TransformComponent, PhysicComponent))
listen_to(physic_sys,render_sys)

# Creating 3 entity
# We pass as keywork argument the component of the entity
e1 = create_entity!(ecs; Health = HealthComponent(100), Transform = TransformComponent(1.0,2.0))
e2 = create_entity!(ecs; Health = HealthComponent(50), Transform = TransformComponent(-5.0,0.0), Physic = PhysicComponent(1.0))
e3 = create_entity!(ecs; Health = HealthComponent(50), Transform = TransformComponent(-5.0,0.0), Physic = PhysicComponent(1.0))

# We launch the system. Internally, it's creating an asynchronous task
run_system!(physic_sys)
run_system!(render_sys)

N = 3

for i in 1:N
    println("FRAME $i")

    # We dispatch data and each system will execute his `run!` function
    dispatch_data(ecs)
    yield()
    sleep(0.016)
end

```
