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…
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.
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