dandelion
Simplicity with resilience and joy.
A whole cloud in one app — web server, workers, queue, pub/sub, cache, cron, a live frontend — as one Elixir release, laid out the way a Go service is. Run one copy on one VM; run 3 or 10 and they join into one system. Only the load balancer, Postgres and file storage stay outside.
This package is the library half: the cloud pieces that are the same in
every dandelion project. The other half, the generator (dandelion_new),
writes a project that uses them.
mix archive.install hex dandelion_new
mix dandelion.new my_app # a service that already depends on this library
Proof
Every claim is recorded, not just written down — evidence/
has a log, a GIF and a page for each, and what is not proven.
An order made on one node appears in a browser on another (two browsers, two different nodes, real Chromium):
A job left by a killed node is run again by another, and payment events keep their order while three nodes race for them — with negative controls that show the same checks fail when the feature is switched off:
The library
Thin over Oban, Cachex,
Phoenix.PubSub and libcluster — not a
new API on top of them. You need Postgres and a Phoenix.PubSub.
def deps do
[{:dandelion, "~> 0.1"}]
end
| Module | What it is | Replaces |
|---|---|---|
Dandelion.Cluster |
nodes find each other through Postgres (NOTIFY) |
Consul, Kubernetes DNS |
Dandelion.Cache |
per-node memory cache, cleared on every node | Redis as a cache |
Dandelion.Queue |
the Oban setup: queues, lifeline, pruner, crontab | Cloud Tasks, Cloud Scheduler |
Dandelion.Queue.Ordered |
order per key, across nodes | SQS FIFO, Kafka partition keys |
Dandelion.PubSub |
a topic → one Oban job per subscriber, in your transaction | Google Pub/Sub, Kafka topics |
Dandelion.Migration |
the database index the ordered queue needs | — |
# application.ex
children = [
MyApp.Repo,
{Dandelion.Cluster, otp_app: :my_app, repo: MyApp.Repo},
{Phoenix.PubSub, name: MyApp.Broadcast},
{Dandelion.Cache, pubsub: MyApp.Broadcast},
{Oban, Dandelion.Queue.config(otp_app: :my_app, repo: MyApp.Repo, crontab: MyApp.Cron.schedule())}
]
# a migration, after Oban's
def up, do: Dandelion.Migration.up()
def down, do: Dandelion.Migration.down()
# in your code
Dandelion.Cache.fetch({:product, sku}, fn -> Repo.get(Product, sku) end)
Dandelion.Cache.delete({:product, sku}) # after the commit, on every node
Repo.transact(fn ->
{:ok, order} = insert_order(params)
Dandelion.PubSub.publish(%{"order.created" => [SendConfirmation]}, "order.created", %{"id" => order.id})
{:ok, order} # saved together, or not at all
end)
Dandelion.Queue.Ordered.insert(MyWorker.new(args), "order:42") # then, in perform/1:
# with :ok <- Dandelion.Queue.Ordered.turn(job), do: ...
Each module's docs say what it promises and what it doesn't. The rules behind them: Postgres decides; memory only makes things faster. Anything that must survive a crash or happen exactly once goes through the database; the cache and live broadcasts may be lost or briefly stale.
Nodes trust each other fully — anyone holding the Erlang cookie can run code on every node. Keep them on a private network.
The rest of the project
On GitHub:
example/— a service using all of it:lib/platform/(what every app runs on) andlib/app/shop/(one domain laid out as handlers → services → repos → models), a Vue + blessing-ui frontend, a release Docker image.deploy/runs it as 3 nodes and proves the claims (an order made on one node shows up on another; a killed node's jobs are run again by the others; payment events keep their order while three nodes race for them).phrasebook/— for each Go habit, the Elixir way: 12 pages on the service, 5 on the rest of the cloud.DESIGN.md— why it's built this way.
Name: dandelion — the plainest flower there is; it grows through cracks in concrete and comes back every time you pull it; its seeds scatter on the wind — one flower becoming many, the way a service starts single and grows into a cluster.
License
MIT — see LICENSE.