Approval Workflow
1. Context
Section titled “1. Context”A persistent record — an invoice, a request, a document — needs a bounded set of human-in-the-loop decisions, typically approve or reject, each of which must record who decided and when, and each of which is a terminal or near-terminal transition.
2. Problem
Section titled “2. Problem”How do you model a decision-driven lifecycle where each transition is triggered by a specific actor’s decision, and that actor and decision must be captured as part of the transition itself, not just the resulting status?
3. Forces
Section titled “3. Forces”- Consistency — the recorded decision (approver, timestamp, remark or reason) should be updated in the same transition handler as the state change it causes.
- Coupling — the FSM needs to resolve the deciding actor without owning that actor’s entity itself.
- Reuse — approve/reject is a recurring shape across many domains (invoices, requests, orders) and should follow the same modeling structure each time.
- Complexity — some transitions are effectively terminal (
Approved,Rejected) and need no further events.
4. Solution
Section titled “4. Solution”Apply Entity-Backed FSM with events shaped
as decisions: event approve in(req:ApproveReq) out(...) and
event reject in(req:RejectReq) out(...). Inside each transition, resolve
the deciding actor, stamp the decision fields on this, and move to the
terminal state with next.
5. Structure
Section titled “5. Structure”stateDiagram-v2 [*] --> PendingApproval PendingApproval --> Approved: approve (records approver, approvedAt) PendingApproval --> Rejected: reject (records rejecter, rejectedAt) Approved --> [*] Rejected --> [*]6. DSL Sketch
Section titled “6. DSL Sketch”@fsm
InvoiceApprovalFSM controls Invoice.status key: id
event approve in(appReq:ApproveInvoiceReq) out(appInvoice:*Invoice) event reject in(rejReq:*RejectInvoiceReq) out(rejInvoice:Invoice)
state PendingApproval on event approve: this.remark = appReq.remark this.approvedAt = nowUTC() approver = User.findById(appReq.approvedBy) this.approvedBy = approver next Approved
on event reject: this.remark = rejReq.reason this.rejectedAt = nowUTC() rejector = User.findById(rejReq.rejectedBy) this.rejectedBy = rejector next Rejected
state Approved state RejectedThis is a minimal illustrative fragment, not a complete duplicated example — see Working Examples for the full, runnable model.
7. Consequences
Section titled “7. Consequences”Benefits
Section titled “Benefits”- Decision data and the state change are defined together in one transition handler, making the required actor and audit fields explicit.
- The shape generalizes across domains: the same approve/reject structure fits invoices, requests, or any decision-gated record.
Trade-offs
Section titled “Trade-offs”- This pattern models exactly one decision per transition. Multi-step or
multi-approver workflows (for example, two required approvals) need
additional states and events, not a bigger
approveevent. - Resolving the deciding actor (
User.findById) introduces a read dependency on another entity during the transition. - Transaction-level atomicity depends on the active FSM, persistence engine, and generator contract; this pattern does not establish that guarantee by itself.
8. Alternatives
Section titled “8. Alternatives”- If no human decision or actor needs to be recorded — the transition is purely system- or event-driven — use plain Entity-Backed FSM instead.
- If the record being decided on has no persisted identity, this pattern does not apply; see In-Memory FSM.
9. Related DSL
Section titled “9. Related DSL”- FSM DSL Reference — events, states, and transitions.
- Database DSL Reference — the entity whose status the FSM controls, and the actor entity it resolves.
- Service DSL Reference — connecting approve/reject API operations to FSM events.
- Expression DSL Reference — supporting logic invoked around a decision.
10. Working Examples
Section titled “10. Working Examples”- Invoice Approval Workflow — an entity-based approval workflow connecting persistent invoices, users, customers, and recorded decisions.
11. Related Patterns
Section titled “11. Related Patterns”- Entity-Backed FSM — the general pattern this one specializes.
- Database-Backed CRUD — the persistence and API surface an approval workflow typically sits alongside.