Durable execution, embedded in your service
Skipper is a lightweight workflow engine for the JVM. Write complex, long-running business logic as plain Java or Kotlin — and let Skipper guarantee it runs to completion, in spite of failures.
No cluster to operate. No new database. Just a dependency:
dev.skipper:skipper — coordinates are illustrative until the first public release.
class CheckoutWorkflow : Workflow() {
private val payments = actions<PaymentActions>()
private val email = actions<EmailActions>()
@WorkflowMethod
suspend fun checkout(order: Order): Receipt {
// Charged once — even if the workflow resumes later.
val charge = payments.charge(order)
// Retried automatically until it succeeds.
email.sendReceipt(order, charge)
return Receipt(charge.id)
}
} public class CheckoutWorkflow extends Workflow {
private final PaymentActions payments = actions(PaymentActions.class);
private final EmailActions email = actions(EmailActions.class);
@WorkflowMethod(returnType = Receipt.class)
public CompletableFuture<Receipt> checkout(Order order) {
// Charged once — even if the workflow resumes later.
Charge charge = payments.charge(order);
// Retried automatically until it succeeds.
email.sendReceipt(order, charge);
return CompletableFuture.completedFuture(new Receipt(charge.getId()));
}
} Workflow durability without the operational tax
The reliability of a workflow engine, delivered as a library you already know how to run.
Durable by default
Workflows are guaranteed to reach a terminal state despite transient failures. Skipper classifies and retries errors for you, so your business logic stays clean.
A library, not a cluster
Embed Skipper in your existing service or run it as a sidecar. There is no separate cluster to deploy, scale, or get paged about at 3am.
Reuses your database
Start instantly on an embedded store, then point Skipper at the MySQL your service already runs — all behind pluggable adapters. No new critical dependency.
A workflow is just a class
This peer-to-peer transfer waits for manual approval on large amounts, then moves the money. Notice what is not here: retries, state persistence, and failure handling are the engine's job.
- No error-handling boilerplate. Skipper retries transient failures; your method stays pure business logic.
waitUntilhibernates. The workflow can pause for a day or longer, consuming zero resources, then resume exactly where it left off.- Signals wake it up.
approve(...)is called from the outside world — an API handler, a queue consumer, or a human. - Automatic checkpoints. Completed actions are persisted, so a resumed workflow never repeats work it has already done.
class PeerToPeerTransfer : Workflow() {
private val ledger = actions<Ledger>()
@StateField var isApproved: Boolean? = null
@WorkflowMethod
suspend fun transfer(request: TransferRequest): Boolean {
// Transfers over $1,000 need a human in the loop.
if (request.amount > 1000) {
// Hibernate until approved — or up to a day — using zero resources.
val approved = waitUntil({ isApproved != null }, Duration.ofDays(1))
if (!approved || isApproved != true) return false
}
ledger.debit(request.from, request.amount)
ledger.credit(request.to, request.amount)
return true
}
// A signal wakes the waiting workflow up from the outside world.
@SignalMethod
fun approve(isApproved: Boolean) {
this.isApproved = isApproved
}
} public class PeerToPeerTransfer extends Workflow {
private final Ledger ledger = actions(Ledger.class);
@StateField Boolean isApproved;
@WorkflowMethod(returnType = Boolean.class)
public CompletableFuture<Boolean> transfer(TransferRequest request) {
// Transfers over $1,000 need a human in the loop.
if (request.getAmount() > 1000) {
// Hibernate until approved — or up to a day — using zero resources.
boolean approved = waitUntil(() -> isApproved != null, Duration.ofDays(1));
if (!approved || !isApproved) {
return CompletableFuture.completedFuture(false);
}
}
ledger.debit(request.getFrom(), request.getAmount());
ledger.credit(request.getTo(), request.getAmount());
return CompletableFuture.completedFuture(true);
}
// A signal wakes the waiting workflow up from the outside world.
@SignalMethod
public void approve(boolean isApproved) {
this.isApproved = isApproved;
}
} Everything a durable workflow needs
Batteries-included primitives for the hard parts of long-running, stateful processes.
Durable state & checkpoints
Completed action results are persisted and restored, so side effects never run twice when a workflow resumes.
Signals & queries
Push data into a running workflow with @SignalMethod, or read its state with @QueryMethod. Signals can be made durable.
Compensation (Saga)
Add @Compensate methods and Skipper unwinds completed actions in reverse order when a workflow fails.
Retries & error handling
A pluggable exception classifier, fixed / exponential / persistent retry strategies, and a dead-letter queue for the rest.
Versioning & evolution
Named checkpoints and version gates let you change running workflows without breaking in-flight instances.
First-class testing
SkipperTest helpers run workflows end-to-end in-memory and assert on state, completion, and compensation.
Admin UI & metrics
A built-in admin UI inspects running instances; core dashboards track actions, scheduler, and storage.
Pluggable storage
Storage and scheduler sit behind clean interfaces — bring your own backend or use the bundled MySQL adapter.
How durable execution works
Four moving parts. Your code touches the first two; Skipper handles the rest.
1. Workflow
Your deterministic business logic, written as a plain Java or Kotlin class.
2. Actions
The side-effecting steps — I/O, RPCs, database writes — that the workflow calls.
3. Checkpoints
Completed actions and state are persisted, so finished work is never repeated.
4. Hibernate & resume
Waits consume no resources; the workflow resumes automatically when a signal or timer fires.
Is Skipper a good fit?
Skipper is opinionated. Here is where it shines — and where it does not.
A great fit if…
- You need durable execution — the workflow must run to a terminal state in spite of failures.
- Your process coordinates multiple steps, long waits, or human approvals.
- You would consider Temporal, but you do not want to run and operate a cluster.
- Your service is Tier 0 and cannot take a dependency on an external workflow cluster.
Maybe not the right tool if…
- Your workflow does not require durable execution.
- Your service is written in a non-JVM language.
Ship your first durable workflow today
Pick a starting point — each takes only a few minutes.
Built in the open
Skipper is released under the Apache 2.0 license. Issues, discussions, and pull requests are welcome — see the community page to get involved.