Learn Enterprise Cpq Oms Glassfish Camunda8 Part 041 Bpmn Model For Quote Approval
title: Build From Scratch: Enterprise Java Microservices CPQ & Order Management Platform - Part 041 description: Designing a production-grade Camunda 8 BPMN model for quote approval, including approval case boundaries, user tasks, timers, escalation, policy evidence, rejection/revision flow, idempotent workers, PostgreSQL synchronization, Kafka events, and operational recovery. series: learn-enterprise-cpq-oms-glassfish-camunda8 seriesTitle: Build From Scratch: Enterprise Java Microservices CPQ & Order Management Platform order: 41 partTitle: BPMN Model for Quote Approval tags:
- java
- microservices
- cpq
- quote
- approval
- camunda-8
- zeebe
- bpmn
- workflow
- postgresql
- mybatis
- kafka
- enterprise-architecture date: 2026-07-02
Part 041 — BPMN Model for Quote Approval
Di part sebelumnya kita memosisikan Camunda 8 sebagai process orchestration engine, bukan database bisnis.
Sekarang kita mulai memakai BPMN secara konkret untuk salah satu proses paling sensitif di CPQ:
quote approval.
Quote approval terlihat sederhana:
- sales submit quote,
- manager approve,
- quote boleh diterima customer.
Namun di enterprise system, approval bukan sekadar tombol Approve.
Approval bisa dipicu oleh:
- discount terlalu besar,
- manual price override,
- margin rendah,
- non-standard contract term,
- special product combination,
- customer risk,
- strategic account exception,
- expired price list,
- unsupported fulfillment dependency,
- regulatory/compliance constraint,
- sales hierarchy,
- delegated approver,
- SLA escalation,
- multi-level approval,
- parallel approval,
- approval invalidated by quote revision.
Jika model approval dibuat asal-asalan, sistem akan punya gejala klasik:
- quote bisa approved padahal item sudah berubah,
- approver menyetujui price yang bukan lagi price current,
- audit tidak bisa menjawab “siapa menyetujui apa, kapan, dan berdasarkan evidence apa”,
- process Camunda terlihat selesai tetapi quote state di database belum berubah,
- retry worker membuat double approval event,
- escalation membuat approval task ganda,
- user task stuck tanpa pemilik bisnis yang jelas.
Bagian ini membangun BPMN quote approval yang production-grade.
1. Mental Model
Quote approval adalah proses manusia + policy + evidence.
Camunda cocok untuk mengatur:
- urutan approval,
- user task,
- timer,
- escalation,
- reject/revise path,
- timeout,
- reminder,
- process visibility,
- operational incident.
Namun Camunda tidak boleh menjadi pemilik utama approval truth.
Aturan utama:
Approval truth lives in PostgreSQL. BPMN coordinates approval work.
Artinya:
- approval case disimpan di database CPQ,
- approval step disimpan di database CPQ,
- evidence snapshot disimpan di database CPQ,
- quote state disimpan di database CPQ,
- Camunda process instance menyimpan pointer dan process variables minimal,
- user task completion harus memanggil domain command yang idempotent,
- perubahan final quote harus dilakukan oleh domain service, bukan langsung dari BPMN variable.
2. Why Quote Approval Needs BPMN
Quote approval bisa saja dibuat tanpa Camunda:
quote_submitted -> approval_required -> manager_clicks_approve -> quote_approved
Ini cukup untuk aplikasi kecil.
Namun enterprise CPQ membutuhkan orchestration karena approval sering:
- long-running,
- melibatkan banyak role,
- punya SLA,
- butuh reminder,
- butuh escalation,
- bisa parallel,
- bisa delegated,
- bisa invalidated,
- harus visible bagi operations,
- harus bisa dipulihkan jika integration/task gagal.
BPMN memberi bahasa eksplisit untuk proses semacam ini.
Tetapi BPMN juga membawa risiko:
Semakin banyak business decision dipindahkan ke diagram, semakin sulit domain model dikontrol, diuji, dan diaudit.
Jadi desain kita harus memisahkan:
| Concern | Owner |
|---|---|
| Policy evaluation | CPQ domain/application service |
| Approval evidence | PostgreSQL |
| Human task routing | Camunda BPMN + Tasklist/task app |
| Timer/escalation | Camunda BPMN |
| Quote canonical state | Quote domain service |
| Approval case canonical state | Approval domain service |
| Process progress | Camunda process instance |
| Audit trail | CPQ audit tables + outbox events |
| Operational incident | Camunda + CPQ fallout/ops view |
3. Approval Domain Objects Recap
Sebelum BPMN dibuat, kita sudah harus punya domain objects berikut.
Quote
ApprovalCase
ApprovalStep
ApprovalDecision
ApprovalPolicyEvaluation
ApprovalEvidenceSnapshot
ApprovalAssignment
ApprovalEscalation
Minimal aggregate-nya:
public final class ApprovalCase {
private ApprovalCaseId id;
private TenantId tenantId;
private QuoteId quoteId;
private int quoteRevision;
private ApprovalCaseStatus status;
private ApprovalPolicySnapshot policySnapshot;
private List<ApprovalStep> steps;
private Instant createdAt;
private Instant completedAt;
private long version;
}
Status approval case:
DRAFT
OPEN
WAITING_FOR_APPROVER
APPROVED
REJECTED
REVISION_REQUESTED
INVALIDATED
CANCELLED
EXPIRED
FALLOUT
Status approval step:
PENDING
ASSIGNED
IN_PROGRESS
APPROVED
REJECTED
REVISION_REQUESTED
ESCALATED
SKIPPED
EXPIRED
CANCELLED
Approval decision harus membawa evidence:
public record ApprovalDecisionCommand(
TenantId tenantId,
ApprovalCaseId approvalCaseId,
ApprovalStepId approvalStepId,
QuoteId quoteId,
int quoteRevision,
UserId approverId,
ApprovalDecisionType decision,
String reason,
String idempotencyKey,
Instant decidedAt
) {}
Kenapa quoteRevision ikut command?
Karena approval terhadap quote revision lama tidak boleh mempengaruhi quote revision baru.
4. BPMN Process Scope
Process name:
quote-approval-v1
Process start condition:
Quote submitted and policy evaluation says approval is required.
Process end conditions:
APPROVED
REJECTED
REVISION_REQUESTED
CANCELLED
INVALIDATED
EXPIRED
FALLOUT
Process tidak boleh dipakai untuk:
- menghitung price,
- menghitung discount,
- menentukan margin,
- menyimpan quote item,
- mengubah quote line secara langsung,
- menulis approval evidence tanpa domain command,
- menentukan policy detail melalui gateway yang terlalu banyak.
BPMN harus bertanya ke domain service:
What is the approval plan?
Who should approve?
What is the current step?
Is the approval case still valid?
What should happen after this decision?
5. Main BPMN Flow
BPMN utama dapat dimodelkan seperti ini.
BPMN diagram di atas bukan berarti semua logic ada dalam gateway.
Gateway hanya membaca result dari worker/domain command.
Contoh variable minimal:
{
"tenantId": "tenant-a",
"quoteId": "q-10001",
"quoteRevision": 4,
"approvalCaseId": "ac-90001",
"approvalMode": "SEQUENTIAL",
"currentStepId": "acs-1",
"caseValid": true,
"lastDecision": "APPROVED",
"moreSteps": false
}
Variable tidak boleh berisi seluruh quote payload, seluruh price breakdown, atau seluruh product configuration.
6. Process Variables Policy
Process variables harus kecil, stabil, dan aman untuk migration.
Gunakan variables berikut:
| Variable | Meaning |
|---|---|
tenantId | tenant boundary |
quoteId | quote pointer |
quoteRevision | revision approved by process |
approvalCaseId | approval case pointer |
approvalMode | sequential/parallel/quorum |
currentStepId | active step pointer |
candidateGroups | group hint for user task |
candidateUsers | explicit user hint if any |
dueAt | due date hint |
lastDecision | latest normalized decision |
moreSteps | route to next approval step |
caseValid | guard result from domain service |
falloutReason | operational reason if process cannot continue |
Jangan simpan:
- quote item list,
- configuration tree,
- pricing breakdown lengkap,
- approver hierarchy snapshot lengkap,
- discount calculation detail,
- customer private data yang tidak perlu,
- payload external system besar,
- database row version yang tidak dipakai routing.
Kenapa?
Karena variable Camunda adalah process state, bukan data warehouse.
Jika variable terlalu besar:
- process migration lebih sulit,
- debugging lebih bising,
- PII exposure lebih besar,
- process replay lebih berat,
- schema evolution lebih menyakitkan.
7. Starting the Approval Process Safely
Jangan start Camunda process sebelum transaction quote submission commit.
Anti-pattern:
BEGIN
update quote status = SUBMITTED
create approval case
start Camunda process
COMMIT
Jika start Camunda process sukses tetapi database commit gagal, Camunda punya process instance untuk approval case yang tidak ada.
Jika database commit sukses tetapi network timeout saat start process, caller tidak tahu apakah process sudah dibuat.
Pattern yang lebih aman:
BEGIN
validate quote
create approval case
update quote status = APPROVAL_PENDING
insert workflow_start_request
insert outbox event QuoteApprovalRequested
COMMIT
WorkflowStartRelay:
load pending workflow_start_request
start Camunda process idempotently
store processInstanceKey
mark request STARTED
Tabel:
create table workflow_start_request (
id uuid primary key,
tenant_id varchar(64) not null,
process_id varchar(128) not null,
business_key varchar(256) not null,
aggregate_type varchar(64) not null,
aggregate_id uuid not null,
payload jsonb not null,
status varchar(32) not null,
process_instance_key varchar(128),
failure_reason text,
created_at timestamptz not null,
started_at timestamptz,
unique (tenant_id, process_id, business_key)
);
Business key:
tenantId:quoteId:quoteRevision:approvalCaseId
Tujuannya:
- start request durable,
- retry aman,
- duplicate start dicegah,
- process instance bisa dikaitkan kembali ke aggregate.
8. Approval Case Loading Worker
Service task pertama:
load-approval-case
Tugas worker:
- baca
tenantId,quoteId,quoteRevision,approvalCaseId, - load approval case dari PostgreSQL,
- cek case masih valid,
- cek quote revision cocok,
- tentukan current step,
- return routing variables minimal.
Pseudo Java:
public final class LoadApprovalCaseWorker {
private final ApprovalApplicationService approvalService;
public Map<String, Object> handle(JobContext job) {
var tenantId = TenantId.of(job.stringVar("tenantId"));
var caseId = ApprovalCaseId.of(job.stringVar("approvalCaseId"));
var quoteId = QuoteId.of(job.stringVar("quoteId"));
var quoteRevision = job.intVar("quoteRevision");
var result = approvalService.loadForWorkflow(
new LoadApprovalCaseForWorkflowCommand(
tenantId,
caseId,
quoteId,
quoteRevision,
job.idempotencyKey()
)
);
return Map.of(
"caseValid", result.caseValid(),
"approvalMode", result.mode().name(),
"currentStepId", result.currentStepId().value(),
"candidateGroups", result.candidateGroups(),
"candidateUsers", result.candidateUsers(),
"dueAt", result.dueAt().toString(),
"moreSteps", result.moreSteps()
);
}
}
Worker ini tidak approve quote.
Worker hanya menyiapkan process untuk user task berikutnya.
9. Human Approval User Task
Camunda user task cocok untuk pekerjaan manusia yang dibantu workflow engine.
Dalam quote approval, user task merepresentasikan:
A human approver must review evidence and submit a decision.
User task metadata:
name: Review Quote Approval
action: approve / reject / request revision
candidateGroups: from approval step assignment
candidateUsers: from explicit delegation if any
dueDate: step due date
form: approval decision form
Data yang harus ditampilkan ke approver sebaiknya berasal dari CPQ API, bukan dari Camunda variable besar.
Task form/app membaca:
GET /api/v1/approval-cases/{approvalCaseId}
GET /api/v1/quotes/{quoteId}?revision=4
GET /api/v1/approval-cases/{approvalCaseId}/evidence
Lalu saat approver submit:
POST /api/v1/approval-cases/{approvalCaseId}/steps/{stepId}/decisions
Idempotency-Key: approve-ac-90001-step-1-user-123-001
If-Match: "case-version-12"
Body:
{
"decision": "APPROVE",
"reason": "Discount exception is acceptable for strategic renewal.",
"quoteId": "q-10001",
"quoteRevision": 4
}
Ada dua pilihan integration dengan Camunda:
Option A — Tasklist/Form completes user task directly
Tasklist completion membawa variable:
{
"lastDecision": "APPROVED",
"approvalComment": "..."
}
Lalu service task setelah user task memanggil domain service untuk apply decision.
Kelebihan:
- simple,
- natural untuk Camunda Tasklist.
Kelemahan:
- harus hati-hati agar decision tidak dianggap final sebelum domain command sukses.
Option B — Business API applies decision, then completes Camunda task
Approval UI memanggil CPQ API. CPQ API:
- validate approval case,
- persist decision,
- audit,
- outbox,
- complete Camunda task atau publish message.
Kelebihan:
- domain truth update terjadi lebih dulu,
- API bisa enforce authorization lebih kuat,
- audit lebih natural.
Kelemahan:
- integration dengan Tasklist lebih kompleks,
- harus mengelola task completion idempotency.
Untuk enterprise CPQ, saya lebih memilih Option B untuk custom approval UI, dan Option A hanya jika memakai Tasklist secara disiplin.
10. Decision Application Worker
Setelah user task selesai, process harus menjalankan worker:
apply-approval-decision
Worker ini tidak percaya begitu saja pada process variable.
Ia harus:
- load approval case,
- load current step,
- validate quote revision,
- validate approver identity,
- validate decision belum diterapkan,
- apply domain transition,
- write audit,
- write outbox event,
- return normalized routing result.
Pseudo Java:
public Map<String, Object> applyDecision(JobContext job) {
var command = new ApplyApprovalDecisionFromWorkflowCommand(
TenantId.of(job.stringVar("tenantId")),
ApprovalCaseId.of(job.stringVar("approvalCaseId")),
ApprovalStepId.of(job.stringVar("currentStepId")),
QuoteId.of(job.stringVar("quoteId")),
job.intVar("quoteRevision"),
ApprovalDecisionType.of(job.stringVar("lastDecision")),
job.optionalStringVar("approvalReason"),
job.idempotencyKey()
);
var result = approvalService.applyDecision(command);
return Map.of(
"lastDecision", result.normalizedDecision().name(),
"moreSteps", result.moreSteps(),
"nextStepId", result.nextStepId().map(ApprovalStepId::value).orElse(null),
"caseStatus", result.caseStatus().name()
);
}
Domain service harus idempotent.
Jika worker retry karena network failure setelah database commit, command kedua harus mengembalikan result yang sama, bukan membuat decision kedua.
11. Sequential Approval
Sequential approval digunakan ketika approval level harus berurutan.
Contoh:
Level 1: Sales Manager
Level 2: Finance Manager
Level 3: Legal
Flow:
Sequential approval invariant:
Step N+1 cannot be opened until Step N reaches APPROVED or SKIPPED by policy.
Database guard:
create unique index uq_open_approval_step_per_case
on approval_step (tenant_id, approval_case_id)
where status in ('PENDING', 'ASSIGNED', 'IN_PROGRESS');
Atau enforce di application service dengan explicit state transition lock:
select * from approval_case
where tenant_id = #{tenantId}
and id = #{approvalCaseId}
for update;
12. Parallel Approval
Parallel approval digunakan ketika beberapa pihak dapat review bersamaan.
Contoh:
- Finance approve discount,
- Legal approve non-standard term,
- Product approve unsupported bundle,
- Delivery approve fulfillment exception.
Mode parallel bisa punya semantics:
| Mode | Meaning |
|---|---|
ALL_OF | semua approval wajib approve |
ANY_OF | satu approval cukup |
QUORUM | minimal N dari M approve |
VETO | satu reject langsung reject |
ADVISORY | decision dicatat tapi tidak block |
Jangan encode semantics ini sepenuhnya di gateway BPMN.
BPMN cukup menjalankan parallel user tasks, lalu worker/domain service mengevaluasi aggregate decision.
Dalam Camunda, parallel multi-instance user task dapat dipakai jika jumlah task berasal dari collection approver steps.
Namun domain tetap harus menjadi pemilik semantics ALL_OF, ANY_OF, QUORUM, dan VETO.
13. Timer, Reminder, and Escalation
Approval tanpa SLA akan menjadi limbo.
Model minimal:
- due date per approval step,
- reminder before due,
- escalation after due,
- expiry after hard deadline.
BPMN pattern:
Prinsip:
- reminder tidak mengubah business decision,
- escalation mengubah assignment/evidence,
- expiry mengubah approval case status,
- semua perubahan harus masuk audit,
- due date harus berasal dari approval policy snapshot, bukan hardcoded BPMN.
Escalation command:
public record EscalateApprovalStepCommand(
TenantId tenantId,
ApprovalCaseId approvalCaseId,
ApprovalStepId stepId,
EscalationReason reason,
Instant escalatedAt,
String idempotencyKey
) {}
Escalation worker:
apply-approval-escalation
Worker ini:
- lock approval case,
- cek step masih open,
- record escalation,
- update candidate group/user,
- publish
ApprovalStepEscalated, - return updated assignment variables.
14. Revision Invalidation
Quote approval harus invalid jika quote berubah materially.
Contoh perubahan material:
- item ditambah,
- product config berubah,
- price berubah,
- discount berubah,
- customer/account berubah,
- contract term berubah,
- validity date berubah,
- fulfillment feasibility berubah.
Jika quote revision berubah saat approval masih berjalan, approval process harus berhenti atau diarahkan ke invalidated path.
Pattern:
- Quote service menerima command revise quote.
- Quote service menandai existing approval case
INVALIDATED. - Quote service publish event
ApprovalCaseInvalidated. - Workflow correlation message dikirim ke Camunda process.
- BPMN menangkap message dan end dengan status invalidated.
BPMN message name:
approval-case-invalidated
Correlation key:
tenantId:approvalCaseId
15. Rejection and Revision Request
Rejection means:
This quote must not proceed as submitted.
Revision request means:
This quote may proceed if changed and resubmitted.
Jangan samakan keduanya.
Domain effects:
| Decision | Approval Case | Quote |
|---|---|---|
| Approve final step | APPROVED | APPROVED or READY_FOR_ACCEPTANCE |
| Reject | REJECTED | REJECTED |
| Request revision | REVISION_REQUESTED | REVISION_REQUIRED |
| Expire | EXPIRED | APPROVAL_EXPIRED |
| Invalidate | INVALIDATED | remains revised/new draft |
Quote state transition harus dilakukan domain service.
BPMN hanya mengarahkan worker:
finalize-quote-approval
Worker finalization:
- load approval case,
- compute final result,
- transition approval case,
- transition quote,
- insert audit,
- insert outbox events,
- update workflow reference.
16. BPMN Error vs Technical Failure
Jangan semua kegagalan dilempar sebagai incident.
Bedakan:
| Condition | Handling |
|---|---|
| database temporarily unavailable | fail job, retry |
| downstream notification timeout | retry or async outbox |
| approval case not found | BPMN error/fallout depending cause |
| quote revision mismatch | business invalidation path |
| unauthorized user decision | reject API command before Camunda completion |
| duplicate decision | idempotent success or conflict |
| policy configuration broken | fallout/manual repair |
| serialization bug | technical incident |
Camunda incident cocok untuk technical stuck condition.
Business fallout harus punya case di CPQ operational model.
Jangan membuat approver menunggu karena worker incident yang sebenarnya data policy broken tanpa operational queue.
17. PostgreSQL Tables for Workflow Synchronization
Minimal tambahan tabel:
create table approval_workflow_ref (
tenant_id varchar(64) not null,
approval_case_id uuid not null,
quote_id uuid not null,
quote_revision int not null,
process_id varchar(128) not null,
process_instance_key varchar(128) not null,
status varchar(32) not null,
started_at timestamptz not null,
ended_at timestamptz,
last_seen_element_id varchar(128),
last_error text,
version bigint not null default 0,
primary key (tenant_id, approval_case_id)
);
Decision table:
create table approval_decision (
id uuid primary key,
tenant_id varchar(64) not null,
approval_case_id uuid not null,
approval_step_id uuid not null,
quote_id uuid not null,
quote_revision int not null,
approver_id varchar(128) not null,
decision varchar(32) not null,
reason text,
evidence_hash varchar(128) not null,
idempotency_key varchar(160) not null,
decided_at timestamptz not null,
unique (tenant_id, approval_step_id, approver_id, idempotency_key)
);
Escalation table:
create table approval_escalation (
id uuid primary key,
tenant_id varchar(64) not null,
approval_case_id uuid not null,
approval_step_id uuid not null,
from_assignee varchar(128),
to_assignee varchar(128),
to_group varchar(128),
reason varchar(64) not null,
escalated_at timestamptz not null,
idempotency_key varchar(160) not null,
unique (tenant_id, approval_step_id, idempotency_key)
);
18. Kafka Events
Events yang relevan:
QuoteApprovalRequested
ApprovalCaseOpened
ApprovalStepAssigned
ApprovalStepDecisionRecorded
ApprovalStepEscalated
ApprovalCaseApproved
ApprovalCaseRejected
ApprovalCaseRevisionRequested
ApprovalCaseInvalidated
QuoteApproved
QuoteRejected
QuoteRevisionRequired
Event bukan command UI.
Event tidak boleh menjadi satu-satunya sumber truth approval.
Event payload minimal:
{
"eventId": "evt-001",
"eventType": "ApprovalCaseApproved",
"eventVersion": 1,
"occurredAt": "2026-07-02T05:00:00Z",
"tenantId": "tenant-a",
"correlationId": "corr-123",
"causationId": "cmd-456",
"aggregateType": "ApprovalCase",
"aggregateId": "ac-90001",
"payload": {
"quoteId": "q-10001",
"quoteRevision": 4,
"approvalCaseId": "ac-90001",
"result": "APPROVED"
}
}
Outbox publish harus satu transaction dengan approval state change.
19. API Shape
Business API:
POST /api/v1/quotes/{quoteId}/submit
POST /api/v1/approval-cases/{approvalCaseId}/steps/{stepId}/decisions
POST /api/v1/approval-cases/{approvalCaseId}/steps/{stepId}/escalations
POST /api/v1/approval-cases/{approvalCaseId}/cancel
GET /api/v1/approval-cases/{approvalCaseId}
GET /api/v1/approval-cases?status=OPEN&assignee=me
GET /api/v1/approval-cases/{approvalCaseId}/evidence
GET /api/v1/approval-cases/{approvalCaseId}/timeline
Internal workflow API:
POST /internal/workflows/approval-cases/{approvalCaseId}/load
POST /internal/workflows/approval-cases/{approvalCaseId}/apply-decision
POST /internal/workflows/approval-cases/{approvalCaseId}/finalize
POST /internal/workflows/approval-cases/{approvalCaseId}/escalate
POST /internal/workflows/approval-cases/{approvalCaseId}/mark-fallout
Jangan expose Camunda process instance key ke public API sebagai primary business identifier.
Public user berpikir dalam quoteId dan approvalCaseId, bukan processInstanceKey.
20. Approval Evidence Model
Approval evidence harus menjawab:
Apa yang dilihat approver saat mengambil keputusan?
Minimal evidence:
- quote header snapshot,
- quote revision,
- customer/account summary,
- item summary,
- price breakdown summary,
- discount/margin signal,
- policy triggers,
- prior approval decisions,
- exception reason,
- validity period,
- requester identity,
- generated-at timestamp,
- hash.
Evidence snapshot tidak harus menyimpan seluruh quote detail dua kali.
Namun harus cukup untuk audit.
Pattern:
approval_evidence_snapshot
- canonical JSON evidence payload
- evidence_hash
- quote_snapshot_hash
- price_snapshot_hash
- configuration_snapshot_hash
Saat decision dibuat:
decision.evidence_hash = currently_visible_evidence_hash
Jika evidence berubah sebelum approval, approver harus melihat versi terbaru atau decision ditolak karena stale.
21. Authorization Rules
Approval authorization bukan hanya “punya role APPROVER”.
Harus mempertimbangkan:
- tenant,
- assignment,
- delegation,
- candidate group,
- approval level,
- requester cannot approve own exception if policy forbids,
- approver limit,
- quote amount,
- discount threshold,
- region/business unit,
- conflict of interest,
- emergency override.
Authorization check ada di approval domain/application service.
Camunda candidate group hanya membantu routing task.
Jangan menganggap candidate group di BPMN sebagai authorization final.
22. Observability
Log fields minimal:
tenantId
correlationId
quoteId
quoteRevision
approvalCaseId
approvalStepId
processInstanceKey
jobType
decision
approverId
idempotencyKey
stateBefore
stateAfter
durationMs
Metrics:
approval_case_opened_total
approval_case_approved_total
approval_case_rejected_total
approval_case_revision_requested_total
approval_step_decision_duration_seconds
approval_step_escalated_total
approval_case_invalidated_total
approval_worker_retry_total
approval_worker_incident_total
approval_case_sla_breached_total
Operational views:
- open approvals by age,
- approvals near SLA breach,
- approvals by approver group,
- stuck workflow refs,
- cases invalidated by revision,
- rejected quote reasons,
- approval fallout queue.
23. Testing Strategy
Unit Tests
Test approval domain transitions:
- pending to approved,
- pending to rejected,
- pending to revision requested,
- sequential next step,
- parallel all-of,
- veto reject,
- stale quote revision,
- duplicate decision,
- unauthorized approver,
- escalation after due.
Worker Tests
Test each worker with fake job variables:
- load approval case,
- apply decision,
- finalize approval,
- escalate,
- mark fallout.
BPMN Tests
Test process path:
- simple approval approved,
- rejected path,
- revision requested path,
- sequential multi-level approval,
- parallel approval,
- timer reminder,
- escalation,
- invalidation message,
- worker failure retry,
- incident creation.
Integration Tests
Test full flow:
create quote
configure item
price quote
submit quote
approval case created
workflow started
approver sees task
approver approves
quote moves to APPROVED
outbox events emitted
Golden Evidence Tests
Approval evidence JSON must be stable.
Store golden payloads for:
- discount approval,
- manual override approval,
- legal term approval,
- multi-level approval,
- revision invalidation.
24. Anti-Patterns
Anti-Pattern 1: Whole Quote in Process Variables
This makes process state huge and stale.
Use IDs and hashes.
Anti-Pattern 2: Approval Policy in BPMN Gateways
Do not model every discount threshold as BPMN gateway.
Policy belongs to domain/rule evaluator.
Anti-Pattern 3: User Task Completion Equals Business Approval
Completing a task is not enough.
Business approval must be persisted through domain command.
Anti-Pattern 4: Approval Without Evidence Hash
If you cannot prove what was approved, you do not have defensible approval.
Anti-Pattern 5: Camunda Incident as Business Queue
Incident is technical/process stuck signal.
Business fallout needs CPQ operational case.
Anti-Pattern 6: Approver Authorization Only in UI
UI checks are convenience.
Domain service must enforce.
Anti-Pattern 7: Non-Idempotent Decision Worker
Worker retry must never create duplicate approval decision.
25. Implementation Milestone
Build in this order:
- approval case schema,
- approval step schema,
- approval evidence snapshot,
- approval domain transition unit tests,
- quote submit command creates approval case,
- workflow start request table,
- approval process BPMN skeleton,
- load approval case worker,
- simple user task approval,
- apply decision worker,
- finalize approval worker,
- rejection path,
- revision requested path,
- timer reminder,
- escalation,
- invalidation message,
- parallel approval,
- operational views,
- audit explorer,
- incident/fallout recovery path.
Do not start from BPMN diagram alone.
Start from domain invariants, then let BPMN orchestrate them.
26. Final Mental Model
Quote approval is not a workflow checkbox.
It is a controlled transition from:
commercial proposal with exception
to:
commercial proposal approved with evidence
Camunda helps coordinate people, time, escalation, and long-running progress.
PostgreSQL/domain service preserves truth.
Kafka communicates outcome.
Audit proves what happened.
Redis may accelerate lookup but must not own approval state.
The key invariant:
A quote is not approved because a BPMN task completed. A quote is approved because the approval domain accepted a valid decision against a valid quote revision and persisted the transition atomically with audit and events.
References
- Camunda 8 service tasks create jobs and wait for workers to complete them: https://docs.camunda.io/docs/components/modeler/bpmn/service-tasks/
- Camunda 8 user tasks model human work assisted by workflow tooling: https://docs.camunda.io/docs/components/modeler/bpmn/user-tasks/
- Camunda 8 Tasklist helps orchestrate human workflows and assigned work: https://docs.camunda.io/docs/components/tasklist/introduction-to-tasklist/
- Camunda 8 message events wait for a referenced message from an external system or process: https://docs.camunda.io/docs/components/modeler/bpmn/message-events/
- Camunda 8 timer events support timer start, intermediate catch, and boundary timer behavior: https://docs.camunda.io/docs/components/modeler/bpmn/timer-events/
You just completed lesson 41 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.