Before we talk about data structures, it helps to name a problem you’ve likely encountered in other languages. You call a function that tries to find a user in a database. If the user exists, you get a User object. If they don’t, you get null or nil.
This feels fine until you need to know why something failed. Did the user not exist, or did the database connection time out? Suddenly you’re returning magic strings, throwing exceptions, or checking boolean flags. The code becomes a minefield of if statements checking for special cases.
In Elixir, we solve this by treating data structures as contracts. We don’t just return a value. We return a structure that explicitly declares what happened.
The Tuple as a Fixed Contract
While a list is designed for iteration and can grow or shrink, a tuple is a fixed container. It has a specific size and a specific meaning for each position. That rigidity is not a limitation, it’s the point.
When you match against {city, state}, you are asserting a contract. You are saying: I expect exactly two elements, and I know what each one means. If the system hands you {city, state, country}, the match fails because the contract was violated.
{city, state} = {"Austin", :tx}
# city => "Austin", state => :tx
{city, state} = {"Austin", :tx, :us}
# ** (MatchError)
We use tuples when the shape of the data is part of its identity. A coordinate {x, y} is not just a list that happens to have two numbers. It is a contract that defines a point in space.
🎯 Join Groxio's Newsletter
Weekly lessons on Elixir, system design, and AI-assisted development -- plus stories from our training and mentoring sessions.
We respect your privacy. No spam, unsubscribe anytime.
Tagged Tuples: Making Success Explicit
The most common application of this contract is the tagged tuple, a two-element tuple where the first element is an atom and the second is the payload. You have seen {:ok, value} and {:error, reason} throughout Elixir code, and now you know why they exist.
This pattern turns success and failure from hidden states into explicit data. Instead of catching an exception or checking a boolean, you match on the tag.
case Accounts.fetch_user(id) do
{:ok, user} ->
render(conn, :show, user: user)
{:error, :not_found} ->
send_resp(conn, 404, "Not found")
{:error, reason} ->
send_resp(conn, 500, "Unexpected error: #{inspect(reason)}")
end
The language does not let you “forget” that the operation could have failed. To access the user, you must first acknowledge the :ok tag. That discipline is what makes Elixir code easier to reason about.
Bruce demonstrates this in class with a function that randomly returns {:ok, state} or {:error, reason}. Without the tag, a naive match picks up the error message as if it were valid data, silently, with no crash. With the tag, the mismatch is immediate and loud. That is the feature.
The Practical Fix: Match the Tag First
The most common mistake when starting with tagged tuples is trying to reach inside them before verifying the tag. It usually looks like this:
# Don't do this
result = File.read("config.exs")
if elem(result, 0) == :ok do
content = elem(result, 1)
end
Do this instead:
case File.read("config.exs") do
{:ok, content} ->
# use content
{:error, reason} ->
# handle reason
end
By matching on the tag, your code only executes when the contract is met. The rule is simple: if you find yourself using elem/2 to pull values out of a tuple, you are probably missing a pattern match.
Keywords: Where the Syntax Gets Strange
If tuples are fixed contracts, how do you handle situations that need flexibility, like optional configuration for a function? This is where keyword lists come in, and where most newcomers hit what Bruce calls “a wall of confusion.”
A keyword list is a list of two-element tuples where the first element of each tuple is an atom. That is all it is. But Elixir allows a shorthand that strips away the brackets and flips the colon, making it look like something entirely different.
These two are identical:
[{:fast, true}, {:cheap, true}]
[fast: true, cheap: true]
The reason this matters is what happens when a keyword list appears as the last argument to a function. Elixir lets you drop the outer brackets entirely:
# These are the same call
String.split(str, " ", [trim: true])
String.split(str, " ", trim: true)
The second form looks like the function has three arguments. It has two: the string and the keyword list. Once you can mentally put those brackets back in, the wall of confusion disappears.
This pattern shows up everywhere in Elixir: function options, Ecto queries, Phoenix router macros. When you see a trailing key: value at the end of a function call, you are looking at a keyword list.
Choosing the Right Shape
Here is the practical before/after that ties it together:
# Before: return nil and branch later
user = Repo.get(User, id)
if user do
send_welcome(user)
else
IO.puts("User not found")
end
# After: return a tagged tuple and match once at the boundary
case Accounts.fetch_user(id) do
{:ok, user} -> send_welcome(user)
{:error, :not_found} -> IO.puts("User not found")
end
As for when to reach for each structure: use tuples for fixed-size records where position implies meaning. Use lists for collections you will iterate over. Use maps when you need to look things up by key and the set of keys is not fixed. Use keyword lists specifically for function options, where the sugared syntax earns its keep.
Design return values as contracts, not conveniences.
In the next post, we will look at how these contracts power the engine of Elixir logic: control flow and pattern-based decisions.
If you want to go deeper on these patterns with guided exercises and Bruce teaching live, check out the Groxio Elixir course.
📚 Production Architecture Decisions Start with Data Contracts
This comes from our Learning Elixir course, where Bruce teaches the mental models behind production architecture decisions through real-world scenarios. Learn to design return values as explicit contracts so your code stays clear, predictable, and maintainable. Available via monthly subscription -- try it for one month.
— Paulo & Bruce