PATTERN Cited by 1 source
Workflow primitives as annotated classes¶
Pattern¶
Expose durable-workflow primitives (workflow method, state fields, signals, actions, compensation, checkpointing) as annotations on plain Java / Kotlin classes, with no codegen client, no DSL, and no separate workflow-definition format. Developers write a class, tag methods and fields with a small set of annotations, and the workflow engine handles the orchestration / persistence / replay behaviour invisibly. The class is the workflow definition.
Skipper (Airbnb, 2026-04-28) is the
canonical wiki instance; its minimal annotation set is
@WorkflowMethod, @StateField (/@StateParam),
@SignalMethod, @Execute(checkpoint = true), @Compensate.
(Source:
sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)
Canonical instance¶
From Skipper's post:
// Define workflow logic as normal-looking Kotlin
class ChargeAndAccept : Workflow() {
private val billing = actions<BillingActions>()
private val reservations = actions<ReservationActions>()
@StateParam var paymentCaptured = false
@WorkflowMethod
suspend fun execute(r0: Reservation): Reservation {
val r1 = billing.charge(r0) // durable side-effect boundary
waitUntil { paymentCaptured } // durable wait (resumes after restart)
return reservations.markAccepted(r1)
}
}
// Side effects live in Actions; one annotation makes it checkpointable
class BillingActions : Actions() {
@Execute(checkpoint = true)
suspend fun charge(r: Reservation): Reservation =
billingApi.chargeAsync(r.id, r.amount).await()
}
// Invoke it like a normal typed call (no codegen client)
val out = workflow<ChargeAndAccept>("reservation:${req.id}").execute(req)
(Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)
The annotation set¶
A minimal-but-complete workflow contract:
| Annotation | Target | Purpose |
|---|---|---|
@WorkflowMethod |
Method on Workflow subclass |
The top-level entry point; workflow orchestration logic goes here. |
@StateField / @StateParam |
Field on Workflow subclass |
Persisted across replays; mutated by signals or action returns. |
@SignalMethod |
Method on Workflow subclass |
Externally-invoked handler that mutates state (see concepts/workflow-signal). |
@Execute(checkpoint = true) |
Method on Actions subclass |
The action's result is persisted on success; replay short-circuits it. |
@Compensate |
Method on Actions subclass |
Undo pair for an action; engine invokes in reverse order on non-retryable failure (see concepts/workflow-compensation-action). |
Skipper's pitch is that this is the whole contract: there is no workflow DSL, no XML, no codegen-generated client stub, no separate test harness to bring up. You write a class; you annotate it; you call it.
Why this shape works¶
The Skipper post's explicit design framing:
"By exposing workflows and actions as plain Java/Kotlin classes, with a minimal, annotation-based contract, Skipper enables developers to write business logic that looks like business logic, not framework boilerplate. Primitives such as conditional waits, durable retries, and signals are available, but they surface through the same straightforward class structure, rather than requiring developers to learn a separate execution model or wire up infrastructure abstractions. The contract stays out of the way: a workflow method reads like the process it represents, and an action method looks like the service call it wraps." (Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)
And:
"Skipper isn't the first system to offer 'write normal code, get durable execution' — other workflow engines do as well — but Skipper's focus is on removing the adoption friction: fewer required constructs and less setup, so teams that use Java/Kotlin can get to a first durable workflow with minimal ceremony." (Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)
Test ergonomics follow directly¶
A workflow that's just a class is testable as just a class. The post's test example:
class ListingPublicationTest : SkipperTest() {
@Test
fun testListingPublication() {
val workflow = workflowBuilder(ListingPublicationWorkflow::class.java).build()
workflow.publishListing(ListingSubmission("listing-123", "host-456", true))
helper.expectWorkflowToWait() // workflow waits for photo review
workflow.completePhotoReview(true) // photos approved
helper.waitForWorkflowToComplete()
val result = workflow.getResult()
assertEquals(PublicationResult.Status.SUCCESS, result.getStatus())
}
}
No queues to stand up, no infrastructure to mock. Signals are method calls; state is fields. (Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)
Contrast with DSL-based / codegen-based engines¶
Other workflow engines require a separate definition surface between the business-logic code and the engine:
- AWS Step Functions — Amazon States Language (ASL) JSON definition, external to the service code. Business logic lives in Lambdas invoked from states; the state machine is defined elsewhere.
- Early Temporal versions / Cadence —
interface-based contracts with codegen-generated clients,
more ceremony than Skipper's typed
workflow<T>(...)call. - Argo Workflows / Argo — YAML workflow definitions separate from step implementations.
Skipper's distinguishing property is the zero-ceremony contract: the class is the definition. The annotation set is small enough to remember without reference docs, and there's nothing else to configure.
Applicability¶
Works well when:
- Single-runtime environments. One language (JVM), one service boundary, one annotation system to teach to developers.
- Business logic dominates. The saving from collapsing orchestration plumbing into the workflow class is largest when the orchestration logic itself is non-trivial.
- Fast onboarding matters. Teams without dedicated platform engineers can adopt the engine self-service because the contract is discoverable and the ergonomic cost of getting it wrong is visible.
Limits:
- Cross-language requirements. An annotation-based contract is language-specific; polyglot workflows need a different contract surface (codegen, protobuf, or a cluster-native API).
- Cross-team workflow definitions. If a workflow must be defined / viewed / reviewed outside the host service's codebase (product managers authoring workflows; ops reviewing without JVM access), a separate definition surface may still be needed.
Seen in¶
- sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine
— canonical wiki disclosure. Skipper's 5-annotation contract
(
@WorkflowMethod,@StateField/@StateParam,@SignalMethod,@Execute(checkpoint = true),@Compensate) as the full workflow surface. No codegen, no DSL, no separate config surface. Workflow invocation (workflow<T>("id").execute(req)) and test harness (workflowBuilder<T>::class.java).build()) both stay inside the Kotlin/Java type system. Explicit design goal: "removing the adoption friction: fewer required constructs and less setup."
Related¶
- systems/airbnb-skipper — canonical instance.
- systems/temporal — similar shape (interface-based
workflows + activity classes +
@WorkflowMethod/@SignalMethodannotations) with heavier codegen / determinism-checker infrastructure. - concepts/embedded-workflow-engine — the deployment-model pattern Skipper pairs this with.
- concepts/durable-execution — parent property.
- concepts/workflow-replay-from-checkpointed-actions — the durability mechanism this ergonomic pattern sits on top of.
- concepts/workflow-compensation-action —
@Compensateannotation specifically. - concepts/workflow-signal —
@SignalMethodannotation specifically. - concepts/workflow-determinism-requirement — the correctness invariant the ergonomic contract doesn't explicitly enforce.
- patterns/checkpoint-resumable-fiber — sibling
developer-ergonomic pattern in a different ecosystem
(Cloudflare Project Think's
runFiber()+stash()+onFiberRecovered): both keep the durable-execution primitive syntactically close to ordinary code.