Practical, runnable examples of OTP and functional design patterns in Elixir. Each pattern is a self-contained module with tests, docs, and a real-world use case.
This repository demonstrates deep Elixir/OTP expertise through comprehensive, runnable examples of common patterns. Each pattern includes:
- Clean, documented code with full typespecs
- Comprehensive tests covering happy path, edge cases, and error scenarios
- Detailed guides explaining when and how to use each pattern
- Real-world examples from production experience
- IEx demonstrations you can try immediately
git clone https://github.com/ivan-podgurskiy/elixir-design-patterns.git
cd elixir-design-patterns
mix deps.get
mix testTry a pattern in IEx:
iex -S mix# Start a cache with TTL
{:ok, cache} = Patterns.GenServerCache.start_link()
:ok = Patterns.GenServerCache.put(cache, "user:123", %{name: "John"}, 5000)
{:ok, user} = Patterns.GenServerCache.get(cache, "user:123")| Pattern | Module | Description | Guide |
|---|---|---|---|
| GenServer Cache | Patterns.GenServerCache |
In-memory key-value cache with TTL expiration and statistics | 📖 Guide |
| Supervisor Tree | Patterns.SupervisorTree |
Fault-tolerant supervision with different restart strategies | 📖 Guide |
| Agent State | Patterns.AgentState |
Simple shared state for counters, config, and statistics | 📖 Guide |
| Task.async | Patterns.TaskAsync |
Parallel execution, timeout handling, and result aggregation | 📖 Guide |
| Pattern | Module | Description | Guide |
|---|---|---|---|
| Registry & Dynamic Supervisors | Patterns.RegistryDynamicSupervisor |
Runtime keyed process lookup, on-demand startup, and crash recovery | 📖 Guide |
| Pub/Sub with Registry | Patterns.RegistryPubSub |
Topic-based event fan-out with duplicate-key Registry dispatch | 📖 Guide |
| Process Pooling | Patterns.ProcessPool |
Fixed-size interchangeable worker pools with checkout/checkin | 📖 Guide |
| Circuit Breaker | Patterns.CircuitBreaker |
Fail-fast protection for unreliable services with automatic recovery | 📖 Guide |
| Pattern | Module | Description | Guide |
|---|---|---|---|
Pipeline with with |
Patterns.Pipeline |
Step pipelines using with chains for linear happy paths and first-error short-circuiting |
📖 Guide |
| Railway-Oriented Programming | Patterns.Railway |
Pipe-friendly result combinators (bind, map, tee, recover) for two-track success/failure pipelines |
📖 Guide |
| Behaviour & Protocol | Patterns.BehaviourProtocol |
Module-dispatched behaviours vs. value-dispatched protocols, wired into one notification system | 📖 Guide |
| ETS-Backed Store | Patterns.EtsStore |
GenServer-owned :public ETS tables with direct reads and read-path benchmarks |
📖 Guide |
| Pattern | Module | Description | Guide |
|---|---|---|---|
| Rate Limiter | Patterns.RateLimiter |
Token bucket rate limiting with global and per-key buckets, lazy refill, and retry hints | 📖 Guide |
Coming Soon
- Retry with exponential backoff (standalone module)
- Graceful shutdown handling
- Event Sourcing fundamentals
- GenServer Cache: Production-ready caching with expiration
- Agent State: Lightweight shared state for simple scenarios
- ETS-Backed Store: Shared in-memory tables with optional read bypass
- Supervisor Tree: Fault tolerance with configurable restart strategies
- Registry & Dynamic Supervisors: On-demand keyed process management
- Pub/Sub with Registry: Topic-based in-process event distribution
- Process Pooling: Bounded concurrency with worker checkout/checkin
- Task.async: Concurrent execution with proper error handling
- Task.async: Parallel I/O and CPU-bound operations
- Agent State: Thread-safe atomic operations
- Supervisor Tree: "Let it crash" philosophy in practice
- GenServer Cache: Graceful degradation and recovery
- Circuit Breaker: Fail-fast protection against failing dependencies
- Pipeline with
with: Linear happy paths with first-error short-circuiting - Railway-Oriented Programming: Pipe-friendly combinators for two-track result flows
- Behaviour & Protocol: Compile-time module contracts and runtime type-based dispatch
This project maintains high code quality standards:
# Run all quality checks
mix test # Full test suite
mix credo --strict # Code analysis
mix dialyzer # Static type checking
mix format --check-formatted # Code formatting
mix docs # Generate documentation- Comprehensive Tests: 80+ tests covering happy paths, edge cases, and error scenarios
- High Type Coverage: Public APIs include
@specannotations - Zero Credo Issues: Strict code quality enforcement
- Dialyzer Clean: Static analysis passing
- Detailed Docs: Every pattern module has documentation and a companion guide
{:ok, cache} = Patterns.GenServerCache.start_link()
# Store with TTL
:ok = Patterns.GenServerCache.put(cache, "session:abc", "active", 300_000)
# Retrieve
{:ok, "active"} = Patterns.GenServerCache.get(cache, "session:abc")
# Statistics
%{hits: 1, misses: 0, total_keys: 1} = Patterns.GenServerCache.stats(cache)# Test different supervision strategies
{:ok, sup} = Patterns.SupervisorTree.start_link(:one_for_one)
# Inspect running processes
info = Patterns.SupervisorTree.info(sup)
# %{strategy: :one_for_one, running_count: 3, children: [...]}
# Test fault tolerance
:ok = Patterns.SupervisorTree.crash_child(sup, :worker_1)
# Worker automatically restarts# Fetch multiple URLs concurrently (simulated I/O for demo/testing)
urls = ["http://httpbin.org/delay/1", "http://httpbin.org/ip"]
{:ok, responses} = Patterns.TaskAsync.parallel_fetch(urls, timeout: 5000)
# Process data in parallel
results = Patterns.TaskAsync.parallel_map(1..1000, fn x ->
expensive_computation(x)
end, max_concurrency: 8){:ok, stats} = Patterns.AgentState.Statistics.start_link()
# Time operations
result = Patterns.AgentState.Statistics.time(stats, :api_call, fn ->
Process.sleep(10)
:ok
end)
# Get metrics
avg_time = Patterns.AgentState.Statistics.average_time(stats, :api_call)These patterns are used in production for:
- API Gateways: Parallel service aggregation with fault tolerance
- Caching Layers: High-performance in-memory caches with TTL
- Background Processing: Job queues with supervised workers
- Microservices: Service coordination and health monitoring
- Data Pipelines: ETL processing with error recovery
- Start with Agent State — Simplest pattern for shared state
- Move to GenServer Cache — More complex state with business logic
- Explore Supervision — Understand fault tolerance fundamentals
- Master Task.async — Concurrent programming patterns
Each pattern builds on concepts from the previous ones.
- Let It Crash: Use supervision for fault tolerance
- Immutable Data: Functional programming principles
- Actor Model: Isolated processes communicating via messages
- OTP Design: Battle-tested concurrency patterns
- Single Responsibility: Each pattern solves one problem well
- Composability: Patterns can be combined for complex systems
- Testability: Every pattern is thoroughly tested
- Documentation: Self-documenting code with comprehensive guides
- Elixir 1.19+ with OTP 26+
- asdf (recommended)
Only the Elixir plugin is required — the OTP version is encoded in the release tag:
asdf plugin add elixir
asdf install # reads .tool-versions → 1.19.5-otp-28
mix deps.get
mix testPinned version in .tool-versions:
elixir 1.19.5-otp-28
The -otp-28 suffix is the OTP version that asdf uses for that Elixir build. You do not need a separate erlang plugin.
CI installs the same stack explicitly (OTP 28.1 + Elixir 1.19.5) and additionally tests OTP 26.2.5 as the minimum supported version.
# All tests
mix test
# Specific pattern
mix test test/patterns/genserver_cache_test.exs
# With coverage
mix test --cover# Format code
mix format
# Static analysis
mix credo --strict
# Type checking
mix dialyzer
# Generate docs
mix docs && open doc/index.htmlThis is primarily a demonstration repository, but improvements are welcome:
- Bug Reports: Open an issue with reproduction steps
- Documentation: Clarifications and corrections
- Performance: Benchmarks and optimizations
- Examples: Additional real-world usage scenarios
See CONTRIBUTING.md for commit message conventions (Conventional Commits) and PR workflow.
This repository uses Renovate for dependency version updates. Dependabot is used only for vulnerability detection — not for opening version-update PRs.
Enable Dependabot alerts manually in GitHub:
- Open Settings → Code security and analysis
- Enable Dependabot alerts
- If Dependabot security updates or Dependabot version updates are enabled, disable version updates (Renovate handles those)
Renovate opens weekly update PRs (security updates are prioritized immediately). All dependency PRs require manual review and approval before merge.
- ✅ Phase 1 Complete: Core OTP patterns with full documentation
- ✅ Phase 2 Complete: Registry & Dynamic Supervisors, Pub/Sub, Process Pool, and Circuit Breaker
- ✅ Phase 3 Complete: Functional programming patterns (Pipeline, Railway, Behaviour & Protocol, ETS-backed stores)
- 🚧 Phase 4 In Progress: Rate Limiter complete; retries, graceful shutdown, and event sourcing planned
- Elixir School — Comprehensive Elixir tutorial
- Programming Elixir — Dave Thomas book
- Designing Elixir Systems — OTP patterns
- GenServer Cache Guide
- Supervisor Tree Guide
- Agent State Guide
- Task.async Guide
- Registry & Dynamic Supervisors Guide
- Pub/Sub with Registry Guide
- Process Pooling Guide
- Circuit Breaker Guide
- Pipeline with
withChains Guide - Railway-Oriented Programming Guide
- Behaviour & Protocol Systems Guide
- ETS-Backed Stores Guide
- Rate Limiter Guide
Built with ❤️ for the Elixir community
Demonstrating production-ready patterns that power reliable, concurrent systems.