# Foundation
<p align="center">
<img src="assets/foundation.svg" alt="Foundation logo" width="200" height="200">
</p>
<p align="center">
<strong>Lightweight resilience primitives for Elixir</strong>
</p>
<p align="center">
<a href="https://hex.pm/packages/foundation"><img src="https://img.shields.io/hexpm/v/foundation.svg" alt="Hex version"></a>
<a href="https://hexdocs.pm/foundation"><img src="https://img.shields.io/badge/hex-docs-blue.svg" alt="Hex Docs"></a>
<a href="https://github.com/nshkrdotcom/foundation/blob/main/LICENSE"><img src="https://img.shields.io/hexpm/l/foundation.svg" alt="License"></a>
</p>
---
Foundation provides composable building blocks for resilient Elixir applications:
backoff policies, retry loops, rate-limit windows, circuit breakers, semaphores,
and telemetry helpers.
## Features
- **Backoff** - Exponential, linear, and constant strategies with jitter
- **Retry** - Configurable retry loops with timeout and progress tracking
- **Polling** - Long-running workflow polling with backoff and cancellation
- **Rate Limiting** - Shared backoff windows for API rate limits
- **Circuit Breaker** - Protect downstream services with automatic recovery
- **Semaphores** - Counting and weighted semaphores for concurrency control
- **Dispatch** - Layered limiter combining concurrency, throttling, and byte budgets
- **Telemetry** - Lightweight helpers with optional reporter integration
## Requirements
- Elixir 1.15+
- OTP 26+
## Installation
Add `foundation` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:foundation, "~> 0.2"}
]
end
```
## Usage
### Backoff and Retry
```elixir
alias Foundation.Backoff
alias Foundation.Retry
# Create a backoff policy
backoff = Backoff.Policy.new(
strategy: :exponential,
base_ms: 100,
max_ms: 5_000,
jitter_strategy: :factor,
jitter: 0.1
)
# Create a retry policy
policy = Retry.Policy.new(
max_attempts: 5,
backoff: backoff,
retry_on: fn
{:error, :timeout} -> true
{:error, :rate_limited} -> true
_ -> false
end
)
# Run with retry
{result, _state} = Retry.run(fn -> fetch_data() end, policy)
```
### Retry Runner with Telemetry
```elixir
alias Foundation.Retry.{Config, Handler, Runner}
config = Config.new(max_retries: 3, base_delay_ms: 100)
handler = Handler.from_config(config)
{:ok, result} = Runner.run(
fn -> call_api() end,
handler: handler,
telemetry_events: %{
start: [:my_app, :api, :start],
stop: [:my_app, :api, :stop],
retry: [:my_app, :api, :retry]
}
)
```
### HTTP Retry Helpers
```elixir
alias Foundation.Retry.HTTP
HTTP.retryable_status?(429) # true
HTTP.retryable_status?(500) # true
HTTP.retryable_status?(400) # false
# Parse Retry-After header
HTTP.parse_retry_after("120") # {:ok, 120}
HTTP.parse_retry_after("Wed, 08 Jan 2026 12:00:00 GMT") # {:ok, seconds_until}
```
### Polling
```elixir
alias Foundation.Poller
alias Foundation.Backoff
{:ok, result} = Poller.run(
fn attempt ->
case check_job_status(job_id) do
{:ok, :completed, data} -> {:ok, data}
{:ok, :pending} -> {:retry, :pending}
{:ok, :failed, reason} -> {:error, reason}
end
end,
backoff: Backoff.Policy.new(strategy: :exponential, base_ms: 500, max_ms: 10_000),
timeout_ms: 60_000,
max_attempts: 20
)
```
### Rate Limit Backoff Windows
```elixir
alias Foundation.RateLimit.BackoffWindow
# Get or create a limiter for a key
limiter = BackoffWindow.for_key(:openai_api)
# Set backoff after receiving 429
BackoffWindow.set(limiter, 30_000)
# Wait for backoff to clear before next request
BackoffWindow.wait(limiter)
```
### Circuit Breaker
```elixir
alias Foundation.CircuitBreaker
# Functional API (stateless)
cb = CircuitBreaker.new("payment_service", failure_threshold: 3, reset_timeout_ms: 30_000)
{result, cb} = CircuitBreaker.call(cb, fn ->
PaymentService.charge(amount)
end)
# Registry API (stateful, for shared circuit breakers)
alias Foundation.CircuitBreaker.Registry
{:ok, _pid} = Registry.start_link(name: MyApp.CircuitBreakers)
result = Registry.call(MyApp.CircuitBreakers, "payment_service", fn ->
PaymentService.charge(amount)
end)
```
### Semaphores
```elixir
# Counting semaphore (limit concurrent operations)
alias Foundation.Semaphore.Counting
registry = Counting.default_registry()
{:ok, result} = Counting.with_acquire(registry, :db_connections, 10, fn ->
execute_query()
end)
# Weighted semaphore (byte budgets)
alias Foundation.Semaphore.Weighted
{:ok, sem} = Weighted.start_link(max_weight: 10_000_000)
Weighted.with_acquire(sem, byte_size(payload), fn ->
upload_data(payload)
end)
# Simple limiter
alias Foundation.Semaphore.Limiter
Limiter.with_semaphore(5, fn ->
process_item()
end)
```
### Layered Dispatch
```elixir
alias Foundation.Dispatch
alias Foundation.RateLimit.BackoffWindow
# Create a rate limiter
limiter = BackoffWindow.for_key(:api_dispatch)
# Start dispatch with concurrency, throttling, and byte limits
{:ok, dispatch} = Dispatch.start_link(
limiter: limiter,
key: :api,
concurrency: 100,
throttled_concurrency: 5,
byte_budget: 5_000_000
)
# Execute with rate limiting
result = Dispatch.with_rate_limit(dispatch, byte_size(request), fn ->
send_request(request)
end)
# Signal backoff (e.g., after 429 response)
Dispatch.set_backoff(dispatch, 30_000)
```
### Telemetry
```elixir
alias Foundation.Telemetry
# Emit events
Telemetry.execute([:my_app, :request, :complete], %{count: 1}, %{status: 200})
# Measure function duration
{:ok, result} = Telemetry.measure([:my_app, :db, :query], %{table: :users}, fn ->
Repo.all(User)
end)
# Optional: Start a reporter (requires telemetry_reporter dependency)
{:ok, _pid} = Telemetry.start_reporter(name: :my_reporter, transport: MyTransport)
{:ok, handler_id} = Telemetry.attach_reporter(
reporter: :my_reporter,
events: [[:my_app, :request, :complete]]
)
```
## Documentation
Full documentation is available at [HexDocs](https://hexdocs.pm/foundation).
## License
Foundation is released under the MIT License. See [LICENSE](LICENSE) for details.