Skuld
Evidence-passing Algebraic Effects for Elixir.
The problem
Between your pure business logic and your side-effecting infrastructure sits the orchestration layer: "fetch the user, check permissions, load their subscription, compute the price, write the invoice." This code encodes your most important business rules, but it's tangled with databases, APIs, and randomness - making it hard to test, hard to refactor, and impossible to property-test.
The insight
Skuld adds a layer between pure and side-effecting code: effectful code. Domain logic requests effects (database access, randomness, error handling) without performing them. Handlers decide what those requests mean. The same orchestration code runs with real handlers in production and pure in-memory handlers in tests - fully deterministic, trivially testable.
What Skuld solves
| Pain point | What Skuld does | More |
|---|---|---|
| Orchestration code is untestable | Swap handlers: same code runs in-memory with no DB, no network | Testing |
| Stateful test doubles for complex flows | Reusable in-memory handlers with read-after-write consistency | Stateful testing |
| Non-deterministic UUIDs / randomness | Fresh and Random have deterministic test handlers - same test, same values | Determinism |
| N+1 queries | deffetch + query batch independent loads automatically | Batching |
| Long-running workflows across restarts | EffectLogger serialises progress; resume from where you left off | Durable workflows |
| LiveView multi-step operations | AsyncComputation bridges effects into LiveView's process model | LiveView |
| Hexagonal architecture plumbing | Port.Contract / Port.Adapter.Effectful - typed boundaries, no parameter threading | Hex arch |
See What Skuld Solves for worked examples of each.
Quick example
defmodule Onboarding do
use Skuld.Syntax
defcomp register(params) do
# Read configuration from the environment
config <- Reader.ask()
# Generate a deterministic ID
id <- Fresh.fresh_uuid()
# Use a port for the database call
user <- UserRepo.EffectPort.create_user!(%{id: id, name: params.name, tier: config.default_tier})
# Accumulate domain events
_ <- EventAccumulator.emit(%UserRegistered{user_id: id})
{:ok, user}
end
endRun with production handlers:
Onboarding.register(%{name: "Alice"})
|> Reader.with_handler(%{default_tier: :free})
|> Fresh.with_uuid7_handler()
|> Port.with_handler(%{UserRepo => UserRepo.Ecto})
|> EventAccumulator.with_handler(output: fn r, events ->
MyApp.EventBus.publish(events)
r
end)
|> Throw.with_handler()
|> Comp.run!()Run with test handlers - same code, no database, fully deterministic:
Onboarding.register(%{name: "Alice"})
|> Reader.with_handler(%{default_tier: :free})
|> Fresh.with_test_handler()
|> Port.with_test_handler(%{
UserRepo.EffectPort.key(:create_user, %{id: _, name: "Alice", tier: :free}) =>
{:ok, %User{id: "test-uuid", name: "Alice", tier: :free}}
})
|> EventAccumulator.with_handler(output: fn r, events -> {r, events} end)
|> Throw.with_handler()
|> Comp.run!()Installation
Add skuld to your dependencies in mix.exs (see Hex for the current version):
def deps do
[
{:skuld, "~> 0.3"}
]
endWhat can it do?
Foundational effects
Effects that solve problems every Elixir developer recognises. They feel like well-structured Elixir code with better testability.
| Side-effecting operation | Effectful equivalent |
|---|---|
| Configuration / environment | Reader |
| Process dictionary / state | State, Writer |
| Random values | Random |
| Generating IDs (UUIDs) | Fresh |
| Transactions | Transaction |
| Blocking calls to external code | Port, Port.Contract |
| Effectful code from plain code | Port.Adapter.Effectful |
| Mutation dispatch | Command |
| Domain event collection | EventAccumulator |
| Fork-join concurrency | Parallel |
| Thread-safe state | AtomicState |
| Effects from LiveView | AsyncComputation |
| Raising exceptions | Throw |
| Resource cleanup (try/finally) | Bracket |
| Effectful list operations | FxList, FxFasterList |
Advanced effects
Effects that use cooperative fibers and continuations to do things that aren't possible with standard BEAM patterns. You don't need these to get value from Skuld - the foundational effects stand on their own.
| Pattern | Effectful equivalent |
|---|---|
| Coroutines / suspend-resume | Yield |
| Cooperative fibers | FiberPool |
| Bounded channels | Channel |
| Streaming with backpressure | Brook |
| Automatic N+1 query batching | query, deffetch, Query.Cache |
| Serializable coroutines | EffectLogger |
Documentation
New to algebraic effects? Start with Why Effects? - the problem, explained without jargon.
Ready to code? Jump to Getting Started for your first computation.
Full documentation
| Layer | Topic | Description |
|---|---|---|
| 1 | Why Effects? | The problem effects solve |
| 2 | The Concept | How algebraic effects work |
| What Skuld Solves | Concrete problems, worked examples | |
| 3 | Getting Started | Your first computation |
| 4 | Syntax In Depth | comp, else, catch, defcomp |
| 5 | Foundational Effects | |
| State & Environment | State, Reader, Writer | |
| Error Handling | Throw, Bracket | |
| Value Generation | Fresh, Random | |
| Collections | FxList, FxFasterList | |
| Concurrency | Parallel, AtomicState, AsyncComputation | |
| Persistence | Transaction, Command, EventAccumulator | |
| External Integration | Port, Port.Contract, Port.Adapter.Effectful | |
| 6 | Advanced Effects | |
| Yield | Coroutines | |
| Fibers & Concurrency | FiberPool, Channel, Brook | |
| Query & Batching | Automatic N+1 prevention | |
| EffectLogger | Serializable coroutines | |
| 7 | Recipes | |
| Testing | Property-based testing with effects | |
| Hexagonal Architecture | Port.Contract + Port.Adapter.Effectful | |
| Decider Pattern | Event-sourced domain logic | |
| Handler Stacks | Composing production & test stacks | |
| LiveView | Multi-step wizards | |
| Durable Workflows | Persist-and-resume with EffectLogger | |
| Data Pipelines | Streaming with Brook | |
| Batch Loading | N+1-free data access | |
| 8 | Internals | |
| Internals | Implementation details | |
| Reference | API reference and comparisons |
Demo Application
See TodosMcp - a voice-controllable todo application built with Skuld. It demonstrates command/query structs with algebraic effects for LLM integration and property-based testing. Try it live at https://todos-mcp-lu6h.onrender.com/
License
MIT License - see LICENSE for details.