Service tasks: delegates, expressions, delegate expressions
Motto — A service task is a seam, not a place. The model names what happens, and one of three wiring styles decides where the code lives.
Part of Phase 04 — Service integration & error handling. Concept reading: Principle 6 — separate the flow from the work.
The Problem
Phase 1’s service tasks used inline expressions
(${execution.setVariable('decision', ...)}). That’s fine for demos, but
unacceptable for a bureau call with authentication, timeouts, and logging. That
logic belongs in real classes in your codebase. Every team then argues the same
question: how does a box in the diagram find your Java code? Flowable gives three
answers, and choosing the wrong one costs you testability or Spring injection.
The Concept
<style> .dgm-std{background:var(–color-surface);border:1px solid var(–color-border);border-top:3px solid var(–color-border-danger-outline); border-radius:16px;padding:28px 26px 22px;box-shadow:0 1px 3px rgb(var(–shadow-color) / 0.04),0 8px 24px rgb(var(–shadow-color) / 0.04); margin:26px 0;max-width:100%;font-family:inherit;text-align:left;position:relative} .dgm-std-chip{position:absolute;top:-11px;left:24px;background:var(–color-background-danger-solid);color:var(–white); font-size:10.5px;font-weight:700;letter-spacing:1px;text-transform:uppercase; border-radius:20px;padding:3px 12px;box-shadow:0 2px 6px color-mix(in oklab, var(–color-background-danger-solid) 28%, transparent)} .dgm-std h4{margin:0 0 4px;font-size:11px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;color:var(–color-text-secondary)} .dgm-std-sub{font-size:12px;color:var(–color-text-secondary);margin:0 0 16px} .dgm-std-q{background:var(–color-surface-tertiary);border:1px dashed var(–color-border);border-radius:10px;padding:9px 14px;text-align:center; font-size:11px;font-weight:700;color:var(–color-text);margin-bottom:10px} .dgm-std-branches{display:grid;grid-template-columns:repeat(3,1fr);gap:10px} .dgm-std-b{border-radius:11px;padding:11px 13px;border:1.5px solid} .dgm-std-b h6{margin:0 0 4px;font-size:10.5px;font-weight:700} .dgm-std-b p{margin:0;font-size:10px;color:var(–color-text-secondary);line-height:1.35} .dgm-std-b.accent{background:var(–color-background-danger-surface);border-color:var(–color-border-danger-surface)} .dgm-std-b.accent h6{color:var(–color-text-danger)} .dgm-std-b.blue{background:var(–color-background-info-surface);border-color:var(–color-border-info-surface)} .dgm-std-b.blue h6{color:var(–color-text-info)} .dgm-std-b.green{background:var(–color-background-success-surface);border-color:var(–color-border-success-surface)} .dgm-std-b.green h6{color:var(–color-text-success)} .dgm-std-b.red{background:var(–color-background-danger-surface);border-color:var(–color-border-danger-surface)} .dgm-std-b.red h6{color:var(–color-text-danger)} .dgm-std-b.neutral{background:var(–color-surface-tertiary);border-color:var(–color-border)} .dgm-std-b.neutral h6{color:var(–color-text)} @media (max-width:640px){.dgm-std{padding:20px 16px 18px}.dgm-std-branches{grid-template-columns:1fr}} </style> <div class=“dgm-std”> <span class=“dgm-std-chip”>Three ways to wire a service task</span> <h4>class, delegateExpression, or expression — pick one binding style</h4> <p class=“dgm-std-sub”>All three end up calling your code; they differ in how Spring resolves the bean.</p> <div class=“dgm-std-q”>serviceTask ‘bureauCall’</div> <div class=“dgm-std-branches”> <div class=“dgm-std-b accent”> <h6>flowable:class</h6> <p>Engine instantiates the class directly</p> </div> <div class=“dgm-std-b blue”> <h6>flowable:delegateExpression</h6> <p>Resolves a Spring bean implementing JavaDelegate</p> </div> <div class=“dgm-std-b green”> <h6>flowable:expression</h6> <p>Calls any bean method, stores the return value</p> </div> </div> </div>
| Wiring | Gets Spring injection? | Needs JavaDelegate? | Use when |
|---|---|---|---|
flowable:class |
no — engine calls new |
yes | plain-engine (non-Spring) setups, simple stateless glue |
flowable:delegateExpression |
yes — it is a bean | yes | the default in Spring Boot apps |
flowable:expression |
yes | no — any method | wrapping existing services untouched: ${bureauGateway.score(pan)} |
Whichever style you choose, two contracts hold:
- The delegate runs inside the engine’s transaction (Phase 2, lesson 03). Throw, and the segment rolls back. Mark the task async, and the throw becomes a retrying job instead.
- Failures come in two kinds, and the type you throw decides the routing. A
BpmnErroris a business outcome the diagram should show, like no bureau record routing to a reject path. Error boundary events catch it (lesson 04). Any other exception is a technical failure that triggers rollback and retry (lesson 05). Mixing these up is the single most common Flowable design bug.
Use It
All three styles side by side in
code/Delegates.java. The Spring-bean style you’ll use most:
@Component("bureauDelegate")
public static class SpringBureauDelegate implements JavaDelegate {
private final BureauGateway gateway;
public SpringBureauDelegate(BureauGateway gateway) {
this.gateway = gateway;
}
@Override
public void execute(DelegateExecution execution) {
execution.setVariable("score",
gateway.score((String) execution.getVariable("pan")));
}
}
This wires up as
<serviceTask id="bureauCall" flowable:delegateExpression="${bureauDelegate}"/>.
Here’s the business-vs-technical failure contract, in code:
if (score == -1) {
throw new BpmnError("NO_BUREAU_RECORD", "PAN has no bureau file"); // route on it
}
// bureau down? just let the IOException/RuntimeException fly -> rollback + retry
Testing is the quiet win of the expression style. ${bureauGateway.score(pan)} calls
a plain method, so your existing unit tests already cover it, and the process test
only has to assert the wiring.
Ship It
This lesson ships code/Delegates.java, a reference file
showing all three wirings with the failure contract annotated. Copy it into a Spring
Boot service alongside Phase 2’s starter.
Check Yourself
Q1. Your delegate needs a repository injected. Which wiring?
- A)
flowable:class— add a setter - B)
flowable:delegateExpression— the delegate is a Spring bean with normal injection - C)
flowable:expressionon a static method - D) any; injection always works
<details><summary>Answer</summary>B — flowable:class bypasses Spring entirely (the
engine calls new), so nothing is injected. Delegate-expression resolves a managed
bean.</details>
Q2. The bureau returns “no record exists for this PAN”. You should…
- A) throw
RuntimeExceptionso the job retries - B) return normally and hope a gateway checks
- C) throw
BpmnError("NO_BUREAU_RECORD")so the model routes it explicitly - D) log and continue
<details><summary>Answer</summary>C — “no record” is a business outcome, not a malfunction, and retrying won’t change it. It belongs on the diagram as an error boundary path.</details>
Q3. A delegate writes a variable, then throws a plain exception. The variable is…
- A) saved — writes are immediate
- B) rolled back with the rest of the transaction segment
- C) saved only if the task was async
- D) moved to history
<details><summary>Answer</summary>B — Phase 2’s rules apply: the delegate runs inside the segment’s transaction, and everything in it rolls back together.</details>
Challenge. Wrap an existing class you own with the expression style — no
JavaDelegate, no engine imports. Write down what you’d lose versus a delegate:
access to DelegateExecution, local variables, BpmnError codes. That trade-off is
the real decision between styles 2 and 3.
Related
- Next: The HTTP task
- Failure routing: BPMN errors vs technical errors
