This repository is nebulex_disk_lfu (a persistent disk-based cache adapter
with LFU eviction for Nebulex), not Nebulex core. When imported Nebulex
sections reference missing usage-rules/*.md paths or Nebulex-core files,
treat them as upstream guidance and prioritize this repository's local files
and modules.
- This local preface.
nebulex:workflowsection in this file.nebulex:nebulexsection in this file (as framework guidance).nebulex:elixir-styleandnebulex:elixirsections in this file.
lib/nebulex/adapters/disk_lfu.ex- Main disk LFU adapter implementation.lib/nebulex/adapters/disk_lfu/options.ex- Adapter option definitions/docs.lib/nebulex/adapters/disk_lfu/store.ex- Disk storage engine.lib/nebulex/adapters/disk_lfu/meta.ex- Metadata management.lib/nebulex/adapters/disk_lfu/helpers.ex- Shared adapter utilities.lib/nebulex/adapters/disk_lfu/transaction/options.ex- Transaction option definitions.test/nebulex/adapters/disk_lfu_test.exs- Adapter behavior tests.test/nebulex/adapters/disk_lfu_eviction_test.exs- LFU eviction tests.README.md- Public usage/configuration for this adapter.CHANGELOG.md- Adapter release history.
Start here, then read these at session start and refer back while coding:
usage-rules/nebulex.md- Nebulex-specific rulesusage-rules/elixir-style.md- Style guidelinesusage-rules/elixir.md- Core Elixir rules
If these files are not found, check
AGENTS.mdor the localusage-rules/folder instead.
When rules conflict, prioritize them in this order:
usage-rules/workflow.mdusage-rules/nebulex.mdusage-rules/elixir-style.mdusage-rules/elixir.md
If these files are not found, apply the same precedence to the corresponding sections in
AGENTS.md.
At the start of each session, quickly establish context:
- Run
git status --shortandgit diff --name-onlyto check local modifications and currently touched files. - Run
git log --oneline -20to see recent changes. - Run
git branch -ato see active branches and current branch. - Read
README.mdand the latest section ofCHANGELOG.md. - Check
.tool-versionsor theelixirversion inmix.exsfor supported Elixir/OTP versions.
If on a feature branch, also run:
git log --oneline main..HEADto see the branch's commits.git diff main...HEADto understand the branch's full scope.
When relevant to the task:
- Check open issues and PRs with
gh issue listandgh pr list. Ifghis unavailable or unauthenticated, skip this step.
- Latest release: check the latest section in
CHANGELOG.md. - Read
CHANGELOG.mdfor recent features, breaking changes, and the project's direction. - Changelog policy: user-visible behavior changes should be documented; internal refactors may be omitted before a release.
- Read the PR description and all comments:
gh pr view <number>andgh pr view <number> --comments. - Review the diff:
gh pr diff <number>. - Check
CHANGELOG.mdto understand if the change aligns with the project's direction. - Verify code follows
usage-rules/conventions (Elixir patterns, Nebulex-specific rules, style guidelines). - Run the validation commands (see below) before approving.
- Provide constructive feedback referencing specific lines and conventions.
- Structure review feedback as:
- findings first (ordered by severity, with file:line references),
- open questions/assumptions,
- brief summary last.
- Branch from
mainwith a descriptive branch name (e.g.,fix/some-bug,feat/cache-warming-support). - Update
CHANGELOG.mdunder the appropriate section (Enhancements, Bug fixes, Backward-incompatible changes). - Run all validation commands before pushing.
- Reference related GitHub issues in the PR description (e.g., "Closes #123").
- Use
gh pr createwith a clear title and description.
Commit messages must follow the Conventional Commits format:
type(scope): short summary
featfixrefactordocstestchoreperfcibuild
- Use imperative mood in the summary.
- Keep the summary lowercase and do not end it with a period.
- Use a scope when it adds clarity (e.g.,
cache,decorators,telemetry,workflow). - Keep the first line concise (ideally <= 72 chars).
feat(cache): add runtime option validation for ttlfix(decorators): handle nested context pop safelychore(workflow): refine session bootstrap steps
Before submitting or approving any code change, run:
# Quick targeted validation (recommended first)
mix test path/to/changed_test.exs
# Format check
mix format --check-formatted
# Static analysis
mix credo --strict
# Documentation (if docs were changed)
mix docsThen run full-suite validation before merge/release:
mix test.ciNebulex is a fast, flexible, and extensible caching library for Elixir that provides:
- Multiple cache adapters (local, distributed, multilevel, partitioned, coherent).
- Declarative decorator-based caching inspired by Spring Cache Abstraction.
- OTP design patterns and fault tolerance.
- Telemetry instrumentation.
- Event streaming via
Nebulex.Streamsfor distributed invalidation. - Support for TTL, eviction policies, transactions, and more.
| Path | Purpose |
|---|---|
lib/nebulex/cache.ex |
Main Cache API |
lib/nebulex/cache/ |
Cache feature modules (KV, Options, etc.) |
lib/nebulex/adapter.ex |
Adapter behaviour and macros |
lib/nebulex/adapters/ |
Built-in adapter modules in core (e.g., Nil, common helpers) |
lib/nebulex/caching/decorators.ex |
Decorator implementation |
lib/nebulex/caching/ |
Caching internals (Context, Runtime) |
lib/nebulex/event.ex |
Cache event types |
lib/nebulex/telemetry.ex |
Telemetry instrumentation |
lib/nebulex/utils.ex |
Shared utilities |
mix.exs |
Dependencies and project config |
CHANGELOG.md |
Release history and breaking changes |
test/ |
Test suite (mirrors lib/ structure) |
guides/ |
User-facing guides, behavioral references, and examples |
guides/introduction/ |
Getting started, available adapters |
guides/learning/ |
Declarative caching, cache patterns, adapter creation, info API |
guides/upgrading/v3.0.md |
v3 migration guide |
Nebulex v3 separates adapters into dedicated packages:
nebulex- Core.nebulex_local- Local cache adapter.nebulex_distributed- Partitioned, multilevel, and coherent adapters.nebulex_redis_adapter- Adapter for Redis (including Redis Cluster).nebulex_adapters_cachex- Adapter forcachexlibrary.nebulex_disk_lfu- Persistent disk-based cache adapter with LFU eviction for Nebulex.
See
guides/introduction/nbx-adapters.mdfor more information about the available adapters.
Add the required dependencies to your mix.exs:
defp deps do
[
{:nebulex, "~> 3.0"},
{:nebulex_local, "~> 3.0"},
# For distributed caching
{:nebulex_distributed, "~> 3.0"},
# Required for caching decorators
{:decorator, "~> 1.4"},
# Optional but highly recommended for observability
{:telemetry, "~> 1.0"}
]
endNote: The
:telemetrydependency is optional but highly recommended for production environments. It enables cache metrics, monitoring, and integration with observability tools.
- Caches MUST be defined using
use Nebulex.Cachewith:otp_appand:adapteroptions. - Caches should be started in the application supervision tree, not manually.
- Use descriptive cache module names that indicate their purpose
(e.g.,
MyApp.LocalCache,MyApp.UserCache). - Use
adapter_optsfor compile-time adapter options like:primary_storage_adapter.
Example:
defmodule MyApp.Cache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Local
endWith compile-time adapter options (Partitioned/Coherent adapters):
defmodule MyApp.PartitionedCache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Partitioned,
adapter_opts: [primary_storage_adapter: Nebulex.Adapters.Local]
endAvoid putting adapter options at the top level:
# WRONG - will raise an error
defmodule MyApp.PartitionedCache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Partitioned,
primary_storage_adapter: Nebulex.Adapters.Local
endNebulex supports multiple cache topologies:
- Local (
Nebulex.Adapters.Local): Single-node generational cache with automatic garbage collection. - Partitioned (
Nebulex.Adapters.Partitioned): Distributed cache with data sharded across cluster nodes using consistent hashing. - Multilevel (
Nebulex.Adapters.Multilevel): Hierarchical cache with multiple levels (e.g., L1 local + L2 distributed). - Coherent (
Nebulex.Adapters.Coherent): Local cache with distributed invalidation viaNebulex.Streams. Ideal for read-heavy workloads.
Use inclusion_policy (not the deprecated model) for multilevel caches:
config :my_app, MyApp.NearCache,
inclusion_policy: :inclusive,
levels: [
{MyApp.NearCache.L1, gc_interval: :timer.hours(12)},
{MyApp.NearCache.L2, primary: [gc_interval: :timer.hours(12)]}
]- All adapters MUST implement the
Nebulex.Adapterbehaviour. - Adapters MUST implement
c:init/1returning{:ok, child_spec, adapter_meta}. - Adapter functions MUST return
{:ok, value}or{:error, reason}tuples. - Use
wrap_error/2fromNebulex.Utilsto wrap errors consistently. - Implement optional behaviours as needed:
Nebulex.Adapter.KV,Nebulex.Adapter.Queryable, etc.
- Use
defcommand/2macro fromNebulex.Adapterto build public command wrappers. - Use
defcommandp/2for private command wrappers. - Command functions automatically handle telemetry, metadata, and error wrapping.
- The first parameter to commands should always be
name(the cache name or PID). - The last parameter should always be
opts(keyword list).
Example:
defcommand fetch(name, key, opts)
defcommandp do_put(name, key, value, on_write, ttl, keep_ttl?, opts), command: :put- Read operations that can fail MUST return
{:ok, value}or{:error, %Nebulex.KeyError{}}for missing keys. - Write operations MUST return
{:ok, true}for success or{:ok, false}for conditional failures (e.g.,put_new). - Delete operations MUST return
:okregardless of whether the key existed. - NEVER return bare
:erroratoms; always use{:error, reason}tuples.
- Provide bang versions (
!) of functions that unwrap{:ok, value}or raise exceptions. - Bang functions MUST use
unwrap_or_raise/1fromNebulex.Utils. - Functions that return
:okshould have bang versions that also return:ok. - Functions that return
{:ok, boolean}should have bang versions that return the boolean.
Example:
def fetch!(name, key, opts) do
unwrap_or_raise fetch(name, key, opts)
end
def put!(name, key, value, opts) do
_ = unwrap_or_raise do_put(name, key, value, :put, opts)
:ok
endUse fetch_or_store/3 and get_or_store/3 for read-through caching:
# Returns {:ok, value} or {:error, reason}
cache.fetch_or_store("user:123", fn ->
case Repo.get(User, 123) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end)
# Returns value directly (raises on error)
cache.get_or_store!("user:123", fn ->
Repo.get!(User, 123)
end)fetch_or_store/3- Returns{:ok, value}from cache or fallback function. If the fallback returns{:error, reason}, it propagates the error without caching.get_or_store/3- Simpler variant that stores the direct return value from the fallback function.
- Use
Nebulex.Cache.Optionsmodule for option validation. - Call
Options.validate_runtime_shared_opts!/1to validate runtime options. - Use
Options.pop_and_validate_timeout!/2for TTL and timeout options. - Use
Options.pop_and_validate_boolean!/2for boolean options. - Use
Options.pop_and_validate_integer!/2for integer options. - Validate options as early as possible, preferably at the beginning of the function.
Example:
def put(name, key, value, opts) do
{ttl, opts} = Options.pop_and_validate_timeout!(opts, :ttl)
{keep_ttl?, opts} = Options.pop_and_validate_boolean!(opts, :keep_ttl, false)
do_put(name, key, value, :put, ttl, keep_ttl?, opts)
end- All cache functions should accept
:telemetry,:telemetry_event, and:telemetry_metadataoptions. - Document adapter-specific options clearly in the module documentation.
- Use
use Nebulex.Cachingto enable decorator support in a module. - Configure default cache via
use Nebulex.Caching, cache: MyCache. - Always use decorators on functions, not on function heads with multiple clauses.
- Prefer module captures over anonymous functions for better performance:
match: &__MODULE__.match_fun/1. - Avoid capturing large data structures in decorator lambdas.
Invalid:
@decorate cacheable(key: id)
def get_user(nil), do: nil
def get_user(id) do
# logic
endValid:
@decorate cacheable(key: id)
def get_user(id) do
do_get_user(id)
end
defp do_get_user(nil), do: nil
defp do_get_user(id) do
# logic
end- Use
:keyoption to specify explicit cache keys; avoid relying solely on default key generation. - Use
:referencesfor implementing cache key references and memory-efficient caching. - Use
:matchoption to conditionally cache values (e.g.,match: &match_fun/1). - Use
:on_erroroption to control error handling (:raiseor:nothing). - Specify TTL via
:optsoption:opts: [ttl: :timer.hours(1)]. - Use
:transactionoption to wrap cache operations in a transaction, preventing race conditions and cache stampede.
Transaction example (prevents concurrent updates to the same key):
@decorate cacheable(key: id, transaction: true)
def get_user(id) do
Repo.get(User, id)
end- Use
@decorate cacheablefor read-through caching patterns. - Combine with
:referencesoption when the same value needs multiple cache keys. - Use
:matchfunction with references to ensure consistency (e.g., validating email matches).
Example:
@decorate cacheable(key: id)
def get_user(id) do
Repo.get(User, id)
end
@decorate cacheable(key: email, references: &(&1 && &1.id), match: &match_email(&1, email))
def get_user_by_email(email) do
Repo.get_by(User, email: email)
end
defp match_email(%{email: email}, email), do: true
defp match_email(_, _), do: false- Use
@decorate cache_putfor write-through caching patterns. - Always use
:matchoption to conditionally update cache (e.g., only on{:ok, value}). - Avoid using
cache_putandcacheableon the same function.
Example:
@decorate cache_put(key: user.id, match: &match_ok/1)
def update_user(user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
defp match_ok({:ok, user}), do: {true, user}
defp match_ok({:error, _}), do: false- Use
@decorate cache_evictfor cache invalidation. - Use
key: {:in, keys}to evict multiple keys at once. - Use
:all_entriesoption to clear the entire cache. - Use
:before_invocationoption to evict before function execution. - Use
:queryoption for complex eviction patterns based on match specifications.
Example:
@decorate cache_evict(key: {:in, [user.id, user.email]})
def delete_user(user) do
Repo.delete(user)
end
@decorate cache_evict(all_entries: true)
def clear_all_users do
Repo.delete_all(User)
end
@decorate cache_evict(query: &__MODULE__.query_for_tag/1)
def delete_by_tag(tag) do
# Delete logic
end
def query_for_tag(%{args: [tag]}) do
[{:entry, :"$1", %{tag: :"$2"}, :_, :_}, [{:"=:=", :"$2", tag}], [true]]
endNebulex.Streams provides event streaming for cache operations, enabling
distributed cache invalidation patterns.
Add use Nebulex.Streams to your cache module:
defmodule MyApp.Cache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Local
use Nebulex.Streams
endImplement custom stream handlers by using Nebulex.Streams.Handler:
defmodule MyApp.CacheEventHandler do
use Nebulex.Streams.Handler
@impl true
def handle_event(event, state) do
# Handle cache events (put, delete, etc.)
{:cont, state}
end
endThe Nebulex.Adapters.Coherent adapter uses Nebulex.Streams to provide
local caching with distributed invalidation:
- Each node maintains its own local cache.
- Write operations trigger invalidation events via
Phoenix.PubSub. - Other nodes delete the invalidated keys from their local caches.
- Next read on other nodes results in a cache miss, fetching fresh data.
defmodule MyApp.CoherentCache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Coherent,
adapter_opts: [primary_storage_adapter: Nebulex.Adapters.Local]
endConfiguration:
config :my_app, MyApp.CoherentCache,
primary: [
gc_interval: :timer.hours(12),
max_size: 1_000_000
]- Use
deftests domacro for shared test suites that can run across multiple adapters. - Structure tests with
describeblocks grouping related functionality. - Use context fixtures with
%{cache: cache}for test setup. - Test both successful and error scenarios for each function.
Example:
defmodule MyAdapterTest do
import Nebulex.CacheCase
deftests do
describe "put/3" do
test "puts the given entry into the cache", %{cache: cache} do
assert cache.put(:key, :value) == :ok
assert cache.fetch!(:key) == :value
end
test "raises when invalid option is given", %{cache: cache} do
assert_raise NimbleOptions.ValidationError, fn ->
cache.put(:key, :value, ttl: "invalid")
end
end
end
end
end- Use
assert cache.function() == expected_valuefor exact equality. - Use
assert_raise ErrorType, ~r"message pattern"for exception testing. - Test edge cases:
nil, boolean values (true,false), empty collections. - Test both normal and bang (
!) versions of functions. - Avoid pattern matching in assertions when the full value is known (use direct equality).
Invalid:
assert {:ok, value} = cache.fetch(:key)Valid:
assert cache.fetch(:key) == {:ok, expected_value}- Emit telemetry events for all cache commands when
:telemetryoption istrue. - Use
:telemetry_prefixoption to customize event names (defaults to[:cache_name, :cache]). - Provide comprehensive metadata:
:adapter_meta,:command,:args,:result. - Support custom
:telemetry_eventand:telemetry_metadataoptions per command.
- Use
Nebulex.Telemetry.span/3for span events (start, stop, exception). - Include measurements like
:durationand:system_time. - Document all telemetry events in module documentation with measurement and metadata keys.
- Provide example telemetry handlers in documentation.
- Use
Nebulex.Errorfor general cache errors. - Use
Nebulex.KeyErrorfor missing key errors. - Use
Nebulex.CacheNotFoundErrorfor dynamic cache lookup failures. - Wrap adapter-specific errors using
wrap_error/2fromNebulex.Utils.
- Adapter functions should wrap errors consistently using
wrap_error/2. - Include relevant context in error metadata (
:key,:command,:reason). - Preserve original error information in the
:reasonfield.
Example:
def fetch(_adapter_meta, key, opts) do
case do_fetch(key) do
{:ok, value} -> {:ok, value}
{:error, :not_found} -> wrap_error Nebulex.KeyError, key: key
{:error, reason} -> wrap_error Nebulex.Error, reason: reason, command: :fetch, key: key
end
end- Provide explicit keys in decorators when possible; avoid relying on default key generation.
- For complex keys, use module captures:
key: &MyModule.generate_key/1. - Keep captured data in decorator lambdas small; fetch large configs inside functions.
- Use cache key references (
:referencesoption) to avoid storing duplicate values. - Store references in a local cache and values in a remote cache (e.g., Redis) for optimization.
- Set TTL for references to prevent dangling keys.
- Use external references with
keyref(key, cache: AnotherCache)for cross-cache references.
- Use
Streamfor large result sets instead of loading all data at once. - Leverage
Task.async_stream/3for concurrent cache operations when appropriate. - Set appropriate TTL values to balance freshness and performance.
- Use
put_all/2for batch operations instead of multipleput/3calls.
- Start with a clear
@moduledocexplaining the purpose and main features, except modules that useNimbleOptionsfor option documentation. - Options documented using
NimbleOptionsshould provide functions to insert that documentation into the module docs. Therefore, it is not required to document an option in themoduledocor in the function@docif it is already inserted usingNimbleOptions. For example,#{Nebulex.Cache.Options.start_link_options_docs()}. - Include usage examples in module documentation.
- Document all compile-time options.
- Document all runtime shared options.
- Provide telemetry event documentation with measurements and metadata.
- The maximum text length is 80 characters, and you should aim to adhere to this limit. However, there are special cases where exceeding it is acceptable. For example, you may exceed the limit for a link (e.g., my link) or a code snippet that only exceeds the limit by a few characters (e.g., 1 or 2). If a code snippet exceeds the 80-character limit by more than 1 or 2 characters, format it using the Elixir formatter.
- When you make a change to the documentation, use
mix docsto validate it.
- Use
@docfor all public functions. - Include
@typedocfor all custom types. - Provide examples in function documentation using doctests when applicable.
- Document all options with descriptions and default values.
- Group related functions using
@doc group: "Group Name". - Follow the same 80-character line-length guidance described in "Module Documentation," including the same exceptions for links and short formatter-friendly code snippets.
- When you make a change to the documentation, use
mix docsto validate it.
- Avoid obvious comments; code should be self-explanatory.
- Use comments for complex algorithms or non-obvious business logic. Use a
single
#for code comments. E.g.,# My comment .... - For separating sections in a module, use
##. E.g.,## API,## Private functions, etc. - Mark internal functions with
@doc falseor@moduledoc false. - Use
# Inline common instructionsfollowed by@compile inline: [function_name: arity]. - The maximum text length is 80 characters; use multiple lines if the comment exceeds the limit.
- Adapter modules:
Nebulex.Adapters.*(e.g.,Nebulex.Adapters.Local). - Cache modules:
<App>.Cacheor<App>.<Context>Cache(e.g.,MyApp.Cache,MyApp.UserCache). - Behaviour modules:
Nebulex.Adapter.<Feature>(e.g.,Nebulex.Adapter.KV).
- Use descriptive function names:
fetch/2,put/3,delete/2,has_key?/2. - Bang versions:
fetch!/2,put!/3,delete!/2. - Private helpers: prefix with
do_(e.g.,do_fetch,do_put). - Predicate functions: suffix with
?(e.g.,has_key?/2,expired?/2).
- Cache instance:
cache. - Adapter metadata:
adapter_meta. - Options:
opts. - Keys:
keyorkeys. - Values:
valueorvalues. - TTL:
ttl.
- Main cache API:
lib/nebulex/cache.ex. - Adapter behaviour:
lib/nebulex/adapter.ex. - Adapter implementations:
lib/nebulex/adapters/<adapter_name>.ex. - Cache features:
lib/nebulex/cache/<feature>.ex. - Decorators:
lib/nebulex/caching/decorators.ex. - Mix tasks:
lib/mix/tasks/<task_name>.ex.
- Keep related functionality together (e.g., all KV operations in
Nebulex.Cache.KV). - Use nested modules for options, helpers, and internal implementation details.
- Separate public API from internal implementation.
Nebulex provides mix nbx.gen.cache to generate a cache module:
mix nbx.gen.cache -c MyApp.Cache
This generates a cache using Nebulex.Adapters.Local by default. For other
adapters (Partitioned, Multilevel, Coherent), manually update the generated
module:
# Generated (Local adapter by default)
defmodule MyApp.Cache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Local
end
# Manually update for Partitioned
defmodule MyApp.PartitionedCache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Partitioned,
adapter_opts: [primary_storage_adapter: Nebulex.Adapters.Local]
end- Do NOT use decorators on multi-clause functions without proper wrapper functions.
- Do NOT forget to validate options at the beginning of functions.
- Do NOT return inconsistent error types; always use tuples or raise exceptions via bang functions.
- Do NOT capture large data structures in decorator lambdas.
- Do NOT forget to handle
nil, boolean, and edge case values in tests. - Do NOT use
cache_putandcacheabledecorators on the same function. - Do NOT forget to evict cache references when using
:referencesoption; use TTL or explicit eviction. - Do NOT implement adapter callbacks without proper error wrapping.
- Do NOT use
primary_storage_adapterat the top level; wrap it inadapter_opts: [primary_storage_adapter: ...]. - Do NOT use the deprecated
modeloption for multilevel caches; useinclusion_policyinstead. - Do NOT use
mix nbx.gen.cache.partitionedormix nbx.gen.cache.multilevel(they don't exist); usemix nbx.gen.cacheand manually configure the adapter. - Do NOT use the old
all/2callback; useget_all/2with query spec instead. - Do NOT use
:keysoption in decorators; usekey: {:in, keys}instead. - Do NOT use
:key_generatoroption; usekey: &MyModule.generate_key/1. - Do NOT skip telemetry support in adapter implementations.
- Do NOT use pattern matching in test assertions when the full value is known.
- Maintain backward compatibility when adding new options (use default values).
- Deprecate old APIs before removal; provide migration path in documentation.
- Follow semantic versioning strictly: major version for breaking changes.
- Test against multiple Elixir and OTP versions in CI.
- Keep dependencies minimal and well-justified.
- Prefer standard library solutions over external dependencies.
- Use optional dependencies for non-core features.
- Document all dependencies in README with their purpose.
Most of these guidelines are based on The Elixir Style Guide by Christopher Adams, licensed under CC-BY-3.0.
-
Use blank lines between
defs to break up a function into logical paragraphs. For example:def some_function(some_data) do some_data |> other_function() |> List.first() end def some_function do result end def some_other_function do another_result end def a_longer_function do one two three four end
-
If the function head and
do:clause are too long to fit on the same line, putdo:on a new line, indented one level more than the previous line. For example:def some_function([:foo, :bar, :baz] = args), do: Enum.map(args, fn arg -> arg <> " is on a very long line!" end)
When the
do:clause starts on its own line, treat it as a multiline function by separating it with blank lines.# not preferred def some_function([]), do: :empty def some_function(_), do: :very_long_line_here # preferred def some_function([]), do: :empty def some_function(_), do: :very_long_line_here
-
Add a blank line after a multiline assignment as a visual cue that the assignment is 'over'. For example:
# not preferred some_string = "Hello" |> String.downcase() |> String.trim() another_string <> some_string # preferred some_string = "Hello" |> String.downcase() |> String.trim() another_string <> some_string
# also not preferred something = if x == 2 do "Hi" else "Bye" end String.downcase(something) # preferred something = if x == 2 do "Hi" else "Bye" end String.downcase(something)
-
Use parentheses when defining a type.
# not preferred @type name :: atom # preferred @type name() :: atom
The rules in this section may not be applied by the code formatter, but they are generally preferred practice.
-
Keep single-line
defclauses of the same function together, but separate multilinedefs with a blank line. For example:def some_function(nil), do: {:error, "No Value"} def some_function([]), do: :ok def some_function([first | rest]) do some_function(rest) end
-
If you have more than one multiline
def, do not use single-linedefs. For example:def some_function(nil) do {:error, "No Value"} end def some_function([]) do :ok end def some_function([first | rest]) do some_function(rest) end def some_function([first | rest], opts) do some_function(rest, opts) end
-
Use the pipe operator to chain functions together. For example:
# not preferred String.trim(String.downcase(some_string)) # preferred some_string |> String.downcase() |> String.trim() # Multiline pipelines are not further indented some_string |> String.downcase() |> String.trim() # Multiline pipelines on the right side of a pattern match # should be indented on a new line sanitized_string = some_string |> String.downcase() |> String.trim()
-
Avoid using the pipe operator just once, unless the first expression is a function. For example:
# not preferred some_string |> String.downcase() # preferred String.downcase(some_string) # not preferred Version.parse(System.version()) # preferred System.version() |> Version.parse()
-
Use parentheses when a
defhas arguments, and omit them when it doesn't. For example:# not preferred def some_function arg1, arg2 do # body omitted end def some_function() do # body omitted end # preferred def some_function(arg1, arg2) do # body omitted end def some_function do # body omitted end
-
Use
do:for single-lineif/unlessstatements.# preferred if some_condition, do: # some_stuff
-
Use
trueas the last condition of thecondspecial form when you need a clause that always matches.# not preferred cond do 1 + 2 == 5 -> "Nope" 1 + 3 == 5 -> "Uh, uh" :else -> "OK" end # preferred cond do 1 + 2 == 5 -> "Nope" 1 + 3 == 5 -> "Uh, uh" true -> "OK" end
-
Use
snake_casefor atoms, functions and variables.# not preferred :"some atom" :SomeAtom :someAtom someVar = 5 def someFunction do ... end # preferred :some_atom some_var = 5 def some_function do ... end
-
Use
CamelCasefor modules (keep acronyms like HTTP, RFC, XML uppercase).# not preferred defmodule Somemodule do ... end defmodule Some_Module do ... end defmodule SomeXml do ... end # preferred defmodule SomeModule do ... end defmodule SomeXML do ... end
-
Functions that return a boolean (
trueorfalse) should be named with a trailing question mark.def cool?(var) do String.contains?(var, "cool") end
-
Boolean checks that can be used in guard clauses (custom guards) should be named with an
is_prefix.defguard is_cool(var) when var == "cool" defguard is_very_cool(var) when var == "very cool"
-
Write expressive code and try to convey your program's intention through control-flow, structure and naming.
-
Comments longer than a word are capitalized, and sentences use punctuation. Use one space after periods.
# not preferred
# these lowercase comments are missing punctuation
# preferred
# Capitalization example
# Use punctuation for complete sentences.- Limit comment lines to 80 characters.
-
Annotations should usually be written on the line immediately above the relevant code.
-
The annotation keyword is uppercase, and is followed by a colon and a space, then a note describing the problem.
# TODO: Deprecate in v1.5.
def some_function(arg), do: {:ok, arg}- In cases where the problem is so obvious that any documentation would be redundant, annotations may be left with no note. This usage should be the exception and not the rule.
start_task()
# FIXME
Process.sleep(5000)-
Use
TODOto note missing features or functionality that should be added at a later date. -
Use
FIXMEto note broken code that needs to be fixed. -
Use
OPTIMIZEto note slow or inefficient code that may cause performance problems. -
Use
HACKto note code smells where questionable coding practices were used and should be refactored away. -
Use
REVIEWto note anything that should be looked at to confirm it is working as intended. For example:REVIEW: Are we sure this is how the client does X currently? -
Use other custom annotation keywords if it feels appropriate, but be sure to document them in your project's
READMEor similar.
- When defining a constant, pick a descriptive name that reflects the intention or usage of the constant and add a comment with a short description.
Not preferred:
@retries 10Preferred:
# Default HTTP retries
@http_retries 10- When the constant is a timeout in milliseconds, use
:timermodule instead of explicit value (e.g.,:timer.seconds/1,:timer.minutes/1,:timer.hours/1).
Not preferred:
# Default HTTP request timeout in milliseconds
@http_request_timeout 10_000Preferred:
# Default HTTP request timeout in milliseconds
@http_request_timeout :timer.seconds(10)- When the constant is a list of atoms or strings, a regex, or anything that can be expressed using Sigils, then use Sigils.
Not preferred:
# User types
@user_types [:admin, :editor, :customer]
# Supported country codes
@user_types ["US", "ES", "CO"]Preferred:
# User types
@user_types ~w(admin editor customer)a
# Supported country codes
@user_types ~w(US ES CO)-
List module attributes, directives, and macros in the following order:
@moduledoc@behaviouruseimportrequirealias@module_attributedefstruct@type@callback@macrocallback@optional_callbacksdefmacro,defmodule,defguard,def, etc.
Add a blank line between each grouping, and sort the terms (like module names) alphabetically. Here's an overall example of how you should order things in your modules:
defmodule MyModule do @moduledoc """ An example module """ @behaviour MyBehaviour use GenServer import Something import SomethingElse require Integer alias My.Long.Module.Name alias My.Other.Module.Example @module_attribute :foo @other_attribute 100 defstruct [:name, params: []] @type params :: [{binary, binary}] @callback some_function(term) :: :ok | {:error, term} @macrocallback macro_name(term) :: Macro.t() @optional_callbacks macro_name: 1 @doc false defmacro __using__(_opts), do: :no_op @doc """ Determines when a term is `:ok`. Allowed in guards. """ defguard is_ok(term) when term == :ok @impl true def init(state), do: {:ok, state} # Define other functions here. end
-
Use the
__MODULE__pseudo variable when a module refers to itself. This avoids having to update any self-references when the module name changes.defmodule SomeProject.SomeModule do defstruct [:name] def name(%__MODULE__{name: name}), do: name end
-
Place
@typedocand@typedefinitions together, and separate each pair with a blank line.defmodule SomeModule do @moduledoc false @typedoc "The name" @type name() :: atom() @typedoc "The result" @type result() :: {:ok, any()} | {:error, any()} ... end
-
Name the main type for a module
t(), for example: the type specification for a struct.defstruct name: nil, params: [] @typedoc "The type for ..." @type t() :: %__MODULE__{ name: String.t() | nil, params: Keyword.t() }
-
Place specifications right before the function definition, after the
@doc, without separating them by a blank line.@doc """ Some function description. """ @spec some_function(any()) :: result() def some_function(some_data) do {:ok, some_data} end
-
Use a list of atoms for struct fields that default to
nil, followed by the other keywords.# not preferred defstruct name: nil, params: nil, active: true # preferred defstruct [:name, :params, active: true]
-
Omit square brackets when the argument of a
defstructis a keyword list.# not preferred defstruct [params: [], active: true] # preferred defstruct params: [], active: true # required - brackets are not optional, with at least one atom in the list defstruct [:name, params: [], active: true]
-
If a struct definition spans multiple lines, put each element on its own line, keeping the elements aligned.
defstruct foo: "test", bar: true, baz: false, qux: false, quux: 1
If a multiline struct requires brackets, format it as a multiline list:
defstruct [ :name, params: [], active: true ]
-
Make exception names end with a trailing
Error.# not preferred defmodule BadHTTPCode do defexception [:message] end defmodule BadHTTPCodeException do defexception [:message] end # preferred defmodule BadHTTPCodeError do defexception [:message] end
-
Use lowercase error messages when raising exceptions, with no trailing punctuation.
# not preferred raise ArgumentError, "This is not valid." # preferred raise ArgumentError, "this is not valid"
-
Always use the special syntax for keyword lists.
# not preferred some_value = [{:a, "baz"}, {:b, "qux"}] # preferred some_value = [a: "baz", b: "qux"]
-
Use the shorthand key-value syntax for maps when all of the keys are atoms.
# not preferred %{:a => 1, :b => 2, :c => 0} # preferred %{a: 1, b: 2, c: 3}
-
Use the verbose key-value syntax for maps if any key is not an atom.
# not preferred %{"c" => 0, a: 1, b: 2} # preferred %{:a => 1, :b => 2, "c" => 0}
-
When writing ExUnit assertions, put the expression being tested to the left of the operator, and the expected result to the right, unless the assertion is a pattern match.
# not preferred assert true == actual_function(1) # preferred assert actual_function(1) == true # required - the assertion is a pattern match, and the `expected` variable is used later assert {:ok, expected} = actual_function(3) assert expected.atom == :atom assert expected.int == 123 # preferred - if the right side is known, even if it is a tuple assert actual_function(11) == {:ok, %{atom: :atom, int: 123}} # preferred - if the right side is known (using a variable) expected = %{atom: :atom, int: 123} assert actual_function(11) == {:ok, expected}
-
Use a blank line for the return or final statement (unless it is a single line).
Avoid:
def some_function(arg) do Logger.info("Arg: #{inspect(some_data)}") :ok endPrefer:
def some_function(some_data) do Logger.info("Arg: #{inspect(some_data)}") :ok end -
Use multi-line when a function returns with a pipe.
Avoid:
def some_function(some_data) do some_data |> other_function() |> List.first() endPrefer:
def some_function(some_data) do some_data |> other_function() |> List.first() end -
Use
withwhen only one case has to be handled, either the success or the error.Avoid:
caseforwarding the same resultcase some_call() do :ok -> :ok {:error, reason} = error -> Logger.error("Error: #{inspect(reason)}") error endPrefer:
withhandling only the needed casewith {:error, reason} = error <- some_call() do Logger.error("Error: #{inspect(reason)}") error end
- Use pattern matching over conditional logic when possible
- Prefer to match on function heads instead of using
if/elseorcasein function bodies %{}matches ANY map, not just empty maps. Usemap_size(map) == 0guard to check for truly empty maps
- Use
{:ok, result}and{:error, reason}tuples for operations that can fail - Avoid raising exceptions for control flow
- Use
withfor chaining operations that return{:ok, _}or{:error, _} - Bang functions (
!) that explicitly raise exceptions on failure are acceptable (e.g.,File.read!/1,String.to_integer!/1) - Avoid rescuing exceptions unless for a very specific case (e.g., cleaning up resources, logging critical errors)
- Elixir has no
returnstatement, nor early returns. The last expression in a block is always returned. - Don't use
Enumfunctions on large collections whenStreamis more appropriate - Avoid nested
casestatements - refactor to a singlecase,withor separate functions - Don't use
String.to_atom/1on user input (memory leak risk) - Lists and enumerables cannot be indexed with brackets. Use pattern matching or
Enumfunctions - Prefer
Enumfunctions likeEnum.reduceover recursion - When recursion is necessary, prefer to use pattern matching in function heads for base case detection
- Using the process dictionary is typically a sign of unidiomatic code
- Only use macros if explicitly requested
- There are many useful standard library functions, prefer to use them where possible
- Use guard clauses:
when is_binary(name) and byte_size(name) > 0 - Prefer multiple function clauses over complex conditional logic
- Name functions descriptively:
calculate_total_price/2notcalc/2 - Predicate function names should not start with
isand should end in a question mark. - Names like
is_thingshould be reserved for guards
- Use structs over maps when the shape is known:
defstruct [:name, :age] - Prefer keyword lists for options:
[timeout: 5000, retries: 3] - Use maps for dynamic key-value data
- Prefer to prepend to lists
[new | list]notlist ++ [new]
- Use
mix helpto list available mix tasks - Use
mix help task_nameto get docs for an individual task - Read the docs and options fully before using tasks
- Run tests in a specific file with
mix test test/my_test.exsand a specific test with the line numbermix test path/to/test.exs:123 - Limit the number of failed tests with
mix test --max-failures n - Use
@tagto tag specific tests, andmix test --only tagto run only those tests - Use
assert_raisefor testing expected exceptions:assert_raise ArgumentError, fn -> invalid_function() end - Use
mix help testfor full documentation on running tests
- Use
dbg/1to print values while debugging. This will display the formatted value and other relevant information in the console.
-
Elixir lists do not support index based access via the access syntax
Never do this (invalid):
i = 0 mylist = ["blue", "green"] mylist[i]Instead, always use
Enum.at, pattern matching, orListfor index based list access, i.e.:i = 0 mylist = ["blue", "green"] Enum.at(mylist, i) -
Elixir variables are immutable, but can be rebound, so for block expressions like
if,case,cond, etc you must bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, i.e.:# INVALID: we are rebinding inside the `if` and the result never gets assigned if connected?(socket) do socket = assign(socket, :val, val) end # VALID: we rebind the result of the `if` to a new variable socket = if connected?(socket) do assign(socket, :val, val) end -
Never nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
-
Never use map access syntax (
changeset[:field]) on structs as they do not implement the Access behaviour by default. For regular structs, you must access the fields directly, such asmy_struct.fieldor use higher level APIs that are available on the struct if they exist,Ecto.Changeset.get_field/2for changesets -
Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common
Time,Date,DateTime, andCalendarinterfaces by accessing their documentation as necessary. Never install additional dependencies unless asked or for date/time parsing (which you can use thedate_time_parserpackage) -
Don't use
String.to_atom/1on user input (memory leak risk) -
Predicate function names should not start with
is_and should end in a question mark. Names likeis_thingshould be reserved for guards -
Elixir's built-in OTP primitives, such as
DynamicSupervisorandRegistry, require names in the child spec, such as{DynamicSupervisor, name: MyApp.MyDynamicSup}, then you can useDynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec) -
Use
Task.async_stream(collection, callback, options)for concurrent enumeration with back-pressure. The majority of times you will want to passtimeout: :infinityas option
- Read the docs and options before using tasks (by using
mix help task_name) - To debug test failures, run tests in a specific file with
mix test test/my_test.exsor run all previously failed tests withmix test --failed mix deps.clean --allis almost never needed. Avoid using it unless you have good reason
-
The
inoperator in guards requires a compile-time known value on the right side (literal list or range)Never do this (invalid): using a variable which is unknown at compile time
def t(x, y) when x in y, do: {x, y}This will raise
ArgumentError: invalid right argument for operator "in", it expects a compile-time proper list or compile-time range on the right side when used in guard expressionsValid: use a known value for the list or range
def t(x, y) when x in [1, 2, 3], do: {x, y} def t(x, y) when x in 1..10, do: {x, y} -
In tests, avoid using
assertwith pattern matching when the expected value is fully known. Use direct equality comparison instead for clearer test failuresAvoid:
assert {:ok, ^value} = testing() assert {:error, :not_found} = fetch()Prefer:
assert testing() == {:ok, value} assert fetch() == {:error, :not_found}Exception: Pattern matching is acceptable when you only want to assert part of a complex structure
# OK: asserting only specific fields of a large struct/map assert {:ok, %{id: ^id}} = get_order() -
In tests, avoid duplicating test data across multiple tests. Use constants, fixture files, or private fixture functions instead
Avoid: Duplicating test data
test "validates user email" do assert valid_email?("user@example.com") end test "creates user" do assert create_user("user@example.com") endPrefer: Use module attributes for constants or fixture functions
@valid_email "user@example.com" test "validates user email" do assert valid_email?(@valid_email) end test "creates user" do assert create_user(@valid_email) endFor complex data structures, create fixture functions:
defp user_fixture(attrs \\ %{}) do %User{ name: "John Doe", email: "john@example.com", age: 30 } |> Map.merge(attrs) end
A config-driven dev tool for Elixir projects to manage AGENTS.md files and agent skills from dependencies
Many packages have usage rules, which you should thoroughly consult before taking any action. These usage rules contain guidelines and rules directly from the package authors. They are your best source of knowledge for making decisions.
When looking for docs for modules & functions that are dependencies of the current project,
or for Elixir itself, use mix usage_rules.docs
# Search a whole module
mix usage_rules.docs Enum
# Search a specific function
mix usage_rules.docs Enum.zip
# Search a specific function & arity
mix usage_rules.docs Enum.zip/1
You should also consult the documentation of any tools you are using, early and often. The best
way to accomplish this is to use the usage_rules.search_docs mix task. Once you have
found what you are looking for, use the links in the search results to get more detail. For example:
# Search docs for all packages in the current application, including Elixir
mix usage_rules.search_docs Enum.zip
# Search docs for specific packages
mix usage_rules.search_docs Req.get -p req
# Search docs for multi-word queries
mix usage_rules.search_docs "making requests" -p req
# Search only in titles (useful for finding specific functions/modules)
mix usage_rules.search_docs "Enum.zip" --query-by title
- Keep state simple and serializable
- Handle all expected messages explicitly
- Use
handle_continue/2for post-init work - Implement proper cleanup in
terminate/2when necessary
- Use
GenServer.call/3for synchronous requests expecting replies - Use
GenServer.cast/2for fire-and-forget messages. - When in doubt, use
callovercast, to ensure back-pressure - Set appropriate timeouts for
call/3operations
- Set up processes such that they can handle crashing and being restarted by supervisors
- Use
:max_restartsand:max_secondsto prevent restart loops
- Use
Task.Supervisorfor better fault tolerance - Handle task failures with
Task.yield/2orTask.shutdown/2 - Set appropriate task timeouts
- Use
Task.async_stream/3for concurrent enumeration with back-pressure