Durable Objects for Elixir - persistent, single-instance objects accessed by ID.
This library provides a programming model for stateful, persistent actors in Elixir, leveraging native GenServer capabilities, Ecto for persistence, and the Spark DSL for a declarative developer experience.
Features
- Global Uniqueness: One instance per (module, object_id) pair across the cluster
- Persistent State: State survives process crashes and restarts via Ecto
- Automatic Lifecycle: Processes hibernate after inactivity, optionally shut down
- Alarm Scheduling: Built-in support for future work with database-backed persistence
- Declarative DSL: Define objects with Spark DSL for clean, expressive code
- Distribution Ready: Optional Horde integration for multi-node clusters
Installation
Add durable_object to your dependencies:
def deps do
[
{:durable_object, "~> 0.1.0"},
# Optional: for distributed mode
{:horde, "~> 0.10"},
# Optional: for Oban-based alarm scheduling
{:oban, "~> 2.17"}
]
endQuick Setup with Igniter
mix igniter.install durable_object
Manual Setup
- Generate and run the migration:
mix durable_object.gen.migration
mix ecto.migrate
- Configure DurableObject in your application:
# config/config.exs
config :durable_object,
repo: MyApp.Repo,
registry_mode: :local, # or :horde for distributed
object_keys: :strings, # :strings | :atoms! | :atoms — controls map key conversion on load
scheduler: DurableObject.Scheduler.Polling,
scheduler_opts: [
polling_interval: :timer.seconds(30),
claim_ttl: :timer.seconds(60)
]Usage
Define a Durable Object
defmodule MyApp.Counter do
use DurableObject
state do
field :count, :integer, default: 0
field :last_incremented_at, :utc_datetime
end
handlers do
handler :increment, args: [:amount]
handler :get
handler :reset
end
options do
hibernate_after :timer.minutes(5)
shutdown_after :timer.hours(1)
end
def handle_increment(amount \\ 1, state) do
new_count = Map.get(state, :count, 0) + amount
new_state = %{state | count: new_count, last_incremented_at: DateTime.utc_now()}
{:reply, new_count, new_state}
end
def handle_get(state) do
{:reply, Map.get(state, :count, 0), state}
end
def handle_reset(state) do
{:reply, :ok, %{state | count: 0}}
end
endUse the Generated Client API
The DSL automatically generates client functions:
# Increment by 5
{:ok, 5} = MyApp.Counter.increment("user-123", 5)
# Get current count
{:ok, 5} = MyApp.Counter.get("user-123")
# Reset
{:ok, :ok} = MyApp.Counter.reset("user-123")Or Use the Generic API
{:ok, 5} = DurableObject.call(MyApp.Counter, "user-123", :increment, [5])
{:ok, 5} = DurableObject.call(MyApp.Counter, "user-123", :get)Alarms
Schedule work to happen in the future:
defmodule MyApp.RateLimiter do
use DurableObject
state do
field :requests, :integer, default: 0
field :window_start, :utc_datetime
end
handlers do
handler :check, args: [:limit]
end
# Schedule initial alarm when object is first loaded
@impl DurableObject.Behaviour
def after_load(state) do
if is_nil(state.window_start) do
{:ok, %{state | window_start: DateTime.utc_now()},
{:schedule_alarm, :reset_window, :timer.minutes(1)}}
else
{:ok, state}
end
end
def handle_check(limit, state) do
if state.requests < limit do
{:reply, :allowed, %{state | requests: state.requests + 1}}
else
{:reply, :rate_limited, state}
end
end
@impl DurableObject.Behaviour
def handle_alarm(:reset_window, state) do
# Reset the window and reschedule
{:noreply, %{state | requests: 0, window_start: DateTime.utc_now()},
{:schedule_alarm, :reset_window, :timer.minutes(1)}}
end
endDistribution with Horde
For multi-node clusters, enable Horde:
# config/config.exs
config :durable_object,
registry_mode: :hordeThis ensures:
- Only one instance of each object exists across the cluster
- Objects are automatically migrated when nodes join/leave
- Alarms fire exactly once (singleton poller)
Telemetry
DurableObject emits telemetry events for observability:
[:durable_object, :storage, :save, :start | :stop | :exception][:durable_object, :storage, :load, :start | :stop | :exception][:durable_object, :storage, :delete, :start | :stop | :exception]
License
MIT License - see LICENSE for details.