Designing an Approval Workflow Engine in Laravel
How to design a configurable multi-level approval workflow in Laravel: data model, approval steps, delegation, audit trail and notifications.
Almost every business application eventually needs approvals: a purchase request, a discount, a leave application, a budget change. The first one is usually hard-coded as a status column and an if. By the fifth, the code is a maze. A small, generic approval engine pays for itself quickly.
Separate the rule from the request
The key idea is two layers. A workflow definition describes who must approve what, in which order. An approval instance is one document moving through that definition. Definitions are configuration that an administrator can change; instances are history that must never change.
A minimal data model
workflows id, name, document_type, is_active
workflow_steps id, workflow_id, sequence, approver_type, approver_id, condition
approvals id, workflow_id, approvable_type, approvable_id, status, current_step
approval_actions id, approval_id, step_id, user_id, action, comment, acted_atapprovable_type and approvable_id are a Laravel polymorphic relation, so the same engine serves purchase requests, discounts and anything added later. approver_type lets a step point to a specific user, a role, or a relationship such as the requester's line manager.
Conditions decide the path
Real approval chains depend on the document: amounts above a limit need one more signature. Store a simple condition on the step, such as a minimum amount, and skip steps whose condition is not met. Resist building a full expression language until you have a real need for it.
Make every action a transaction
Approving a step does three things: records the action, moves the instance to the next step and, on the last step, updates the document. Wrap them in one DB::transaction and lock the approval row with lockForUpdate() so two people cannot approve the same step at once.
The details users ask for
- Delegation: an approver on leave hands over to someone else for a date range.
- Send back: return the document to the requester for correction instead of rejecting it.
- Escalation: a reminder after a set time, then a notice to the next level.
- A visible trail: who approved, when, and what they wrote.
Notifications belong in the queue
Fire an event when an approval moves and let queued listeners send the email or push notification. The approval itself must never fail because a mail server was slow. Use afterCommit so the notification is only sent once the transaction has really been saved. More on that in Laravel queues for business automation.
Keep the audit trail append-only
Never update or delete rows in approval_actions. If a decision is reversed, add a new row that says so. When an auditor asks who approved a discount eight months ago, the answer should be one query away.
Common questions
How do you design a multi-level approval system?
Separate workflow definitions (steps, approvers, conditions) from approval instances (one document's progress). Store each decision as an append-only action row and move the instance step by step inside a database transaction.
How can one approval engine handle different document types in Laravel?
Use a polymorphic relation on the approval record, so purchase requests, discounts and other models can all be attached to the same approval tables.