A fully functional Java project demonstrating a simplified Uber-Eats-style workflow using Cadence. In this repo you’ll find:
-
Two Workflows:
HandleEatsOrder– receives an order, waits for a restaurant’s approval, prepares food, then invokes a child workflow.DeliverOrder– simulates delivery of the order.
-
One Activity Interface (
OrderActivities) and its implementation (OrderActivitiesImpl), responsible for printing/logging order-related messages. -
A Worker Starter (
WorkerStarter.java) that registers workflows and activities on the “EatsTaskList.” -
A Workflow Starter (
WorkflowStarter.java) that kicks off (and signals) theHandleEatsOrderinstance. -
A
logback.xmlundersrc/main/resourcesfor SLF4J/Logback configuration, allowing logs to be captured in both console and the Cadence Web UI.
The rest of this document explains:
- How Does the Code Work
- How to Run the Code from Command Line
- Cadence Learning Process and Implementation
- Feedback on This Process
At a high level, this project implements a two-step Uber-Eats order flow:
-
HandleEatsOrderWorkflow- Start Parameters
userId- a randomly generated UUID representing the customer.orderId- a randomly generated UUID for the specific cart/order.restaurantId- a randomly generated UUID for the restaurant.items- aList<String>with order contents (e.g.,["cheeseburger", "diet coke"]).
- Flow Steps
- Print/Log “Order Received”
- The workflow calls
OrderActivities.notifyOrderReceived(...), which logsto both console and Cadence UI.[Activity] Your order received! user=<userId>, order=<orderId>, restaurant=<restaurantId>, items=[...]
- The workflow calls
- Await Restaurant Signal
- The workflow registers a signal method
acceptOrder(boolean accepted). Internally, it executesblocking until some external thread (in our case,Workflow.await(() -> orderAccepted);
WorkflowStarter) invokeswf.acceptOrder(true).
- The workflow registers a signal method
- Preparation Sleep
- Once
orderAccepted == true, the workflow callsto simulate “food preparation time.”Workflow.sleep(Duration.ofSeconds(3));
- Once
- Invoke Child Workflow
DeliverOrder- We create a typed child workflow stub:
DeliverOrder child = Workflow.newChildWorkflowStub( DeliverOrder.class, ChildWorkflowOptions.newBuilder() .setExecutionStartToCloseTimeout(Duration.ofSeconds(30)) .build() );
- Then call
child.deliver(orderId), which transitions into the child workflow logic.
- We create a typed child workflow stub:
- (Child Workflow) Delivery
- Inside
DeliverOrderImpl.deliver(String orderId), we log:wfLogger.info("ChildWorkflow: Starting delivery for order {}", orderId); Workflow.sleep(Duration.ofSeconds(4)); activities.notifyInFrontDoor(orderId);
- The activity then logs:
[Activity] Order <orderId> is in front of your door!
- Inside
- Print/Log “Order Received”
- Start Parameters
-
OrderActivitiesInterface & Implementation- Interface (
OrderActivities.java) uses the@ActivityInterfaceannotation and declares three methods:These notifications appear in your console.void notifyOrderReceived(String userId, String orderId, String restaurantId, List<String> items); void notifyInFrontDoor(String orderId); void notifyOrderDelivered(String orderId);
- Interface (
-
Worker Starter (
WorkerStarter.java)public class WorkerStarter { public static void main(String[] args) { WorkflowClient client = WorkflowClient.newInstance( new Thrift2ProtoAdapter( IGrpcServiceStubs.newInstance( ClientOptions.newBuilder() .setFeatureFlags( new FeatureFlags() .setWorkflowExecutionAlreadyCompletedErrorEnabled(true)) .setPort(Constants.Port) .build())), WorkflowClientOptions.newBuilder().setDomain(Constants.CadenceDomain).build()); WorkerFactory factory = WorkerFactory.newInstance(client); Worker worker = factory.newWorker(Constants.TaskList); worker.registerWorkflowImplementationTypes( HandleEatsOrderImpl.class, DeliverOrderImpl.class); worker.registerActivitiesImplementations(new OrderActivitiesImpl()); factory.start(); } }
- What it does: Connects to a Cadence server on
localhost:7933in domaincadence-domain. - Registers both
HandleEatsOrderImplandDeliverOrderImplunder the same task listEatsTaskList. - Registers the activity implementation
OrderActivitiesImpl. - Starts polling for tasks—now the worker can pick up both workflow and activity tasks.
- What it does: Connects to a Cadence server on
-
Workflow Starter (
WorkflowStarter.java)public class WorkflowStarter { public static void main(String[] args) { WorkflowClient client = WorkflowClient.newInstance( new Thrift2ProtoAdapter( IGrpcServiceStubs.newInstance( ClientOptions.newBuilder() .setFeatureFlags( new FeatureFlags() .setWorkflowExecutionAlreadyCompletedErrorEnabled(true)) .setPort(Constants.Port) .build())), WorkflowClientOptions.newBuilder() .setDomain(Constants.CadenceDomain) .build()); WorkflowOptions workflowOptions = new WorkflowOptions.Builder() .setTaskList(Constants.TaskList) .setExecutionStartToCloseTimeout(Duration.ofSeconds(Constants.StartToCloseTimeout)) .build(); HandleEatsOrder wf = client.newWorkflowStub(HandleEatsOrder.class, workflowOptions); WorkflowClient.start(wf::handleOrder, UUID.randomUUID().toString(), UUID.randomUUID().toString(), UUID.randomUUID().toString(), Arrays.asList("cheeseburger", "diet coke")); try { Thread.sleep(2000); } catch (InterruptedException e) {} // 3. Send an accept signal to your workflow wf.acceptOrder(true); } }
- Sequence:
- Create a typed stub for
HandleEatsOrder. - Call
WorkflowClient.start(...), which sends a “start workflow” request to Cadence. - Sleep 2 seconds to ensure the workflow execution is up & running (avoiding race conditions).
- Call
wf.acceptOrder(true), sending the signal that “the restaurant accepted the order.”
- Create a typed stub for
- After signaling,
HandleEatsOrderImpl::handleOrdercan proceed past theWorkflow.await(...)line.
- Sequence:
-
Cadence Configuration
- Domain:
cadence-domain(must be registered in Cadence beforehand). - Task List:
EatsTaskList(all workflows/activities poll on this). - Timeouts: Activities, the child workflow and the main workflow use 30 second timeouts defined in
Constants.java.
- Domain:
Below are step-by-step instructions for both PowerShell (Windows) and Bash Shell (macOS/Linux). By default, this project uses the Gradle Wrapper (gradlew / gradlew.bat) so you do not need to install Gradle globally.
-
Ensure Cadence Server is Running
- If you have Docker installed, you can spin up a local Cadence server quickly:
docker run --rm --name cadence-server -p 6379:6379 -p 7933:7933 ubercadence/server:latest
- In another shell, register the domain:
cadence --domain cadence-domain domain register
If you don’t have the Cadence CLI, you can download it from releases or use a Docker container:
docker run --rm -v "${PWD}:/cadence" ubercadence/cli:latest --domain cadence-domain domain register
- If you have Docker installed, you can spin up a local Cadence server quickly:
-
Clone this repo
git clone https://github.com/Bueller87/cadence-handle-eats-order.git -
Grant Execute Permission (macOS/Linux)
chmod +x gradlew
# From project root:
.\gradlew.bat clean build --refresh-dependencies# From project root:
./gradlew clean build --refresh-dependencies- What this does:
- Downloads all dependencies (Cadence client, SLF4J, Logback).
- Compiles all
.javafiles. - Copies
logback.xmlintobuild/resources/main/. - Runs any tests (there are none by default).
- Produces a JAR under
build/libs/.
# From project root:
.\gradlew.bat runWorkeropen a second new powershell instance
# From project root:
.\gradlew.bat runWorkflowBelow is a chronological breakdown of my Cadence learning and how it informed this implementation:
-
Initial Research
- Objective: Understand core Cadence concepts—workflows, activities, task lists, timeouts, signals and child workflows.
- Resources Consulted:
- Cadence Official Documentation (intro tutorials, Java SDK guide).
- Cadence GitHub Samples.
- YouTube Videos.
- Key Takeaways:
- A Workflow is durable, single-threaded, and only contains deterministic (predictable ) code & Cadence API calls.
- An Activity is non-deterministic, can call external systems, and must specify timeouts.
- Signals can be sent to a running workflow to influence its path.
- Child Workflows allow one workflow to spin up sub-workflows and await their completion.
-
Setting Up the Environment
- Spun up a local Cadence docker container.
- Registered a test domain (
cadence-domain).
-
Hello-World Workflow
- Started by replicating a minimal Cadence Java sample (a “HelloWorld” that prints “hello” → sleeps → prints “world”).
- Verified:
- Code structure: Interface + Implementation for workflow and activity.
- How
WorkerFactoryandWorkflowClientconnect tolocalhost:7933. - How
Workflow.sleep(...)appears as a timer in the History.
-
Signal and Sleep Patterns
- Extended to include a Signal:
Verified the workflow could block on
@SignalMethod void someSignal(String data);
Workflow.await(...)and unblock upon receiving a signal. - Verified
Workflow.await(...)andWorkflow.sleep(...)both generate events in the UI.
- Extended to include a Signal:
-
Child Workflows
- Added a child workflow (
ChildWorkflow) that returns a value. - Called the child via a stub:
ChildWorkflow child = Workflow.newChildWorkflowStub( ChildWorkflow.class, ChildWorkflowOptions.newBuilder() .setExecutionStartToCloseTimeout(...) .build() ); String result = child.execute(...);
- Observed how parent workflow history included events for “StartChildWorkflow” and “ChildWorkflowCompleted.”
- Added a child workflow (
-
Combining All Pieces
- Defined a realistic example: “Uber-Eats Order” flow with:
- Activities to log order reception & delivery.
- Signal to accept/reject.
- A 3s preparation sleep.
- A 4s child workflow for delivery.
- Final log when delivery completes.
- Defined a realistic example: “Uber-Eats Order” flow with:
-
Testing and Iteration
- Manually tested in both PowerShell and Bash.
- Confirmed logs appear correctly in both console and UI. Though at this time I cannot find logs in the console.
- Adjusted timeouts when receiving
IllegalStateException(addedScheduleToCloseTimeout).
Below is my feedback on how this Cadence learning & implementation process went, with suggestions for improvement:
| Area | What Went Well | What Can Be Improved |
|---|---|---|
| Official Documentation | - Clear examples for “HelloWorld,” “Signal,” “Child” | - The Java SDK guide has gaps: workflow/activity timeout requirements aren’t front-and-center. - No unified “getting started” for Java+VS Code specifically. |
| Sample Repositories | - GitHub demos show code patterns (Interface/Impl). | - Many samples are in multi-module layouts or require heavy boilerplate. - Lack of a single, end-to-end “Uber-Eats”-style case. |
| Timeout/Error Guidance | - When IllegalStateException occurs, error is clear. |
- Exception message (“Either ScheduleToClose or both ScheduleToStart & StartToClose required”) wasn’t obvious if you haven’t seen it before. - Would be nice if the docs recommended a “minimal working” ActivityOptions snippet in every example. |
| Logging & UI Visibility | - Workflow.getLogger(...) examples exist, but rarely in main docs. |
- Documentation could explicitly show “if you use SLF4J in workflow, it will not appear in UI; use Workflow.getLogger instead.” - More examples showing “Activity Logs” vs “Workflow Logs.” |
| Community Help / Blogs | - StackOverflow questions often drill into specific errors (e.g., missing timeouts). | - Answers are scattered: need to search multiple threads to get a complete picture. - An “official” troubleshooting guide for common pitfalls (timeouts, logging, signals) would be invaluable. |
-
Enhanced Java SDK Quickstart
- A one-page “Java + Gradle + VS Code” quickstart guide in official docs, showing:
- How to generate and run the Gradle wrapper.
- How to import into VS Code and resolve dependencies.
- A minimal sample with a signal + sleep + activity + child-workflow.
- A one-page “Java + Gradle + VS Code” quickstart guide in official docs, showing:
-
Default Timeouts in Samples
- Every code snippet in the Java SDK docs should include:
ActivityOptions.newBuilder() .setScheduleToCloseTimeout(Duration.ofMinutes(1)) .build();
- By default, so newcomers aren’t bitten by missing-timeout errors.
- Every code snippet in the Java SDK docs should include:
-
Logging Guidance
- A dedicated “Logging & Observability” section that:
- Differentiates between
Workflow.getLogger(...)vs plain SLF4J in workflow code. - Shows a complete
logback.xmlsnippet. - Explains where those logs show up in the Web UI (History vs Activity Logs panels).
- Differentiates between
- A dedicated “Logging & Observability” section that:
-
Template Repo
- Provide a standalone “uber-eats-cadence” template repository (similar to this one), so developers can clone & immediately run a realistic end-to-end scenario without building everything from scratch.
-
Expanded “Troubleshooting”
- In docs, add a dedicated sub-page for common errors:
IllegalStateException: Either ScheduleToClose or both ScheduleToStart and StartToClose timeouts are requiredWorkflowServiceExceptionfrom signaling too early- “Workflow not found” / “TaskList not found” errors
- “Logging does not appear in UI” and how to fix with
Workflow.getLogger(...)or Logback.
- In docs, add a dedicated sub-page for common errors:
-
More Recent End to End Demos on YouTube
- Most content on YouTube is from before 2020.
- Developers might get the impression that this is old technology.



