Durable workflows,
cellular by design.

Describe a process as a graph. Wiggle runs it as a durable state machine that survives crashes, waits for humans, retries failures β€” and shards itself across isolated cells when one database is no longer enough.

Apache-2.0 Java 21+ clients: Java Β· Go Β· Python Maven Central io.github.hadielmougy
OrderWorkflow.java
Blueprint orders = Workflow.define("order-fulfilment")
        .step("validate")
        .gate("in-stock")
        .fork(Branch.of("payment",  s -> s.step("authorise").step("capture")),
              Branch.of("shipping", s -> s.step("reserve-stock").step("print-label")))
        .combine("merge")
        .step("notify")
        .build();

That's a complete, durable, parallel workflow. No YAML, no replay, no determinism rules to memorize β€” a compiled graph the server owns, and plain Java (or Go, or Python) methods that serve its steps.

Why teams pick it

Durable execution without the replay tax, and a sharding model that treats isolation as a first-class deployment primitive β€” not an afterthought. The full argument β†’

🧫 Cellular by design

A namespace is a cell: its own database, its own cluster. A Raft coordinator places instances by consistent hashing over epochs; the instance id carries its own routing (orders.e0.s3.01J…). Grow by adding cells; resharding never migrates data.

πŸ’Ύ Durable, honestly

Every instance is DB-backed rows, not a call stack. Exactly-once dispatch, at-least-once execution, lease-based recovery when a worker dies mid-step.

🧭 State machine, not glue code

step, gate, fork, choose, timers, signals, sub-workflows, doWhile, forEach β€” a compiled graph, versioned by content hash. The workflow is data, not replayed code, so there is no determinism contract to get wrong.

πŸ”Œ Pull-based & polyglot

Workers long-poll over gRPC: no inbound connectivity, no broker, backpressure built in. Java, Go, and Python workers interoperate on one server β€” one instance, three languages.

πŸͺΆ Lightweight & embeddable

One JAR plus a database (PostgreSQL, MySQL, Oracle, SQL Server β€” or in-memory for dev). Embed the server in your JVM for tests. The coordinator is opt-in. No Elasticsearch, no sidecar mesh, no mandatory Kubernetes.

πŸ–₯ Operable from day one

A web ops console with a live trace of every instance over the workflow diagram β€” cancel, deliver signals, schedules, search by instance or correlation id. A CLI for the control plane. Health probes and queue-lag monitoring built in.

How it works

Topology and logic are separate. The graph is pure structure; handlers are plain methods matched by name. A method's signature defines its step kind β€” a boolean return is a gate, void is an effect, anything else is a task whose return replaces the context. Nothing merges implicitly, ever.

topology β€” what happens, in what order
Blueprint orders = Workflow.define("order-fulfilment")
    .step("validate")
    .gate("in-stock")      // false β‡’ ends cleanly
    .fork(
        Branch.of("payment", s -> s
            .step("authorise",
                  RetryPolicy.exponential(5, ofMillis(100)))
            .step("capture")),
        Branch.of("shipping", s -> s
            .step("reserve-stock")
            .sleep("await-warehouse", ofMillis(300))
            .step("print-label")))
    .combine("merge")       // isolated branches rejoin HERE
    .step("notify")
    .build();

Branches run on isolated context copies β€” siblings can't see each other's writes. The only way back to shared state is the explicit combine.

logic β€” plain methods, matched by name
@Handlers("order-fulfilment")
class OrderHandlers {
  Order   validate(Order o)  { return o.withStatus("VALIDATED"); }
  boolean inStock(Order o)   { return o.quantity() > 0; }
  Order   authorise(Order o) { return o.withPaymentRef(auth(o)); }

  // the combine is mandatory: fold the branches' results onto the
  // pre-fork base and return the COMPLETE post-join context
  Order merge(@Context Order base,
              @Arm("payment")  Order pay,
              @Arm("shipping") Order ship) {
    return base.withPaymentRef(pay.paymentRef())
               .withShipmentRef(ship.shipmentRef());
  }
}

Handlers can use clocks, randomness, any library β€” and be redeployed at will. No replay means no history to stay compatible with.

These building blocks compose into the shapes real processes take β€” browse the patterns: approvals with deadlines, sagas, fan-out over collections, cron jobs, cross-service orchestration.

Architecture

Clients and pull-based workers speak gRPC to cells. Each cell is a complete cluster with its own database; an optional Raft coordinator (embedded Ratis + RocksDB — no external store) publishes shard→cell rings as epochs. New instances follow the new ring; in-flight ones finish where they live — resharding never migrates data, and any party can compute an instance's owning cell from its id alone. How placement works →

Wiggle architecture: clients and workers talk gRPC to cells; each namespace is a cell with its own cluster and database; a Raft coordinator places namespaces on cells by consistent hashing over epochs; an ops console and CLI operate everything.

Performance

Honest numbers from honest hardware β€” everything below measured on one MacBook Pro (M2 Pro, 10 cores). Full methodology β†’

91.8k
durable step completions/sec β€” engine embedded in one JVM (11,478 instances/sec, batched local chaining)
~300/s
workflow starts sustained with sub-second end-to-end latency β€” real kind cluster, PostgreSQL-backed cells
5.4s
start-availability gap when the coordinator is SIGKILL-ed under load β€” state recovers exactly; running work never notices
Probe sojourn over time: at 300 starts/sec latency settles below one second; at 340 the backlog compounds. Ceiling β‰ˆ 300–340 starts/sec on one laptop.
Probe sojourn (start β†’ COMPLETED) while ramping offered load: flat means the cluster keeps up; monotonic growth means backlog is compounding. All components shared the same 10 cores β€” a floor, not a ceiling.

Start embedded, end sharded

One codebase, four postures β€” without rewriting your workflows.

ModeWhat it isWhen
EmbeddedWiggleServer inside your JVM, in-memory storedev, tests, single-process apps
Standaloneone node, gRPC :8080, in-memory or a databasesmall services, first deploy
Clusterseveral nodes on one database β€” shared queue, leader runs timers/recoveryproduction, HA
Cellularmany cells (each its own DB + cluster) behind a coordinatormulti-tenant isolation, scale-out

β˜• Java

The reference client: DSL, @Handlers, embedded server for tests. Maven Central.

🐹 Go

wiggle-go β€” idiomatic structs-and-interfaces workers, same wire protocol.

🐍 Python

wiggle-python β€” decorator-free handler classes, contextvars step scope.

Quickstart

The fastest end-to-end is one JVM: embedded server, one worker, one instance.

build.gradle.kts
implementation("io.github.hadielmougy:wiggle-client:2.1.8")
implementation("io.github.hadielmougy:wiggle-server:2.1.8")  // embed for tests
implementation("io.github.hadielmougy:wiggle-postgres:2.1.8") // + your storage
or run the server as a container
docker run --rm -p 8080:8080 \
  -e WIGGLE_JDBC_URL=jdbc:postgresql://db:5432/wiggle \
  -e WIGGLE_JDBC_USER=wiggle -e WIGGLE_JDBC_PASSWORD=wiggle \
  hadielmougy/wiggle:2.1.8
hello, durable world
try (WiggleServer server = new WiggleServer(ServerConfig.fromEnvironment()).start();
     WiggleClient client = new WiggleClient(server.baseUrl());
     Worker worker = new Worker(client, "worker-1").register(greet).handlers(new GreetHandlers())) {
    worker.start();
    String id = client.start(greet, Map.of("name", "ada"));
    InstanceView result = client.awaitCompletion(id, ofSeconds(10));   // β†’ COMPLETED
}

Then: onboarding + full configuration reference Β· the DSL cookbook (every operator, runnable) Β· the patterns library.