- Published on
Structs vs Embedded Schemas in Elixir
- Authors
- Name
- Gabriel Perales
- @g_perales

Introduction
When working with Elixir, one of the most common misconceptions I've encountered is that Ecto is simply a database ORM (Object-Relational Mapping) tool. However, Elixir is not an object-oriented language, and this fundamental difference means we need to approach data modeling differently than in languages like Ruby or Python.
In Elixir, when developers need to group related data together, they often default to using structs, which is indeed the idiomatic way in Elixir. However, in a dynamic language like Elixir where field types aren't enforced at compile time, this can lead to code that's difficult to maintain and prone to runtime errors. This is where validation becomes crucial, and you might find yourself writing a lot of manual validation code.
Enter Ecto's embedded schemas - a powerful feature that provides a structured way to group and validate data without the need for database persistence. Whether you're working with JSON APIs, form submissions, or complex data transformations, embedded schemas offer a robust solution for data validation and manipulation.
In this post, we'll dive deep into both approaches, explore their use cases, and help you make informed decisions about when to use each.
Understanding Structs in Elixir
Structs in Elixir are lightweight data structures that provide a way to create new types with predefined fields. Think of them as enhanced maps with compile-time guarantees and default values. They're perfect for simple data grouping when you don't need complex validation or database interactions.
Key Features of Structs
- Compile-time Safety: Fields are guaranteed to exist at compile time, preventing accidental access to undefined fields
- Default Values: Easy initialization with predefined default values
- Required Fields: Use
@enforce_keys
to ensure critical fields are always present - Pattern Matching: Seamless integration with Elixir's pattern matching capabilities
- Lightweight: No runtime overhead compared to more complex solutions
Practical Example: User Struct
defmodule User do
@enforce_keys [:name]
defstruct [:name, age: 0, email: nil]
end
# Creating a user
user = %User{name: "Alice", age: 30, email: "alice@example.com"}
# Pattern matching
case user do
%User{age: age} when age >= 18 -> :adult
_ -> :minor
end
Exploring Embedded Schemas
Embedded schemas are part of Ecto's powerful toolkit for data mapping and validation. They're particularly useful when you need to:
- Validate data from external sources (APIs, forms)
- Transform data between different formats
- Ensure data consistency without database persistence
- Work with complex nested data structures
Key Features of Embedded Schemas
- Built-in Validation: Comprehensive validation rules out of the box
- Type Casting: Automatic conversion between different data types
- Schema Composition: Create complex data structures by composing simpler ones
- Ecto Integration: Seamless work with Ecto's query interface when needed
- Flexible Persistence: Can be used with or without database storage
Practical Example: User Schema with Validation
defmodule User do
use Ecto.Schema
import Ecto.Changeset
@fields [:name, :age, :email]
@required_fields [:name, :email]
embedded_schema do
field :name, :string
field :age, :integer, default: 0
field :email, :string
end
def new(attrs) do
%__MODULE__{}
|> cast(attrs, @fields)
|> validate_required(@required_fields)
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/)
|> validate_number(:age, greater_than_or_equal_to: 0)
end
end
# Usage example
{:ok, user} = User.new(%{
name: "Alice",
age: 30,
email: "alice@example.com"
})
Making the Right Choice
Choose Structs When:
- You need simple data grouping without validation
- Performance is critical (structs have less overhead)
- You're working with internal data that you control
- You need compile-time guarantees
- You're building simple data transfer objects (DTOs)
Choose Embedded Schemas When:
- You need robust data validation
- You're working with external data sources
- You need to transform data between formats
- You want to leverage Ecto's powerful features
- You're building complex nested data structures
Real-world Example: API Integration
Let's see how both approaches work in a real-world scenario - handling API responses:
Using Structs (Simple Case)
defmodule APIResponse do
defstruct [:status, :data, :message]
end
# Simple parsing
response = %APIResponse{
status: 200,
data: %{"user" => %{"name" => "Alice"}},
message: "Success"
}
Using Embedded Schema (Complex Case)
defmodule APIResponse do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :status, :integer
field :message, :string
embeds_one :data, Data do
embedded_schema do
embeds_one :user, User do
embedded_schema do
field :name, :string
field :email, :string
end
end
end
end
end
def parse(json) do
%__MODULE__{}
|> cast(json, [:status, :message, :data])
|> validate_required([:status])
|> validate_number(:status, greater_than_or_equal_to: 100, less_than: 600)
end
end
Conclusion
While structs and embedded schemas might seem similar at first glance, they serve different purposes in Elixir applications. Structs are perfect for simple, internal data structures where you need compile-time guarantees and minimal overhead. Embedded schemas, on the other hand, provide a robust solution for data validation, transformation, and complex data structures.
The key to making the right choice lies in understanding your specific needs:
- Do you need validation? → Embedded Schema
- Is performance critical? → Struct
- Are you working with external data? → Embedded Schema
- Do you need simple data grouping? → Struct
Remember, Ecto is not just a database ORM - it's a powerful toolkit for data mapping and validation. By choosing the right tool for your specific use case, you can write more maintainable and reliable Elixir code.