# ETS, Agents and Shared State — Elixir & Phoenix

Source: https://www.geekswithgeeks.com/en/elixir-phoenix/a-ets

> Store shared state efficiently with ETS and simple state with Agents.

## Where state lives

Elixir has several places for state. A **GenServer** serialises access and is ideal for coordinated updates. An **`Agent`** is a simplified wrapper around state with `Agent.get` and `Agent.update`, handy for small, simple state. **ETS** (Erlang Term Storage) is an **in-memory key-value store** built into the VM, with **constant-time lookups** from any process, without going through a single server process, which makes it ideal for **caches**, counters and lookup tables read by many processes. Tables are created with `:ets.new(name, options)`, using types `:set`, `:ordered_set`, `:bag` or `:duplicate_bag`, access `:public`, `:protected` (the default; only the owner writes) or `:private`, and `read_concurrency: true` for read-heavy tables. An ETS table belongs to the process that created it and is deleted when that process dies, so create tables in a long-lived, supervised process. `:ets.update_counter` provides atomic increments. For persistent or distributed storage, use a database, `:persistent_term` for rarely-changing global config, or libraries such as Cachex and Nebulex.

## A read-heavy cache in ETS

A GenServer owns the table; readers go straight to ETS.

```elixir
defmodule Shop.PriceCache do
  use GenServer
  @table :price_cache

  def start_link(_opts), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)

  # readers do not go through the GenServer: direct, concurrent lookups
  def get(sku) do
    case :ets.lookup(@table, sku) do
      [{^sku, price, expires_at}] ->
        if System.monotonic_time(:second) < expires_at, do: {:ok, price}, else: :miss

      [] ->
        :miss
    end
  end

  def put(sku, price, ttl_seconds \\ 300) do
    expires_at = System.monotonic_time(:second) + ttl_seconds
    :ets.insert(@table, {sku, price, expires_at})
    :ok
  end

  def hit(counter), do: :ets.update_counter(@table, {:stats, counter}, 1, {{:stats, counter}, 0})

  @impl true
  def init(nil) do
    :ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
    {:ok, nil}                                  # the table lives as long as this process
  end
end

# Simple state with an Agent
{:ok, agent} = Agent.start_link(fn -> %{} end)
Agent.update(agent, &Map.put(&1, "pen", 120))
IO.inspect(Agent.get(agent, &Map.get(&1, "pen")))   # 120
```

## A notice board versus a clerk

Asking a GenServer is like queueing to ask a clerk. ETS is a notice board in the corridor: anyone can read it at the same time without waiting, while one person is responsible for keeping it up to date.

**Quiz:** Why is ETS often faster than a GenServer for read-heavy caches?

- [ ] It stores data on disk
- [ ] It compresses data
- [ ] It is written in JavaScript
- [x] Many processes can read ETS concurrently without sending messages through a single process

*Answer:* Many processes can read ETS concurrently without sending messages through a single process. ETS lookups avoid the serialisation of a single server process.
