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):

two browsers on two different nodes; an order made in one shows in the other

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 cluster proof

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:

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.