chore: merge elixir-conventions and oban-conventions into sources/
Absorbed content from rodin/elixir-conventions and rodin/oban-conventions into a sources/ directory. These are reference material — descriptive, not prescriptive. Patterns that prove broadly applicable get promoted into patterns/. Part of taxonomy cleanup (issue #4): - Pattern = prescriptive, follow these - Convention/Source = reference, study for ideas The original repos can now be archived.
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Sources
|
||||
|
||||
Reference material extracted from specific projects. Study for ideas, don't copy blindly.
|
||||
|
||||
These are **descriptive** — they document what a project does and why.
|
||||
The `patterns/` directory is **prescriptive** — it tells you what to do.
|
||||
|
||||
Patterns that prove broadly applicable get promoted from here into `patterns/`.
|
||||
The rest stays as reference for understanding how mature projects solve specific problems.
|
||||
|
||||
## Files
|
||||
|
||||
- `elixir-lang.md` — conventions from the elixir-lang/elixir source
|
||||
- `elixir-lang-analysis.md` — deeper analysis of elixir-lang source architecture
|
||||
- `oban.md` — patterns from oban-bg/oban (job processing, plugin system)
|
||||
@@ -0,0 +1,303 @@
|
||||
# Elixir Language Source: Architectural Conventions
|
||||
|
||||
How does José Valim and the Elixir core team build Elixir itself?
|
||||
What does the language source reveal about conventions that aren't
|
||||
documented anywhere else?
|
||||
|
||||
**Repo:** [elixir-lang/elixir](https://github.com/elixir-lang/elixir)
|
||||
|
||||
---
|
||||
|
||||
## 1. Repo Shape
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Size | 92M |
|
||||
| Source files | 567 .ex/.exs |
|
||||
| Erlang bootstrap | 33 .erl files |
|
||||
| Commits | 22,032 |
|
||||
| Contributors | 1,578 |
|
||||
| Test files | 208 |
|
||||
| Production files | 248 |
|
||||
| Test ratio | 1:1.2 |
|
||||
| TODOs (non-test) | 127 (all version-gated) |
|
||||
|
||||
### Organizational Philosophy
|
||||
|
||||
```
|
||||
lib/
|
||||
├── elixir/ # The language core (compiler + stdlib)
|
||||
│ ├── src/ # 33 Erlang files (bootstrap)
|
||||
│ └── lib/ # Elixir stdlib + compiler
|
||||
├── eex/ # Templating (independent OTP app)
|
||||
├── ex_unit/ # Testing framework (independent OTP app)
|
||||
├── iex/ # Interactive shell (independent OTP app)
|
||||
├── logger/ # Logging (independent OTP app)
|
||||
└── mix/ # Build tool (independent OTP app)
|
||||
```
|
||||
|
||||
Each component is a separate OTP application. They could theoretically
|
||||
be released independently. This is Elixir eating its own dog food —
|
||||
the umbrella project convention that Phoenix apps use comes directly
|
||||
from how the language itself is organized.
|
||||
|
||||
---
|
||||
|
||||
## 2. What the Codebase Values
|
||||
|
||||
### By size (what gets the most lines)
|
||||
|
||||
| Module | Lines | Role |
|
||||
|--------|-------|------|
|
||||
| `Kernel` | 7,102 | The implicit language surface |
|
||||
| `Module.Types.Descr` | 6,301 | Set-theoretic type descriptions |
|
||||
| `Enum` | 5,242 | Collection operations |
|
||||
| `String` | 3,263 | First-class string concept |
|
||||
| `Macro` | 3,102 | Metaprogramming foundation |
|
||||
| `Exception` | 2,720 | Error taxonomy |
|
||||
| `Code.Formatter` | 2,605 | Code formatting as library |
|
||||
|
||||
**The surprise:** The type system (`types/descr.ex` at 6,301 lines) is
|
||||
nearly as large as Kernel (7,102 lines). It's the newest and
|
||||
fastest-growing module — 504 commits, 96% written by José Valim. This
|
||||
is where the investment is going.
|
||||
|
||||
### By authorship (who shapes the language)
|
||||
|
||||
Type system: 396/504 commits from José, 32 from Eric Meadows-Jönsson,
|
||||
31 from Guillaume Duboc. This is auteur-driven development — one person
|
||||
holds the architectural vision for the most complex subsystem.
|
||||
|
||||
---
|
||||
|
||||
## 3. The Bootstrap Problem
|
||||
|
||||
**How does Elixir compile itself?**
|
||||
|
||||
The answer is 33 Erlang files in `lib/elixir/src/`:
|
||||
|
||||
```
|
||||
elixir_bootstrap.erl — minimal Kernel for self-compilation
|
||||
elixir_compiler.erl — the compiler entry point
|
||||
elixir_tokenizer.erl — lexer (in Erlang for speed)
|
||||
elixir_expand.erl — macro expansion
|
||||
elixir_erl.erl — Elixir AST → Erlang AST
|
||||
elixir_erl_pass.erl — code generation pass
|
||||
elixir_env.erl — compilation environment
|
||||
elixir_clauses.erl — pattern matching compilation
|
||||
```
|
||||
|
||||
**Convention:** The tokenizer and core compiler remain in Erlang
|
||||
permanently. This isn't technical debt — it's a deliberate choice.
|
||||
The tokenizer benefits from Erlang's binary pattern matching
|
||||
performance. The compiler needs to exist before Elixir does.
|
||||
|
||||
**Origin:** The bootstrap file dates to Nov 22, 2013 (commit
|
||||
`260be7c8e`: "Start porting elixir_macros to pure elixir"). Before
|
||||
this, MORE of the compiler was in Erlang. The trajectory is clear:
|
||||
minimize Erlang over time, but keep it where it provides genuine value.
|
||||
|
||||
---
|
||||
|
||||
## 4. TODO Culture: Version-Gated Deadlines
|
||||
|
||||
```elixir
|
||||
# TODO: Remove me on v2.0 — 16 occurrences
|
||||
# TODO: Deprecate me on Elixir v1.23 — 6 occurrences
|
||||
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
|
||||
# TODO: Make an error on Elixir v2.0 — 3 occurrences
|
||||
# TODO: Deprecate on Elixir v1.22 — 3 occurrences
|
||||
```
|
||||
|
||||
**Convention:** Every TODO has a version target. No "someday" TODOs
|
||||
exist. When a version ships, grep for that version's TODOs and resolve
|
||||
them all.
|
||||
|
||||
**127 total TODOs** across 567 files. Contrast with Go's 3,428 TODOs
|
||||
across 11K files — the Elixir team treats TODOs as time-bombs, not
|
||||
documentation.
|
||||
|
||||
---
|
||||
|
||||
## 5. Unique Patterns
|
||||
|
||||
### 5.1 Protocol Consolidation
|
||||
|
||||
Protocols dispatch dynamically at runtime by default (checking each
|
||||
struct's implementation). **Protocol consolidation** compiles all known
|
||||
implementations into a single dispatch module at build time.
|
||||
|
||||
From `lib/elixir/lib/protocol.ex`:
|
||||
> "Consolidation directly links the protocol to its implementations.
|
||||
> Invoking a consolidated protocol is equivalent to invoking two remote
|
||||
> functions."
|
||||
|
||||
**Convention:** Mix enables consolidation by default in production. The
|
||||
`@callback __protocol__(:consolidated?)` exists so code can check at
|
||||
runtime whether fast-path dispatch is active.
|
||||
|
||||
**When NOT to use:** Tests often disable consolidation (`consolidate_
|
||||
protocols: false`) so new protocol implementations added during tests
|
||||
are discoverable without recompilation.
|
||||
|
||||
### 5.2 Parallel Type Checker
|
||||
|
||||
`Module.ParallelChecker` (introduced July 2019, PR #9203 by Eric
|
||||
Meadows-Jönsson as "Add ExCk chunk") enables concurrent type checking
|
||||
across modules.
|
||||
|
||||
The type system itself (13,034 lines across 7 files in
|
||||
`lib/elixir/lib/module/types/`) is set-theoretic — types are sets, and
|
||||
operations are set operations (union, intersection, difference).
|
||||
|
||||
**Key files:**
|
||||
- `descr.ex` (6,301 lines) — type descriptions and set operations
|
||||
- `apply.ex` — function application typing
|
||||
- `expr.ex` — expression typing
|
||||
- `pattern.ex` — pattern match typing
|
||||
- `of.ex` — type inference
|
||||
- `helpers.ex` — shared utilities
|
||||
- `traverse.ex` — AST traversal
|
||||
|
||||
### 5.3 Code.Formatter as Library Function
|
||||
|
||||
The code formatter (2,605 lines) is a library function, not a CLI tool.
|
||||
You can call `Code.format_string!/2` from any Elixir code.
|
||||
|
||||
**Introduced:** Oct 7, 2017 (PR #6639 by José Valim). **Zero review
|
||||
comments. Merged in 1 hour.** José opened and merged his own formatter
|
||||
with no external review. This is the BDFL model — the language author
|
||||
ships foundational infrastructure by authority.
|
||||
|
||||
**Convention:** The formatter uses `Inspect.Algebra` (Wadler-Lindig
|
||||
pretty-printing) for layout decisions. It defines all operators and
|
||||
their associativity as module attributes:
|
||||
|
||||
```elixir
|
||||
@pipeline_operators [:|>, :~>>, :<<~, :~>, :<~, :<~>, :"<|>"]
|
||||
@right_new_line_before_binary_operators [:|, :when]
|
||||
@required_parens_logical_binary_operands [:|||, :||, :or, :&&&, :&&, :and]
|
||||
```
|
||||
|
||||
### 5.4 Mix Tasks as Single-File Modules
|
||||
|
||||
55 Mix tasks, each in its own file. Convention:
|
||||
- One task = one file
|
||||
- Module name determines task name: `Mix.Tasks.Deps.Clean` → `deps.clean`
|
||||
- `@shortdoc` for brief help, `@moduledoc` for full docs
|
||||
- `@recursive true` for umbrella traversal
|
||||
|
||||
### 5.5 ExUnit CaseTemplate (Extension Pattern)
|
||||
|
||||
The `ExUnit.CaseTemplate` is how Elixir's test framework supports
|
||||
extension — you define a module that `use`s `CaseTemplate`, and test
|
||||
modules `use YourModule` to inherit setup callbacks and helpers.
|
||||
|
||||
This is the same pattern Phoenix uses for `ConnCase` and `DataCase`.
|
||||
It originates from ExUnit itself — the framework demonstrates its own
|
||||
extension point.
|
||||
|
||||
### 5.6 Logger: Erlang Integration Done Right
|
||||
|
||||
PR #9333 (Sep 2019, merged Nov 2019): "Use Erlang's logger as main
|
||||
logging implementation." The Elixir Logger was rewritten to sit on top
|
||||
of Erlang's `:logger` module rather than reimplementing log dispatch.
|
||||
|
||||
**Convention:** When OTP provides infrastructure, wrap it rather than
|
||||
replace it. The compatibility layer translates Erlang log messages to
|
||||
Elixir format, but dispatch/filtering/handlers are OTP's.
|
||||
|
||||
---
|
||||
|
||||
## 6. PR Discussion Patterns
|
||||
|
||||
### JSON.Encoder (PR #14021, Dec 2024)
|
||||
|
||||
38 review comments, 13 days to merge. Key debate:
|
||||
|
||||
**sabiwara** asked: "What is the reason we went with a different API
|
||||
than Jason?" — questioning why the stdlib JSON module doesn't mirror
|
||||
the dominant community library.
|
||||
|
||||
**michalmuskala** (Jason author): "Once 1.18 is released with the new
|
||||
JSON module, I plan to make a new release of Jason with some small
|
||||
fixes and then effectively deprecate it."
|
||||
|
||||
**Lesson:** When stdlib absorbs community library functionality, the
|
||||
community library author participates in the review. Jason's author
|
||||
blessed the replacement and planned deprecation. This is how healthy
|
||||
ecosystem evolution works.
|
||||
|
||||
### Duration (PR #13385, Mar-Apr 2024)
|
||||
|
||||
75 comments + 116 review comments. The most debated PR in recent
|
||||
Elixir history.
|
||||
|
||||
**Pattern:** Community contributor (@tfiedlerdejanze) opened a PR
|
||||
adding `Date.shift/2`. José redirected to a broader `Duration` type.
|
||||
The contributor iterated through multiple designs.
|
||||
|
||||
**José's key intervention:** "I would rather prefer to pass a Duration
|
||||
to Calendar.ISO.shift_date, if we ever have such a type, rather than a
|
||||
keyword list." — refusing a simpler PR because it would lock in a
|
||||
suboptimal API before the full design was clear.
|
||||
|
||||
**Lesson:** The BDFL model means one person can say "this is the wrong
|
||||
abstraction" and redirect months of work. The PR took 33 days and
|
||||
several complete rewrites. The result was better because someone held
|
||||
the line on "solve the whole problem, not just the immediate pain."
|
||||
|
||||
### Formatter (PR #6639, Oct 2017)
|
||||
|
||||
Zero comments. Merged in 1 hour. 2,605 lines of new code.
|
||||
|
||||
**Lesson:** BDFL-driven projects can ship massive foundational changes
|
||||
with no review. José was both the author and the authority. This is the
|
||||
opposite of CockroachDB's Handle PR (2.5 months, extensive debate).
|
||||
Neither model is wrong — it depends on team structure and trust level.
|
||||
|
||||
---
|
||||
|
||||
## 7. Cross-Ecosystem Comparisons
|
||||
|
||||
| Aspect | Elixir | Go |
|
||||
|--------|--------|-----|
|
||||
| TODOs | 127, all version-gated | 3,428, all owner-attributed |
|
||||
| Formatter origin | BDFL ships in 1hr, no review | `gofmt` shipped with language |
|
||||
| Bootstrap | Erlang (33 files, permanent) | Assembly + Go (self-hosting since 1.5) |
|
||||
| Extension | 6 protocols + CaseTemplate | `internal/` packages (61 of them) |
|
||||
| Type system | Set-theoretic, 13K lines, growing | Static, mature, compile-time only |
|
||||
| Test ratio | 1:1.2 (file per file) | 1:3.3 (package-level tests) |
|
||||
| Governance | BDFL (José) | Committee (Russ Cox + team) |
|
||||
|
||||
---
|
||||
|
||||
## 8. What This Teaches
|
||||
|
||||
1. **BDFL projects can move faster on foundational infrastructure** —
|
||||
the formatter, type system, and JSON module all shipped because one
|
||||
person had authority. But Duration took 33 days because community
|
||||
contribution required iteration with the BDFL's vision.
|
||||
|
||||
2. **Version-gated TODOs are a superior cleanup strategy** for
|
||||
projects with regular release cycles. You never have to decide "is
|
||||
this worth fixing?" — the version bump forces the question.
|
||||
|
||||
3. **Keep the minimum viable bootstrap in the host language.** 33
|
||||
Erlang files is the floor, not a ceiling. The trajectory is always
|
||||
toward more Elixir, less Erlang — but the tokenizer stays in Erlang
|
||||
because binary matching is genuinely faster there.
|
||||
|
||||
4. **The type system's growth rate predicts the language's future.**
|
||||
504 commits, 96% from José, nearly as large as Kernel. Elixir's
|
||||
next 5 years will be defined by gradual typing.
|
||||
|
||||
5. **Community library authors should bless stdlib absorption.** The
|
||||
Jason → JSON.Encoder transition worked because michalmuskala
|
||||
participated in the review and planned deprecation.
|
||||
|
||||
6. **Each OTP app is an independent unit** — this convention flows
|
||||
directly into how Phoenix projects are organized. The language
|
||||
teaches its own architectural pattern by example.
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
@@ -0,0 +1,266 @@
|
||||
# Elixir Language Source: Convention Reference
|
||||
|
||||
Quick-reference for conventions extracted from the elixir-lang/elixir
|
||||
source code. Each entry: pattern name, location, example, when to use,
|
||||
when NOT to use, origin.
|
||||
|
||||
---
|
||||
|
||||
## Version-Gated TODOs
|
||||
|
||||
**Location:** Throughout `lib/`
|
||||
|
||||
```elixir
|
||||
# TODO: Remove me on v2.0
|
||||
# TODO: Deprecate me on Elixir v1.23
|
||||
# TODO: Make an error on Elixir v2.0
|
||||
```
|
||||
|
||||
**When to use:** Any backward-compatible code that should be removed at
|
||||
a known future version. Deprecation paths, compatibility shims, feature
|
||||
flags.
|
||||
|
||||
**When NOT to use:** Performance improvements ("make this faster
|
||||
someday"), refactoring desires, or anything without a clear version
|
||||
boundary. Those belong in issues, not TODOs.
|
||||
|
||||
**Origin:** Consistent throughout history. The pattern predates the
|
||||
repo's earliest commits — José's convention from the start.
|
||||
|
||||
---
|
||||
|
||||
## Independent OTP Applications
|
||||
|
||||
**Location:** `lib/` (6 top-level dirs)
|
||||
|
||||
```
|
||||
lib/elixir/mix.exs # The language core
|
||||
lib/ex_unit/mix.exs # Testing framework
|
||||
lib/mix/mix.exs # Build tool
|
||||
lib/logger/mix.exs # Logging
|
||||
lib/iex/mix.exs # Interactive shell
|
||||
lib/eex/mix.exs # Templates
|
||||
```
|
||||
|
||||
**When to use:** When components have distinct lifecycle, deployment, or
|
||||
dependency requirements. When you want components to be independently
|
||||
testable.
|
||||
|
||||
**When NOT to use:** Small projects where the overhead of multiple
|
||||
applications exceeds the organizational benefit. If components always
|
||||
deploy together and never independently, a single app is simpler.
|
||||
|
||||
**Origin:** Elixir 0.x — the language was always structured this way.
|
||||
|
||||
---
|
||||
|
||||
## Erlang for Performance-Critical Paths
|
||||
|
||||
**Location:** `lib/elixir/src/` (33 .erl files)
|
||||
|
||||
```erlang
|
||||
%% elixir_tokenizer.erl — binary pattern matching is faster in Erlang
|
||||
%% elixir_erl_pass.erl — code generation benefits from proximity to BEAM
|
||||
```
|
||||
|
||||
**When to use:** When Erlang's binary pattern matching or NIF interface
|
||||
provides measurable performance advantage. When code must exist before
|
||||
Elixir's compiler is available (bootstrap).
|
||||
|
||||
**When NOT to use:** For new features that could be written in Elixir.
|
||||
The trajectory is always toward less Erlang. Don't write new Erlang
|
||||
unless profiling proves it's necessary.
|
||||
|
||||
**Origin:** The entire language started as Erlang (2011). Bootstrap file
|
||||
(`elixir_bootstrap.erl`) formalized as distinct from "Elixir written
|
||||
in Erlang" in 2013 (commit `260be7c8e`).
|
||||
|
||||
---
|
||||
|
||||
## Protocol Consolidation
|
||||
|
||||
**Location:** `lib/elixir/lib/protocol.ex`
|
||||
|
||||
```elixir
|
||||
# In mix.exs (default for prod):
|
||||
consolidate_protocols: true
|
||||
|
||||
# Disable for tests:
|
||||
consolidate_protocols: Mix.env() != :test
|
||||
```
|
||||
|
||||
**When to use:** Always in production (it's the default). Consolidated
|
||||
protocols dispatch in two function calls instead of dynamic lookup.
|
||||
|
||||
**When NOT to use:** In tests where you define new protocol
|
||||
implementations at runtime. In dev if you're frequently recompiling
|
||||
protocol implementations.
|
||||
|
||||
**Origin:** PR adding consolidation to escript.build (2014, issue
|
||||
#2699). Later made default for Mix projects.
|
||||
|
||||
---
|
||||
|
||||
## Code.Formatter via Inspect.Algebra
|
||||
|
||||
**Location:** `lib/elixir/lib/code/formatter.ex` (2,605 lines)
|
||||
|
||||
```elixir
|
||||
# The formatter is a library function:
|
||||
Code.format_string!("def foo( x,y),do: x+y")
|
||||
# => "def foo(x, y), do: x + y"
|
||||
|
||||
# Internally uses Wadler-Lindig algebra:
|
||||
import Inspect.Algebra, except: [format: 2, surround: 3, surround: 4]
|
||||
```
|
||||
|
||||
**When to use:** When building code generation tools, macros that
|
||||
produce source, or custom formatting rules. The algebra is available
|
||||
to any Elixir code.
|
||||
|
||||
**When NOT to use:** Don't reimplement formatting logic. Use
|
||||
`Code.format_string!/2` or the `mix format` task. The algebra is for
|
||||
building formatters, not for end-user formatting.
|
||||
|
||||
**Origin:** Oct 7, 2017 (PR #6639, José Valim). Merged in 1 hour, zero
|
||||
review comments.
|
||||
|
||||
---
|
||||
|
||||
## Mix Task Convention
|
||||
|
||||
**Location:** `lib/mix/lib/mix/tasks/` (55 files)
|
||||
|
||||
```elixir
|
||||
defmodule Mix.Tasks.Deps.Clean do
|
||||
@moduledoc "Removes the given dependencies' build artifacts."
|
||||
@shortdoc "Deletes generated files and artifacts for dependencies"
|
||||
|
||||
use Mix.Task
|
||||
@recursive true
|
||||
|
||||
@impl true
|
||||
def run(args) do
|
||||
# ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**When to use:** Any command-line operation that should be available via
|
||||
`mix <task>`. One file per task, module name determines task name.
|
||||
|
||||
**When NOT to use:** Tasks that require interactive user input (use
|
||||
escript or IEx helpers instead). Tasks that need to run without Mix
|
||||
loaded.
|
||||
|
||||
**Origin:** Part of Mix since its creation. The one-file convention
|
||||
is strict — all 55 stdlib tasks follow it.
|
||||
|
||||
---
|
||||
|
||||
## ExUnit.CaseTemplate
|
||||
|
||||
**Location:** `lib/ex_unit/lib/ex_unit/case_template.ex` (162 lines)
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.DataCase do
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
setup tags do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
|
||||
unless tags[:async], do: Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Usage:
|
||||
defmodule MyApp.SomeTest do
|
||||
use MyApp.DataCase, async: true
|
||||
end
|
||||
```
|
||||
|
||||
**When to use:** When multiple test modules share setup logic,
|
||||
assertions, or helper functions. The inheritance model allows
|
||||
composition of test concerns.
|
||||
|
||||
**When NOT to use:** For test helpers that don't need lifecycle
|
||||
callbacks. Simple `import` or `alias` is cleaner for utility functions
|
||||
that don't need `setup`/`setup_all`.
|
||||
|
||||
**Origin:** Commit `1f491dfbe` — "Add support to case templates." The
|
||||
pattern Phoenix adopted for `ConnCase`/`DataCase` comes directly from
|
||||
ExUnit.
|
||||
|
||||
---
|
||||
|
||||
## Logger Wrapping OTP
|
||||
|
||||
**Location:** `lib/logger/`
|
||||
|
||||
```elixir
|
||||
# Elixir's Logger dispatches to Erlang's :logger
|
||||
# Translation layer converts Erlang messages → Elixir format
|
||||
# Filters and handlers use OTP's infrastructure
|
||||
```
|
||||
|
||||
**When to use:** When OTP provides the infrastructure you need.
|
||||
Wrap it — don't replace it. Provide Elixir-idiomatic API on top.
|
||||
|
||||
**When NOT to use:** When OTP's solution has fundamental architectural
|
||||
limitations you can't work around with a wrapper (rare).
|
||||
|
||||
**Origin:** PR #9333 (Sep-Nov 2019). Rewrote Logger from custom
|
||||
dispatch to wrapping Erlang's `:logger`. ~2 month implementation.
|
||||
|
||||
---
|
||||
|
||||
## BDFL Merge Pattern
|
||||
|
||||
**Location:** Throughout git history
|
||||
|
||||
```
|
||||
PR #6639 (Formatter): 0 comments, merged in 1 hour, 2,605 new lines
|
||||
PR #14021 (JSON): 38 review comments, 13 days, community input
|
||||
PR #13385 (Duration): 75+116 comments, 33 days, community redirected
|
||||
```
|
||||
|
||||
**When to use:** Foundational infrastructure where one person holds the
|
||||
architectural vision. The BDFL can ship without review when the change
|
||||
is self-evidently correct (formatter) or redirect community work when
|
||||
the abstraction isn't right (Duration).
|
||||
|
||||
**When NOT to use:** In team-maintained projects without a clear
|
||||
authority. In projects where consensus is required for API decisions.
|
||||
The BDFL model fails when the BDFL is wrong and no one can override.
|
||||
|
||||
**Origin:** José Valim has been sole authority since Elixir's creation
|
||||
(2011). The core team exists but José has final say on language design.
|
||||
|
||||
---
|
||||
|
||||
## Type System Architecture (Set-Theoretic)
|
||||
|
||||
**Location:** `lib/elixir/lib/module/types/` (13,034 lines, 7 files)
|
||||
|
||||
```elixir
|
||||
# Types are sets. Operations are set operations.
|
||||
# descr.ex (6,301 lines) defines the algebra:
|
||||
# - Union of types
|
||||
# - Intersection of types
|
||||
# - Difference (negation types)
|
||||
# - Subtype checking via set inclusion
|
||||
```
|
||||
|
||||
**When to use:** Understanding how Elixir's gradual type system works
|
||||
internally. The set-theoretic approach means types compose naturally —
|
||||
`integer() | String.t()` is literally a set union.
|
||||
|
||||
**When NOT to use:** This is internal compiler infrastructure. Don't
|
||||
depend on `Module.Types` internals — they change frequently (504
|
||||
commits and counting).
|
||||
|
||||
**Origin:** Aug 2019 (PR #9270 by Eric Meadows-Jönsson). Originally
|
||||
just function clause exhaustiveness checking, now growing into full
|
||||
gradual typing. 96% of subsequent work by José.
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
# Patterns Extracted from oban-bg/oban
|
||||
|
||||
## Pattern: Plugin as Behaviour + GenServer
|
||||
|
||||
**Source:** `lib/oban/plugin.ex`
|
||||
**Category:** plugin
|
||||
|
||||
**What:** Define a plugin interface as a behaviour with
|
||||
`start_link/1` and `validate/1` callbacks. Plugins must be
|
||||
OTP-compliant (GenServer/Agent). The host supervises them.
|
||||
|
||||
**Why:** Extensibility without coupling. Oban can start any
|
||||
module that satisfies the behaviour — pruning, cron,
|
||||
lifeline — without knowing implementation details. The
|
||||
`validate/1` callback ensures misconfigured plugins fail at
|
||||
startup, not at runtime.
|
||||
|
||||
**Example:**
|
||||
|
||||
```elixir
|
||||
@callback start_link([option()]) :: GenServer.on_start()
|
||||
@callback validate([option()]) :: :ok | {:error, String.t()}
|
||||
@optional_callbacks [format_logger_output: 2]
|
||||
```
|
||||
|
||||
**When to use:** When your application needs a plugin
|
||||
system where third parties add behavior. The behaviour
|
||||
ensures type safety; supervision ensures fault isolation.
|
||||
|
||||
**When NOT to use:** Internal modules that you control.
|
||||
Behaviours add ceremony — if there is only one
|
||||
implementation, use a module directly.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Structured Telemetry Spans
|
||||
|
||||
**Source:** `lib/oban/telemetry.ex`
|
||||
**Category:** telemetry
|
||||
|
||||
**What:** Emit telemetry events as spans with
|
||||
start/stop/exception structure. Every operation (job
|
||||
execution, engine calls, plugin work) follows the same
|
||||
three-event pattern with consistent metadata shapes.
|
||||
|
||||
**Why:** Uniform observability. Any monitoring tool
|
||||
(AppSignal, Datadog, custom logger) can hook into the same
|
||||
event structure. The span pattern (start → stop|exception)
|
||||
enables latency tracking, error rates, and resource usage
|
||||
measurement without custom instrumentation per feature.
|
||||
|
||||
**Example:**
|
||||
|
||||
```elixir
|
||||
# Event names follow: [:oban, :component, :action, :phase]
|
||||
[:oban, :job, :start]
|
||||
[:oban, :job, :stop] # measurements: duration, memory
|
||||
[:oban, :job, :exception] # + kind, reason, stacktrace
|
||||
|
||||
[:oban, :engine, :fetch_jobs, :start]
|
||||
[:oban, :engine, :fetch_jobs, :stop]
|
||||
[:oban, :engine, :fetch_jobs, :exception]
|
||||
```
|
||||
|
||||
**When to use:** Any library or application that wants
|
||||
observability without coupling to a specific monitoring
|
||||
backend. The pattern works for database queries, HTTP
|
||||
requests, background jobs, cache operations.
|
||||
|
||||
**When NOT to use:** Ultra-hot paths where telemetry
|
||||
overhead matters (millions of events/second). Use sampling
|
||||
or skip entirely.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Engine Abstraction for Backend Swap
|
||||
|
||||
**Source:** `lib/oban/engine.ex`
|
||||
**Category:** engine
|
||||
|
||||
**What:** Define a behaviour (`Engine`) with callbacks for
|
||||
all database operations (insert, fetch, complete, etc.).
|
||||
Ship multiple implementations (Basic/Inline/Lite) that swap
|
||||
at config time.
|
||||
|
||||
**Why:** Different environments need different backends:
|
||||
Postgres for production, SQLite for development, inline
|
||||
(in-memory) for testing. The engine abstraction lets you
|
||||
swap without changing application code.
|
||||
|
||||
**Example:**
|
||||
|
||||
```elixir
|
||||
@callback init(conf, opts) :: {:ok, meta} | {:error, term}
|
||||
@callback insert_job(conf, changeset, opts) :: {:ok, Job.t()}
|
||||
@callback fetch_jobs(conf, meta, opts) :: {:ok, {meta, [Job.t()]}}
|
||||
@callback complete_job(conf, Job.t()) :: :ok
|
||||
```
|
||||
|
||||
**When to use:** When your system needs to support multiple
|
||||
storage backends, or when testing requires a fundamentally
|
||||
different execution model (synchronous vs async).
|
||||
|
||||
**When NOT to use:** Single-backend applications. The
|
||||
abstraction layer adds complexity that is only justified
|
||||
when you actually swap implementations.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Keyword Validation with Reduce-While
|
||||
|
||||
**Source:** `lib/oban/validation.ex`
|
||||
**Category:** config
|
||||
|
||||
**What:** Validate keyword options by iterating with
|
||||
`Enum.reduce_while/3` and a validator function. Stop at
|
||||
first error. Return `:ok` or `{:error, reason}`.
|
||||
|
||||
**Why:** Keyword lists are the standard Elixir config
|
||||
format. Validating them procedurally (nested if/case) gets
|
||||
messy. The reduce-while + validator pattern is composable:
|
||||
each option validates independently, errors short-circuit,
|
||||
and the validator function can be swapped or extended.
|
||||
|
||||
**Example:**
|
||||
|
||||
```elixir
|
||||
def validate(opts, validator) when is_list(opts) do
|
||||
Enum.reduce_while(opts, :ok, fn opt, acc ->
|
||||
case validator.(opt) do
|
||||
:ok -> {:cont, acc}
|
||||
{:error, _} = error -> {:halt, error}
|
||||
end
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
**When to use:** Any public API that accepts keyword
|
||||
options from users. Libraries, GenServer init, plugin
|
||||
configs.
|
||||
|
||||
**When NOT to use:** Internal functions where the caller
|
||||
is trusted. Also avoid for deeply nested configs — use
|
||||
schema-based validation (NimbleOptions, Ecto embedded
|
||||
schemas) instead.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Testing Mode Toggle
|
||||
|
||||
**Source:** `lib/oban/testing.ex`, `lib/oban/config.ex`
|
||||
**Category:** testing
|
||||
|
||||
**What:** Support a `testing:` config option that switches
|
||||
execution mode: `:disabled` (production), `:inline`
|
||||
(execute immediately in caller process), `:manual` (enqueue
|
||||
but don't execute — assert on DB state).
|
||||
|
||||
**Why:** Background job systems are inherently async, which
|
||||
makes testing hard. The mode toggle gives you: (1) inline
|
||||
for unit tests that need synchronous execution, (2) manual
|
||||
for integration tests that verify enqueueing without
|
||||
side effects.
|
||||
|
||||
**Example:**
|
||||
|
||||
```elixir
|
||||
# In test config:
|
||||
config :my_app, Oban, testing: :manual
|
||||
|
||||
# In tests:
|
||||
use Oban.Testing, repo: MyApp.Repo
|
||||
|
||||
perform_job(MyWorker, %{id: 1})
|
||||
assert_enqueued worker: MyWorker, args: %{id: 1}
|
||||
```
|
||||
|
||||
**When to use:** Any async system that needs deterministic
|
||||
testing — job queues, event buses, notification systems.
|
||||
The testing mode replaces "sleep and hope" with explicit
|
||||
control.
|
||||
|
||||
**When NOT to use:** Synchronous systems that are already
|
||||
deterministic. Also avoid if the mode toggle leaks into
|
||||
production code paths (keep it config-only, not conditional
|
||||
logic scattered through business code).
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Stopper for Goroutine Lifecycle (CockroachDB)
|
||||
|
||||
**Source:** `pkg/util/stop/stopper.go` (cockroachdb)
|
||||
**Category:** concurrency
|
||||
|
||||
**What:** A dedicated struct that manages the lifecycle of
|
||||
all goroutines in a component: tracks active tasks, refuses
|
||||
new work during shutdown (quiesce), waits for completion,
|
||||
then runs closers.
|
||||
|
||||
**Why:** In distributed systems, clean shutdown is critical.
|
||||
You need to: (1) stop accepting new work, (2) finish
|
||||
in-flight work, (3) release resources in order. The Stopper
|
||||
centralizes this instead of scattering shutdown logic across
|
||||
every goroutine.
|
||||
|
||||
**Example:**
|
||||
|
||||
```go
|
||||
type Stopper struct {
|
||||
quiescer chan struct{} // closed when quiescing
|
||||
stopped chan struct{} // closed when fully stopped
|
||||
mu struct {
|
||||
syncutil.RWMutex
|
||||
_numTasks int32
|
||||
quiescing, stopping bool
|
||||
closers []Closer
|
||||
}
|
||||
}
|
||||
|
||||
// RunAsyncTask refuses new work during quiesce
|
||||
func (s *Stopper) RunAsyncTask(ctx context.Context,
|
||||
taskName string, f func(context.Context)) error {
|
||||
if !s.addTask() {
|
||||
return ErrUnavailable
|
||||
}
|
||||
go func() {
|
||||
defer s.decTask()
|
||||
f(ctx)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:** Any server or subsystem that spawns
|
||||
goroutines and needs graceful shutdown. Especially in
|
||||
long-running services where leaked goroutines cause
|
||||
resource exhaustion.
|
||||
|
||||
**When NOT to use:** Simple programs with a single main
|
||||
goroutine. Or when `errgroup` with context cancellation
|
||||
suffices for the shutdown coordination.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Atomic File Operations with Suffix Convention
|
||||
|
||||
**Source:** `tsdb/db.go` (prometheus)
|
||||
**Category:** storage
|
||||
|
||||
**What:** Use directory suffixes (`.tmp-for-creation`,
|
||||
`.tmp-for-deletion`) to make multi-step file operations
|
||||
crash-safe. On startup, clean up any dirs with these
|
||||
suffixes (they represent incomplete operations).
|
||||
|
||||
**Why:** Database storage needs atomicity. If the process
|
||||
crashes between creating a block and finalizing it, you
|
||||
need to know the block is incomplete. The suffix convention
|
||||
makes incomplete state visible at the filesystem level
|
||||
without requiring a separate journal.
|
||||
|
||||
**Example:**
|
||||
|
||||
```go
|
||||
const (
|
||||
tmpForDeletionBlockDirSuffix = ".tmp-for-deletion"
|
||||
tmpForCreationBlockDirSuffix = ".tmp-for-creation"
|
||||
)
|
||||
|
||||
// On startup: remove any .tmp-* dirs (incomplete ops)
|
||||
// On create: write to dir.tmp-for-creation, then rename
|
||||
// On delete: rename to dir.tmp-for-deletion, then remove
|
||||
```
|
||||
|
||||
**When to use:** Any system that manages files/directories
|
||||
and needs crash consistency without a full WAL. Simpler
|
||||
than a write-ahead log for coarse-grained operations.
|
||||
|
||||
**When NOT to use:** When you already have a WAL or
|
||||
transaction log. Or for fine-grained operations where
|
||||
rename semantics are insufficient.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Options as DefaultOptions() + Override
|
||||
|
||||
**Source:** `tsdb/db.go` (prometheus)
|
||||
**Category:** configuration
|
||||
|
||||
**What:** Provide a `DefaultOptions()` function returning a
|
||||
fully-populated config struct. Users copy and override only
|
||||
what they need. No nil-means-default ambiguity.
|
||||
|
||||
**Why:** Large config structs (20+ fields) are unwieldy.
|
||||
By providing sane defaults as a function (not a package-
|
||||
level var), you avoid mutation bugs and make it clear what
|
||||
"normal" looks like. Users only specify deviations.
|
||||
|
||||
**Example:**
|
||||
|
||||
```go
|
||||
func DefaultOptions() *Options {
|
||||
return &Options{
|
||||
WALSegmentSize: wlog.DefaultSegmentSize,
|
||||
RetentionDuration: int64(15 * 24 * time.Hour / ...),
|
||||
MinBlockDuration: DefaultBlockDuration,
|
||||
MaxBlockDuration: DefaultBlockDuration,
|
||||
SamplesPerChunk: DefaultSamplesPerChunk,
|
||||
// ... 20 more fields with sane defaults
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
opts := tsdb.DefaultOptions()
|
||||
opts.RetentionDuration = 30 * 24 * time.Hour
|
||||
db, err := tsdb.Open(dir, nil, nil, opts, nil)
|
||||
```
|
||||
|
||||
**When to use:** Config structs with many fields where most
|
||||
users want defaults. Especially when zero-value semantics
|
||||
would be confusing (e.g., 0 retention = infinite? or off?).
|
||||
|
||||
**When NOT to use:** Small configs (3-4 fields) where
|
||||
struct literal with zero-means-default is clear enough.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Scrape Loop with Aligned Timestamps
|
||||
|
||||
**Source:** `scrape/scrape.go` (prometheus)
|
||||
**Category:** concurrency
|
||||
|
||||
**What:** Periodic scrape loops that align timestamps to
|
||||
intervals with a small tolerance, enabling better storage
|
||||
compression downstream.
|
||||
|
||||
**Why:** Time-series databases compress better when
|
||||
timestamps are regular. A 2ms tolerance on alignment
|
||||
means scraped data aligns to the expected grid while
|
||||
accommodating real-world jitter.
|
||||
|
||||
**Example:**
|
||||
|
||||
```go
|
||||
var ScrapeTimestampTolerance = 2 * time.Millisecond
|
||||
var AlignScrapeTimestamps = true
|
||||
|
||||
// In scrape loop: if scrape finishes within tolerance
|
||||
// of the expected timestamp, snap to the grid
|
||||
```
|
||||
|
||||
**When to use:** Any periodic data collection where
|
||||
downstream storage benefits from timestamp regularity.
|
||||
Metrics, heartbeats, polling loops.
|
||||
|
||||
**When NOT to use:** Event-driven data where timestamps
|
||||
must reflect actual occurrence time. Audit logs, user
|
||||
actions, financial transactions.
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
Reference in New Issue
Block a user