Guide · Ruby on Rails

Virtual actors in Ruby on Rails

Solid Objects is a SQL-backed virtual actor library for Ruby on Rails: stable identities, durable state, ordered operations, and automatic activation on SQLite, PostgreSQL, or MySQL.

Short answer

Yes. Solid Objects is a SQL-backed virtual actor library for Ruby on Rails. The gem is solid_objects. It gives each actor a stable identity, durable state, ordered operations, and automatic activation.

Solid Objects requires Rails. It is a Rails engine for Ruby 3.3 or newer and Rails 7.1 or newer. It is not a framework-independent Ruby actor runtime. State and mailboxes live in the SQLite, PostgreSQL, or MySQL database that the Rails application already uses. Redis and a separate actor service are not necessary.

Solid Objects is a pre-1.0 release. It makes no production-ready claim. Read Compatibility and maturity before you choose it.

What a virtual actor is

A virtual actor is a logical object that always exists by name. The caller does not create it, start it, or stop it. The runtime loads it when a message arrives and releases it when it is idle. Microsoft Orleans made this model known as "virtual actors".

Solid Objects implements four properties of that model:

Property What it means How Solid Objects does it
Stable identity An actor is addressed by type and ID, for example one cart per user. ShoppingCart.ref(user.id) returns a cheap reference. The reference does not load the actor.
Automatic activation The first message activates the actor. An idle actor is released. A process claims a fenced activation lease when work arrives. The lease is released after the idle timeout.
Durable state State survives process restarts and deploys. Actor attributes are a JSON document in the application database.
Ordered turns One identity runs one operation at a time, in a fixed order. Each call is a durable mailbox message with a per-actor sequence number.

Different identities run concurrently. One identity is a serialization point on purpose.

A small example

This actor holds one ticket for a buyer and releases the hold after ten minutes. Put it in app/actors/ticket_sale.rb:

class TicketSale < SolidObjects::Actor
  attribute :available, default: 1
  attribute :holds, default: -> { {} }

  def hold(buyer:)
    return { held: false, available: } if available.zero? || holds.key?(buyer)

    self.available -= 1
    self.holds = holds.merge(buyer => Time.current.to_i)
    schedule(at: 10.minutes.from_now, key: buyer).expire(buyer:)
    { held: true, available: }
  end

  def expire(buyer:)
    return available unless holds.key?(buyer)

    self.holds = holds.except(buyer)
    self.available += 1
  end
end

Call it from a controller, a job, or the console:

TicketSale.ref("event-42").hold(buyer: "ada")

Two concurrent hold calls for event-42 enter the same mailbox. They commit one at a time, so only one buyer gets the ticket. The hold and its reminder commit in one transaction. The reminder runs in the Solid Objects runtime process:

bundle exec solid_objects start

If that process stops, the reminder stays in SQL. It runs when the process starts again.

The example needs installation, migrations, and an authorization policy. The generated policies deny every operation by default. For the complete setup, use one of these guides:

When to use it

Solid Objects is a good candidate when most of these conditions are true:

When to use something else

Do not use an actor when a simpler tool enforces the invariant:

Choosing Solid Objects has the full checklist and the cost model.

How it compares

This table compares coordination models for a Rails application. It does not rank the projects. Facts about other projects were checked on October 7, 2026, against the sources in Primary references.

Approach Unit of order Durable state Delayed work Extra service Good for
with_lock or a short SQL transaction Rows in one transaction Application tables None No An invariant that fits in one request
Active Job with Solid Queue None. limits_concurrency limits overlap for each key, but it does not set an order Application tables, owned by the application Scheduled jobs No; Solid Queue uses the database Background work that does not own entity state
Sidekiq None in the open-source gem. Unique jobs and rate limits are Sidekiq Enterprise features Application tables, owned by the application Scheduled jobs Redis High-volume background jobs
In-process concurrency: Ractor, concurrent-ruby-edge actors, Async One Ruby object in one process None. State is lost when the process stops In process only No Parallel or concurrent work inside one process. Ractor is experimental in Ruby 4.0
Solid Objects Actor type and ID JSON state in the application database Durable per-actor reminders No; the solid_objects start process runs in the app Durable per-identity state with ordered operations
Dapr actors Actor type and ID, one turn at a time A transactional Dapr state store Durable reminders through the Dapr Scheduler service A Dapr sidecar, plus the placement and Scheduler services. Dapr has no official Ruby SDK Polyglot services on a Dapr platform
Temporal (temporalio gem) A workflow execution Temporal event history Durable timers A Temporal Service, self-hosted or Temporal Cloud Long-running workflows that replay deterministic code

Orleans concept map

Orleans is the reference design for virtual actors on .NET. Solid Objects uses the same programming model on a SQL database. It does not copy the Orleans cluster, placement, or feature set.

Orleans Solid Objects Difference
Grain class SolidObjects::Actor subclass None in concept
Grain identity (key) Actor type and actor ID None in concept
Activation on first call Activation lease on first claimed message Solid Objects fences each activation with a database generation
Turn-based execution Ordered mailbox, one turn at a time Orleans can enable reentrancy. Solid Objects turns for one identity never interleave
Grain persistence JSON attributes in the application database An Orleans grain calls WriteStateAsync. Solid Objects persists state with the turn that changed it
Reminders schedule Both are durable. Orleans skips a tick that falls due while the cluster is down. A due Solid Objects reminder runs when a runtime process starts. Solid Objects has no non-durable timers
Silos and cluster membership Any Rails process that runs solid_objects start Solid Objects has no placement, directory, or cluster membership. The database is the coordination point
Streams Observables, broadcasts, and reactive ERB Solid Objects delivers committed revisions to Action Cable

Solid Objects delivery is at least once. Write each handler so that it can run again without harm.

Guarantees and boundaries

The correctness contract states each guarantee and the crash matrix.

Compatibility and maturity

The roadmap records what is tested, what is partial, and what is next. For TypeScript and Node.js, use the solid-objects package.

Primary references

Other projects change. Check these sources again before you base an architecture decision on one row.

Generated from docs/virtual-actors.md in cardmagic/solid-objects-ruby, v0.17.1, commit 7e19e64. For TypeScript and Node.js, read Virtual actors in TypeScript and Node.js.