Camunda 8 Service Task with Java

 In Camunda 8, a Service Task is executed by a Job Worker (external worker). In Spring Boot, the easiest way to implement this is with @JobWorker, where the job type (a.k.a. task type) in the BPMN matches the worker’s type.

This blog shows a complete working example: BPMN service task → Java worker → update variables → handle failures.


1) BPMN: Configure the Service Task (Job Type)

In Camunda Modeler:

  1. Add a Service Task

  2. Set Implementation to Job Worker (or “External” depending on Modeler version)

  3. Set Type to: payment-worker (example)

  4. (Optional) Set Retries: 3

Important: The Type must match the Java worker annotation:

@JobWorker(type = "payment-worker")

2) Spring Boot Dependencies (Maven)

Use Camunda 8 Spring Boot starter (Zeebe):

<dependency> <groupId>io.camunda</groupId> <artifactId>spring-boot-starter-camunda</artifactId> </dependency>

If you’re using an older setup, you may see io.camunda.spring:zeebe-spring-boot-starter. Use whichever matches your project, but the concept remains the same.


3) Configuration (application.yaml)

If you run Camunda 8 locally (Self-Managed)

You’ll typically configure the Zeebe gateway connection:

camunda: client: mode: self-managed zeebe: gateway-address: 127.0.0.1:26500 auth: enabled: false

If you use Camunda 8 SaaS

You configure clusterId / clientId / clientSecret etc. (different block). Share your mode if you want the exact config.


4) Java Worker Example (@JobWorker) — Read + Update Variables

Here’s a clean worker that:

  • reads process variables (orderId, amount)

  • does some business logic

  • returns variables (txnId, paymentStatus)

package com.realtech.workflow.workers; import io.camunda.zeebe.client.api.response.ActivatedJob; import io.camunda.zeebe.spring.client.annotation.JobWorker; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Component public class PaymentWorker { @JobWorker(type = "payment-worker", autoComplete = true) public Map<String, Object> handle(final ActivatedJob job) { Map<String, Object> vars = job.getVariablesAsMap(); String orderId = (String) vars.get("orderId"); Double amount = vars.get("amount") == null ? null : ((Number) vars.get("amount")).doubleValue(); // --- Business logic (example) --- // Call payment gateway, validate, etc. String txnId = "TXN-" + System.currentTimeMillis(); Map<String, Object> out = new HashMap<>(); out.put("txnId", txnId); out.put("paymentStatus", "SUCCESS"); out.put("processedOrderId", orderId); out.put("chargedAmount", amount); return out; // variables updated in the workflow } }

Why autoComplete=true is nice
It automatically completes the job after the method returns successfully.


5) Failure Handling (Retries + Incidents)

In real projects, jobs fail. You should:

  • throw an exception to trigger retries

  • or use a BPMN Error pattern if you want a modeled error path

Option A: Throw Exception → Zeebe retries

@JobWorker(type = "payment-worker", autoComplete = true) public Map<String, Object> handle(ActivatedJob job) { Map<String, Object> vars = job.getVariablesAsMap(); if (vars.get("amount") == null) { throw new RuntimeException("amount is missing"); } // ... }

If retries become 0, Camunda will create an Incident visible in Operate.

Option B: Manual control (autoComplete=false)

Useful when you want full control over complete/fail:

@JobWorker(type = "payment-worker", autoComplete = false) public void handle(final ActivatedJob job) { try { // do work... Map<String, Object> out = Map.of("paymentStatus", "SUCCESS"); // complete job with variables job.getClient().newCompleteCommand(job.getKey()) .variables(out) .send() .join(); } catch (Exception ex) { job.getClient().newFailCommand(job.getKey()) .retries(job.getRetries() - 1) .errorMessage(ex.getMessage()) .send() .join(); } }

6) Variable Mapping Tip (Avoid “null” surprises)

If your DMN/service tasks depend on variables, prefer:

  • consistent variable names (orderId, amount)

  • JSON-friendly structures (Camunda 8 is JSON-first)

  • input/output mapping in BPMN if needed

Example: map amount into a nested object:

  • payment.amount = amount


7) How to Test Quickly (Local)

Minimum steps:

  1. Start Camunda 8 (Docker Compose)

  2. Deploy BPMN from Modeler

  3. Start a process instance (with variables)

  4. Run your Spring Boot app

  5. Watch the service task complete in Operate

Sample start variables:

{ "orderId": "ORD-1001", "amount": 1499 }

8) Common Mistakes (That Cause “Worker Not Picking Job”)

  • Job type mismatch: BPMN type ≠ @JobWorker(type="...")

  • Worker not running / not connected to gateway

  • Wrong gateway address/port

  • Auth enabled but not configured (SaaS/self-managed mismatch)

  • Variables are missing or wrong type (e.g., amount is string not number)


Conclusion

In Camunda 8, a Service Task is executed by a Job Worker. With Spring Boot, @JobWorker is the simplest way to:

  • subscribe to a job type

  • implement business logic

  • update process variables

  • handle failures and retries cleanly

💼 Need Help with Camunda, Jira, or Enterprise Workflows?

I help teams solve real production issues and build scalable systems.

Services I offer:
• Camunda & BPMN workflow design and debugging  
• Jira / Confluence setup and optimization  
• Java, Spring Boot & microservices architecture  
• Production issue troubleshooting  


📩 Email: ishikhanirankari@gmail.com | info@realtechnologiesindia.com

✔ Available for quick consulting calls and project-based support
✔ Response within 24 hours


🎥 Learn IT with Shikha on YouTube

Prefer learning through videos? Watch practical tutorials on Kafka, Camunda, Alfresco, Java, Spring Boot, Microservices and Enterprise Architecture.

▶ Subscribe to Learn IT with Shikha on YouTube

Comments

Popular posts from this blog

Top 50 Camunda BPM Interview Questions and Answers for Developers (2026 Guide)

10 BPMN Best Practices Every Camunda Developer Should Know

OOPs Concepts in Java | English | Object Oriented Programming Explained