The first reason to reach for a struct is not elegance. It is that Elixir can stop you before a typo turns into a real bug.
Suppose you are modeling a user with a plain map:
defmodule User do
def new do
%{nmae: "Bruce", age: 21}
end
end
That code runs. The key is wrong, but Elixir has no way to know that :nmae was supposed to be :name. A map is open-ended. Any key can be valid if your program decides it is valid.
That is useful when the keys are fungible, like coordinates on a board, IDs in a cache, or dynamic values coming from the outside world. But it is a poor fit when you are describing something with fixed named fields.
For that, Elixir gives us a struct.
Bruce opens the lesson with the cleanest possible definition:
“A struct is nothing more than a map with a specialized key.” — Bruce Tate
That is the whole idea. A struct is still a map, but it is a map with rules.
Defining the Rules
You define a struct inside a module with defstruct. The module gives the data a name. The struct gives the data its allowed fields.
There are two common ways to write it.
If you only want to declare the keys, use a list:
defmodule Point do
defstruct [:x, :y]
end
Now %Point{} has the keys :x and :y, both defaulting to nil.
If you want defaults, use a keyword list:
defmodule User do
defstruct name: "Jill", age: 25
end
Now %User{} starts with those values:
%User{}
#=> %User{name: "Jill", age: 25}
And if you misspell a key, Elixir stops you at the boundary:
%User{nmae: "Bruce", age: 21}
Bruce makes the payoff explicit in class:
“This won’t even let me through the compiler, because now I’ve said this is a struct, and Elixir has tools to make sure it knows exactly which keys are allowed.” — Bruce Tate
That is the emotional center of structs. The bug moves from “somewhere downstream” to “right here.”
🎯 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.
The Hidden Key
The natural question is: how does Elixir know?
The answer is that every struct carries one extra key:
u = %User{}
Map.keys(u)
#=> [:__struct__, :age, :name]
There are the fields we expected, :age and :name. But there is also :__struct__.
If you ask for it directly, you see the module:
u.__struct__
#=> User
That key is the link between the map-shaped data and the module that defines the rules. The data itself says, “I am a User.” From there, Elixir can look at User.defstruct and know which fields belong.
This is why the phrase “just a map” is true, but incomplete. A struct is a map with identity. The __struct__ key gives the map a type-shaped boundary.
Protocols Are Contracts
In the previous post, we looked at maps as Elixir’s random-access workhorse. Maps are good at key-value lookup, bulk operations, and enumeration. You can call Map.keys/1, Map.values/1, Map.to_list/1, and you can pass a map to many functions in Enum.
That works because maps implement certain protocols.
Bruce gives a useful definition:
“Think of a protocol as a contract, a way or a feature that a particular type of function or a particular module, which remember represents a data type.” — Bruce Tate
A protocol says, “this data type supports this behavior.” Enumerable means you can count and walk through the values. Collectible means you can put values into it in a standard way.
Maps support those contracts. Structs do not automatically support the same ones.
Bruce says it plainly:
“The protocols for a map are different than the protocols for a struct.” — Bruce Tate
A map and a struct may have the same physical shape, but they do not promise the same behavior.
For example:
Enum.count(%{one: 1})
#=> 1
Enum.count(%Point{x: 1, y: 2})
#=> raises Protocol.UndefinedError
That is not Elixir being stingy. It is Elixir guiding you.
A struct is not meant to be treated as a bag of interchangeable key-value pairs. It represents fixed named data. You do not usually ask, “how many arbitrary entries are in this user?” You ask for user.name, pattern match on %User{name: name}, or update one known field with %{user | age: 26}.
The restriction is the feature.
Map or Struct?
The choice is mostly about what the keys mean.
Use a map when the keys are data. If you are storing board positions, lookup tables, counters, dynamic JSON-like data, or values you expect to enumerate, a map is the right shape. The keys are homogeneous enough that treating them in bulk makes sense.
Use a struct when the keys are fields. A user has a name and an age. A point has an x and a y. A tetromino, which we will use next, has a name, location, rotation, and color. Those are not random entries. They are the vocabulary of the data type.
This is also why structs fit naturally beside modules. The module holds the functions. The struct holds the fixed shape those functions operate on.
Elixir Uses This Too
One of the reassuring things about structs is that Elixir uses them for its own data.
Dates are structs:
Date.utc_today() |> Map.keys()
#=> [:__struct__, :calendar, :day, :month, :year]
DateTimes are structs too:
DateTime.utc_now() |> Map.keys()
#=> [:__struct__, :calendar, :day, :hour, :microsecond, :minute, :month, :second, :std_offset, :time_zone, :utc_offset, :year, :zone_abbr]
You do not need to understand sigils, calendars, or time zone internals yet to appreciate the design. The important part is that the language reaches for the same abstraction it gives you.
Bruce closes that thought with a practical kind of confidence: if the Elixir and Phoenix teams use structs to model their own data, they are good enough for the data in our applications too.
Maps gave us random access. Structs add a boundary: fixed keys, named fields, and earlier errors. Once that model clicks, the next step is straightforward. We can stop passing related values around one at a time and start moving domain data as a single thing.
That is where structs become containers.
Want to Learn Elixir's Data Shapes by Mental Model?
This post comes from Bruce's structured Elixir course, where maps, structs, protocols, and modules are taught as production architecture decisions instead of syntax trivia. Build the mental models that help you choose the right shape for real-world Elixir code.
— Paulo & Bruce