ECS Rails

An Entity–Component–System reimagining of ActiveRecord that stays idiomatic to Ruby on Rails.

The full API below is implemented and tested (534 examples on real PostgreSQL). A companion bulletin-board app is built entirely on it and runs live at ecs-rails.kranzky.com. See the v0.1 retrospective for the full story of how it was designed.

The idea

Replace one-table-per-model with one-table-per-component. An entity is a lightweight identity row; all state and behaviour live in small, reusable components that are composed onto it.

```ruby class User < ApplicationEntity component Name component Email component Avatar component Moderator # a marker: no data, presence is the meaning end

class Email < ApplicationComponent validates :address, presence: true

def send_welcome_email # self is the Email, never the User end end ```

```ruby user = User.create! # one row in entities, no component rows user.email # => # — virtual, not persisted user.email.address = "a@b.com" user.save! # now `emails` gets a row

user.send_welcome_email # delegated to the Email component user.errors[:”email.address”] # component errors merge onto the entity ```

Every v0.1 capability, working today:

```ruby # Lazy components — no row until a value differs from its default. user.avatar.persisted? # => false, costs no INSERT

Presence / markers — a user IS a moderator when the row exists.

user.add(Moderator); user.moderator? # => true user.remove(Moderator)

Query by composition — avoids AR’s .with (CTEs); scopes to the entity model.

Post.with_component(PublishState, state: “published”) User.without_component(Avatar)

Preload to bound the query count on a list view.

Post.with_component(PublishState).includes_components(Title, Body, Likes) ```

Components are shared by type, so Likes behaves identically on a Post and a Comment — reuse without STI and without polymorphic associations.

Getting started

ruby # Gemfile — note the packaging name differs from the require path (see Names) gem "ecs_on_rails"

sh bundle install rails g ecs_rails:install rails g ecs_rails:component Email address:string verified:boolean

Entities go in app/entities, components in app/entities/components (configurable); the install generator wires the autoloading.

Documentation

  • Architecture — the invariants. Start here.
  • v0.1 retrospective — what was built, what the demo found, what’s next.
  • ADRs — why the design is the way it is (14 decisions, several amended by their own demo).
  • RFCs — the 13 features, each one commit.
  • Backlog — what deliberately isn’t built yet.
  • Friction log — the demo’s running verdict on the API.

Development

Requires Ruby >= 3.2 and a running PostgreSQL.

sh createdb ecs_rails_test bundle install bundle exec rspec

Set DATABASE_URL to point the suite at a different database.

Names

ecs_rails everywhere except the Gemfile — see ADR-0007.

   
GitHub repo ecs_rails
RubyGems gem ecs_on_rails
Ruby module EcsRails
require ecs_rails
Generators ecs_rails:install, :component, :relationship

Only the published gem name differs. RubyGems collapses -, _ and case when comparing names, so ecs-rails, ecs_rails and ecsrails are one name — and it belongs to an unrelated, still-maintained gem. ecs_on_rails keeps the rails keyword without the rails- prefix that convention reserves for Rails Core Team gems.

Licence

MIT. See LICENSE.txt.