Modules and Imports
Modules are how Elixir organizes code. They are not classes — there is no inheritance, no instance state, nothing to instantiate. A module is a namespace that contains functions, types, and metadata. Every function in Elixir lives inside a module, and the BEAM treats each module as a unit of compilation, hot code loading, and tracing.
defmodule and Module Names
defmodule Billing.Invoice do
def total(%{lines: lines}) do
Enum.reduce(lines, 0, fn line, acc -> acc + line.amount end)
end
end
Billing.Invoice is the full module name. The dot is purely cosmetic — it is not real nesting. Billing.Invoice and Billing are unrelated modules unless you explicitly create both. The dotted naming is a convention for organizing related functionality, not a feature of the language.
By convention, module names are PascalCase, files match module names (Billing.Invoice lives in lib/billing/invoice.ex), and you keep one main module per file. Mix's compiler can find files by guessing this layout, so straying from it costs you tooling support.
The Four Module Directives: import, alias, require, use
This is where most newcomers get confused. Elixir has four ways to bring code from another module into the current one, and they do different things.
alias: Shorten Names
alias lets you refer to a module by its last segment (or a custom name).
defmodule Web.UserController do
alias MyApp.Accounts.User
alias MyApp.Accounts, as: A
def show(id) do
A.get_user!(id)
|> render_user()
end
defp render_user(%User{} = user), do: user.name
end
alias is by far the most common directive. It does not change behavior, just naming. You will see it at the top of nearly every Elixir module.
import: Bring Functions Into Scope
import makes another module's functions callable without a module prefix.
defmodule Calculator do
import Kernel, except: [+: 2]
import :math, only: [sqrt: 1, pow: 2]
def hypotenuse(a, b), do: sqrt(pow(a, 2) + pow(b, 2))
end
Use import sparingly. When you read sqrt(x), you cannot tell where sqrt came from. Phoenix tests heavily import Phoenix.ConnTest and Plug.Conn so that test code reads naturally, but most application code prefers alias and explicit module calls.
The :only option is a strong convention — it makes the import explicit about what is being borrowed, and tools can warn if those functions go unused.
require: Make Macros Available
require ensures a module is loaded so its macros can be used. Functions don't need this — only macros do, because macros run at compile time.
defmodule Audit do
require Logger
def record(event) do
Logger.info("audit: #{inspect(event)}")
:ok
end
end
Logger.info/1 is a macro, not a function. Without require Logger, the compiler would refuse to expand it. Most macros come from libraries — Ecto's from, Logger's logging functions, ExUnit's assert — and those libraries usually arrange for require to happen automatically via use.
use: Run a Macro That Injects Code
use SomeModule is shorthand for require SomeModule; SomeModule.__using__(opts). The target module's __using__/1 macro returns code that gets injected into yours.
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end
That use MyAppWeb, :controller injects imports, aliases, and pipelines that make the controller work. It is also the source of confusion when something is in scope and you cannot find where it came from. When in doubt, find the __using__ definition and read what it actually does.
A rough rule: alias is almost always right. import is sometimes right. require is right when you need a macro. use is right when a library tells you to use it.
Module Attributes
Module attributes are set with @ and serve three purposes: documentation, compile-time constants, and metadata for tools.
defmodule HTTP.Client do
@moduledoc """
A thin wrapper around Finch with retries.
"""
@default_timeout 5_000
@retryable_statuses [408, 429, 500, 502, 503, 504]
@doc """
Issues a GET request and decodes the JSON body.
"""
@spec get(String.t(), keyword()) :: {:ok, map()} | {:error, term()}
def get(url, opts \\ []) do
timeout = Keyword.get(opts, :timeout, @default_timeout)
do_request(:get, url, timeout)
end
defp do_request(method, url, timeout) do
# ...
end
end
@moduledoc and @doc are read by ExDoc to generate HTML documentation and by IEx so you can type h HTTP.Client.get/2 and see the docs. @spec declares types for Dialyzer to check.
@default_timeout and @retryable_statuses are compile-time constants. Each time they appear in code, the compiler inlines the value. This means changing an attribute requires recompiling everything that uses it — but it also means there is zero runtime overhead.
A pitfall worth knowing: module attributes accumulate by default if you reuse the same name. @foo 1; @foo 2 makes @foo a list [2, 1], not a redefinition. Most attributes you care about are configured to overwrite, but custom ones do not. If you find yourself needing append-only attributes, that is what Module.register_attribute/3 is for.
Documentation as a First-Class Citizen
Elixir takes documentation seriously enough that it is part of the language, not an afterthought.
defmodule Phone do
@moduledoc "Phone number normalization."
@doc """
Normalizes a phone number to E.164 format.
## Examples
iex> Phone.normalize("(415) 555-0100")
"+14155550100"
iex> Phone.normalize("invalid")
:error
"""
def normalize(input) do
# ...
end
end
The ## Examples block is not just decoration. ExUnit can run those iex> lines as tests via doctests. This is how Elixir's standard library stays so well-documented — the examples are checked on every commit.
Nested Modules
You can nest modules inside defmodule blocks, but the nesting is purely syntactic.
defmodule Payments do
defmodule Stripe do
def charge(amount), do: {:ok, amount}
end
defmodule Adyen do
def charge(amount), do: {:ok, amount}
end
end
This compiles to three modules: Payments, Payments.Stripe, and Payments.Adyen. You could equally write them in three separate files with three separate defmodule declarations. Most teams prefer the separate-file approach because it plays better with Mix's incremental compilation and with editors. Nesting is reserved for cases where the inner modules are intentionally tied to the outer one, like Phoenix.Endpoint and its nested Phoenix.Endpoint.Cowboy2Handler.
Module Conventions Worth Following
A few conventions that almost every Elixir codebase follows:
The order at the top of a file is: @moduledoc, then use, then import, then alias, then require, then attributes, then function definitions. Editors and formatters will not enforce this, but humans expect it.
Files mirror module names. lib/my_app/accounts/user.ex defines MyApp.Accounts.User. Mix's auto-discovery and Phoenix's generators all assume this.
Tests live in test/ mirroring lib/. lib/my_app/accounts/user.ex is tested by test/my_app/accounts/user_test.exs. The _test.exs extension matters — files ending in .exs are scripts that get evaluated, not compiled into the artifact.
Public API modules sit at one level up from implementation. Discord, for instance, exposes a clean Manifold API for distributed messaging while implementation modules live under Manifold.*. Callers should rarely reach into nested modules directly.
Common Pitfalls
Confusing alias with import. alias shortens names. import removes the prefix entirely. If you find yourself wishing you could call User.get!/1 as just get!/1, that is what import does — but it usually makes the code harder to read.
Using use without understanding what it does. use Foo injects whatever Foo.__using__/1 returns. That can be hundreds of lines of imports, function definitions, and module attributes. When a controller magically has functions you did not define, this is why. Read the __using__ macro of any library you use.
Defining helper modules with no @moduledoc. Tools like Credo will warn about it, and IEx help will be empty. Even @moduledoc false is better than nothing — it explicitly marks a module as internal.
Putting application logic in the top-level namespace. MyApp.User versus MyApp.Accounts.User. The second tells you which subsystem owns user data. Phoenix's generators encourage context-based naming for this reason — it scales better than a flat namespace.
Forgetting that @attr is compile-time, not runtime. Putting @version Application.get_env(:my_app, :version) reads the config at compile time, baking a stale value into your release. Use Application.get_env/2 directly in a function for runtime values.
Key Takeaways
- Modules are namespaces with functions, attributes, and docs — not classes.
aliasshortens names,importbrings functions into scope,requireenables macros,useruns a macro that injects code.- Module attributes (
@) are compile-time. Document with@moduledoc,@doc, and@spec. - File names mirror module names. Tests mirror
lib/intest/. - Doctests in
@docexamples are real tests — ExUnit runs them. - Reach for
aliasfirst. The other directives need stronger justification.