← Back to list

Elixir’s defdelegate: An Underrated Tool for Stable APIs

For the past few weeks, I’ve been building an Elixir package. As part of that process, I’ve been studying well-established codebases to…

Andrew (Nature) Okoye in Beyond Localhost · 2026-04-19 14:08 · 11 claps · 3.4 min read
#elixir #programming #software-architecture
Open on Medium ↗
Wiki topics: 💻 · Programming 🏛️ · Architecture

Elixir’s defdelegate: An Underrated Tool for Stable APIs

From tightly coupled modules to a clean public API: how defdelegate simplifies access and reduces complexity.

From tightly coupled modules to a clean public API: how defdelegate simplifies access and reduces complexity.

For the past few weeks, I’ve been building an Elixir package. As part of that process, I’ve been studying well-established codebases to understand how experienced teams structure their APIs — one of them being Phoenix. While exploring the Phoenix.Controller module, I came across a line that immediately stood out:

defdelegate get_csrf_token(), to: Plug.CSRFProtection

It looked simple, but it raised an important question: why delegate a function instead of calling it directly? In this article, we’ll break down how Elixir’s defdelegate works, and more importantly, why it matters. You’ll see how it helps you design APIs that stay stable even as your internal implementation changes — without breaking your clients. If you’re coming from languages like JavaScript, this might feel similar to certain design patterns — but in Elixir, it’s built right into the language.

The problem Elixir’s defdelegate addresses

Imagine you create a function called encode_string/1, and this function is exposed as part of your module's public API to the clients. Clients can import your function and call it with the proper arguments. Consider the snippet below:

defmodule MyApp.Utils do
  def encode_string(value) do
    Base.encode64(value)
  end
end

By “client,” I mean any code that calls or depends on a function exposed by your module.

Now, what do you think will happen if, sometime in the future, you decide to move the encode_string function to an entirely different module, probably because the Utils module is getting congested, and you decide to move it to a more specific module? Consider the snippet below:

defmodule MyApp.Encoding do
  def encode_string(value) do
    Base.encode64(value)
  end
end

With this simple change, every code that depends on MyApp.Utils.encode_string/1 will break. One way to fix this will be to look through your entire codebase and replace every usage with:

MyApp.Encoding.encode_string/1

That might be manageable in a small project — but in a large codebase or a published library, this becomes risky and error-prone. This is exactly the kind of problem that Elixir's defdelegate is designed to solve.

Using Elixir’s defdelegate to preserve your API

Instead of forcing every client to update its code based on the change we introduced earlier, we can implement a layer of indirection using defdelegate. Consider the snippet below:

defmodule MyApp.Utils do
  defdelegate encode_string(value), to: MyApp.Encoding
end

What we have done here is that we have made MyApp.Utils.encode_string/1 to act as a proxy forwarding calls to MyApp.Encoding. Our public API remains unchanged, even though the implementation has moved.

In other languages, this kind of problem is often solved using the Facade design pattern, where a stable interface delegates work to underlying components. defdelegate gives us a concise, built-in way to achieve the same result in Elixir.

Reducing coupling with a unified interface

Another benefit of defdelegate is that it allows you to expose a single, cohesive API to your clients. Without it, consumers may need to import multiple modules just to access related functionality: Consider the snippet below:

defmodule NatureUI do
  # Users must know which module to call
  def styles(assigns), do: NatureUI.Components.Assets.styles(assigns)
  def button(assigns), do: NatureUI.Components.Button.button(assigns)
  def card(assigns), do: NatureUI.Components.Card.card(assigns)
  def table(assigns), do: NatureUI.Components.Table.table(assigns)
  def modal(assigns), do: NatureUI.Components.Modal.modal(assigns)
  def form(assigns), do: NatureUI.Components.Form.form(assigns)
  # ... More repetitive wrappers
end

Let us define a module that needs the functions in the NatureUI module. Consider the snippet below:

# User has to know and import ALL these modules
defmodule MyApp do
  import NatureUI.Components.Assets
  import NatureUI.Components.Button  
  import NatureUI.Components.Card
  import NatureUI.Components.Form
  import NatureUI.Components.Modal
  import NatureUI.Components.Table
  # ... imagine other imports

  def render(assigns) do
    ~H"""
    <div>
      <%= styles(@assigns) %>      <!-- from Assets -->
      <%= button("Click") %>       <!-- from Button -->
      <%= card(%{title: "Hi"}) %>  <!-- from Card -->
      <%= form(@changeset) %>      <!-- from Form -->
    </div>
    """
  end
end

From the snippet above, you can see how the client(MyApp module) is tightly coupled to the internal module’s structure and how we had to do a number of imports to access different UI widgets. Now, let us introduce defdelegate. Consider the snippet below:

defmodule NatureUI do
  defdelegate styles(assigns), to: NatureUI.Components.Assets
  defdelegate button(label), to: NatureUI.Components.Button
  defdelegate card(attrs), to: NatureUI.Components.Card
  defdelegate form(changeset), to: NatureUI.Components.Form
  defdelegate modal(assigns), to: NatureUI.Components.Modal
  defdelegate table(data), to: NatureUI.Components.Table
end

With this refactor, we will update the MyApp module like so:

defmodule MyApp do
  import NatureUI  # ← Just one!

  def render(assigns) do
    ~H"""
    <div>
      <%= styles(@assigns) %>
      <%= button("Click") %>
      <%= card(%{title: "Hi"}) %>
      <%= form(@changeset) %>
    </div>
    """
  end
end

With this refactor, you can see how the client code is simpler; your clients no longer need to understand your internal structure—they interact with a single, stable interface.

Conclusion

Elixir’s defdelegate may look like a small feature, but it has a big impact on how you design your APIs. It allows you to keep your public interface stable while your implementation evolves, reducing coupling and making your code easier to maintain. In other languages, this often requires patterns like Facade or Proxy. In Elixir, it’s built right in. Little things like this can make you fall in love with a language 🤗

Your API stays stable. Your implementation stays flexible. That’s the real power of defdelegate.


메타데이터
post_id
90a40618febf
slug
elixirs-defdelegate-an-underrated-tool-for-stable-apis-90a40618febf
url
https://medium.com/beyond-localhost/elixirs-defdelegate-an-underrated-tool-for-stable-apis-90a40618febf
canonical_url
https://medium.com/beyond-localhost/elixirs-defdelegate-an-underrated-tool-for-stable-apis-90a40618febf
author_url
https://medium.com/@nature.exs
status
ok
fetched_at
2026-06-09 15:37:30