View Source ActiveMemory (ActiveMemory v0.8.1)

The typed, attribute-queryable in memory store for ETS and Mnesia.

In most applications, a huge share of database load is reads of data that barely changes: reference data, configuration, authorization, catalog data. ActiveMemory exists to take that load off the database — load those tables into memory once at boot, serve every read for those tables from RAM, and write back only when something actually changes. A cache with no cache to manage: no keys to design, no cold misses, no invalidation dance. The in-memory copy serves the reads, while the database remains the durable source of truth.

Records are found by their attributes, in any combination, with no ETS match specifications to hand write:

AdminStore.one(%{email: email, active?: true})
ProductStore.select(%{category: "electronics", in_stock?: true})
TokenStore.withdraw(%{value: submitted_token})

The same interface runs on :ets or :mnesia, with boot-time seeding, record expiry, crash resilience and atomic take-once reads built in.

the-pieces

The pieces

  • ActiveMemory.Table — define a table's fields and its ETS/Mnesia options. A table is either an attributes block or an Ecto embedded_schema.
  • ActiveMemory.Store — a supervised process owning one table, and the API you read and write through.
  • ActiveMemory.ActiveRepo — the same API over several tables from one process.
  • ActiveMemory.Query — the match/1 macro, for queries that need more than equality.

getting-started

Getting started

Define a table, define a store, add the store to your supervision tree.

defmodule MyApp.People.Person do
  use ActiveMemory.Table, options: [index: [:last]]

  attributes auto_generate_uuid: true do
    field(:email, :string)
    field(:first, :string)
    field(:last, :string)
    field(:age, :integer)
    field(:admin?, :boolean)
  end
end

defmodule MyApp.People.Store do
  use ActiveMemory.Store, table: MyApp.People.Person
end
# in MyApp.Application
children = [MyApp.People.Store]

That is the whole setup. The table is created when the store starts, so there are no migrations to run.

{:ok, person} = MyApp.People.Store.write(%MyApp.People.Person{email: "kara@bsg.com"})
{:ok, person} = MyApp.People.Store.get(person.uuid)
people = MyApp.People.Store.all(order_by: :last, limit: 20)

Tables default to :mnesia; pass type: :ets for an ETS table. See ActiveMemory.Store for the full read and write API.

working-with-ecto

Working with Ecto

A table takes Ecto types, and can be an Ecto schema outright, so Ecto.Changeset works on it and write/1 accepts a changeset the way Ecto.Repo.insert/2 does:

%MyApp.People.Person{}
|> MyApp.People.Person.changeset(params)
|> MyApp.People.Store.write()

The Coming from Ecto guide covers what carries over, what is named differently, and where the two genuinely differ.

what-else-is-built-in

What else is built in

  • Record expiry. A ttl on a table gives every record a lifetime; reads never return an expired record and the owning process sweeps them to reclaim memory. See ActiveMemory.Table.
  • Crash resilience. ActiveMemory.TableHeir holds ETS tables when a store crashes, so the data survives the restart. No configuration needed.
  • Atomic take-once reads. withdraw/1 finds a record and removes it in one atomic operation, so exactly one concurrent caller wins — what you want for one time tokens and 2FA codes.

testing

Testing

A table's module name is its ETS/Mnesia table name, so isolation comes down to whether tests share a table. A test module that defines its own table and store runs async: true alongside every other test module with no configuration. Test modules that share your application's store need async: false, since they write to the same global table. See Testing for both patterns.

when-to-reach-for-it

When to reach for it

ActiveMemory suits a small-to-medium dataset that is read constantly but changes rarely — reference data, configuration, admin users and permissions, products and plans — plus short-lived records that benefit from ttl and withdraw/1, like one time tokens and 2FA codes.

It is not a system of record. ETS lives and dies with the node, and Mnesia persists only with disc_copies; keep durable data in a database. For caching computed values by key with eviction policies, a cache such as Cachex or Nebulex is the better fit.

Running across several nodes means a replicated Mnesia table, which makes network partitions a concern — see the majority option in ActiveMemory.Table.