DEV Community

Cover image for Leverage ETS for Shared State in Phoenix
Rushikesh Pandit
Rushikesh Pandit

Posted on

Leverage ETS for Shared State in Phoenix

When building web applications, managing shared state efficiently is often a challenge. For Elixir developers, ETS (Erlang Term Storage) provides an incredibly powerful tool to tackle this problem. ETS is a robust, in-memory data store built into the Erlang VM, offering fast lookups, concurrent access, and persistence during a process’s lifetime.

In this blog, we’ll explore how ETS works, why it’s an excellent choice for shared state in Phoenix applications, and demonstrate practical examples with code snippets.

What is ETS?

ETS (Erlang Term Storage) is a highly efficient in-memory storage system available in Elixir and Erlang. It supports storing key-value pairs, lists, and tuples and is designed to handle high-performance operations with concurrent reads and writes. ETS tables live as long as the process that owns them, making them ideal for managing temporary or shared state.

Why Use ETS in Phoenix?

ETS is a great fit for Phoenix applications in scenarios such as:

  1. Caching: Store frequently accessed data, such as API responses or computed results, for quick retrieval.
  2. Session Management: Maintain temporary user session data across requests.
  3. Rate Limiting: Track user activity to enforce API rate limits.
  4. Real-Time Data Sharing: Share data between processes efficiently.

ETS Basics

To get started, let’s create and manipulate a simple ETS table. You can create a table using :ets.new/2 and interact with it using functions like :ets.insert/2 and :ets.lookup/2.

# Creating an ETS table
table = :ets.new(:example_table, [:set, :public])

# Inserting data
:ets.insert(table, {:key, "value"})

# Retrieving data
case :ets.lookup(table, :key) do
  [{:key, value}] -> IO.puts("Found: #{value}")
  [] -> IO.puts("Not found")
end
Enter fullscreen mode Exit fullscreen mode

Using ETS in Phoenix

Let’s dive into a practical example: caching frequently accessed data in a Phoenix application.

1. ETS for Caching API Data

Suppose you have a Phoenix controller that fetches data from an external API. Instead of fetching the data on every request, you can cache the result in ETS.

ETS Cache Setup

Create a module to manage your ETS table:

defmodule MyApp.Cache do
  @table :api_cache

  def start_link do
    :ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
    {:ok, %{}}
  end

  def put(key, value) do
    :ets.insert(@table, {key, value})
  end

  def get(key) do
    case :ets.lookup(@table, key) do
      [{^key, value}] -> {:ok, value}
      [] -> :error
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Fetching and Caching API Data

Use this module in your controller:

defmodule MyAppWeb.PageController do
  use MyAppWeb, :controller

  alias MyApp.Cache

  def index(conn, _params) do
    data =
      case Cache.get(:api_data) do
        {:ok, cached_data} ->
          cached_data

        :error ->
          # Fetch data from external API
          api_data = fetch_data_from_api()
          Cache.put(:api_data, api_data)
          api_data
      end

    render(conn, "index.html", data: data)
  end

  defp fetch_data_from_api do
    # Simulated API call
    %{key: "value", timestamp: DateTime.utc_now()}
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, subsequent requests to index/2 will retrieve data from the ETS cache instead of making repeated API calls.

2. Rate Limiting with ETS

Another practical use case is rate limiting. You can use ETS to track how many requests a user has made within a given time window.

Rate Limiter Module

defmodule MyApp.RateLimiter do
  @table :rate_limiter

  def start_link do
    :ets.new(@table, [:named_table, :set, :public])
    {:ok, %{}}
  end

  def allow?(user_id, limit \\ 10) do
    case :ets.lookup(@table, user_id) do
      [{^user_id, count}] when count >= limit ->
        false

      [{^user_id, count}] ->
        :ets.insert(@table, {user_id, count + 1})
        true

      [] ->
        :ets.insert(@table, {user_id, 1})
        true
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Using the Rate Limiter

Integrate it into a Phoenix controller to enforce limits:

defmodule MyAppWeb.ApiController do
  use MyAppWeb, :controller

  alias MyApp.RateLimiter

  def index(conn, _params) do
    user_id = get_user_id(conn)

    if RateLimiter.allow?(user_id) do
      json(conn, %{message: "Request allowed"})
    else
      conn
      |> put_status(:too_many_requests)
      |> json(%{error: "Rate limit exceeded"})
    end
  end

  defp get_user_id(conn) do
    # Example: extract user ID from session or token
    conn.assigns[:current_user].id
  end
end
Enter fullscreen mode Exit fullscreen mode

This setup ensures that a user cannot exceed the specified request limit, and ETS handles the tracking efficiently.

Advanced ETS Tips

  1. Named Tables: Use named tables ([:named_table]) for global access across processes.
  2. Concurrency Options: Use :read_concurrency and :write_concurrency for performance optimization in read-heavy or write-heavy scenarios.
  3. Persistence: Combine ETS with :dets (Disk ETS) to persist data across application restarts.

Limitations of ETS
While ETS is powerful, it’s important to be aware of its limitations:

  1. Table Lifespan: Tables are tied to the lifecycle of their owning process. If the process crashes, the table is lost.
  2. Manual Garbage Collection: ETS doesn’t automatically manage expired data. You’ll need to implement a cleanup mechanism if necessary.
  3. Cluster Awareness: ETS is not distributed across nodes in a cluster. For distributed setups, consider using Mnesia or a database.

ETS is a fantastic tool for managing shared state in Phoenix applications. Its simplicity and speed make it ideal for caching, rate limiting, and real-time data sharing. However, understanding its limitations is key to using it effectively.

Have you used ETS in your Phoenix projects? Share your experiences in the comments or let me know what other ETS use cases you'd like to explore!

Feel free to reach out if you need help.

LinkedIn: https://www.linkedin.com/in/rushikesh-pandit
GitHub: https://github.com/rushikeshpandit
Portfolio: https://www.rushikeshpandit.in

#myelixirstatus , #elixir , #phoenixframework

Top comments (0)