Skip to content

Distributed Workflow Engine

Difficulty: Advanced | Time: 3+ hours | Integration: State Machines, Commands, Events, Broadcasting, CQRS

Implement a sophisticated workflow system with state transitions, conditional branching, and distributed processing using specification-driven business rules with real-time updates and event sourcing.

The Challenge: Complex Workflow Management

Your enterprise application needs to handle complex, multi-step business processes (document approvals, order fulfillment, employee onboarding) with conditional logic, parallel processing, and real-time status updates across distributed teams.

🔍 The Problem: Monolithic Workflow Logic

Here's the nightmarish workflow code that has become unmaintainable:

php
<?php

namespace App\Services;

use App\Models\WorkflowInstance;
use App\Models\User;
use App\Models\Document;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;

class DocumentApprovalService
{
    public function processDocumentSubmission(Document $document): void
    {
        // Hardcoded workflow logic mixed with business rules
        $workflow = WorkflowInstance::create([
            'type' => 'document_approval',
            'subject_id' => $document->id,
            'subject_type' => Document::class,
            'status' => 'started',
            'current_step' => 'initial_review',
            'started_by' => auth()->id(),
        ]);
        
        // Step 1: Initial Review
        if ($document->department === 'hr') {
            // HR documents need compliance check first
            if ($this->needsComplianceReview($document)) {
                $workflow->update(['current_step' => 'compliance_review']);
                $this->assignToComplianceTeam($document);
            } else {
                $workflow->update(['current_step' => 'manager_review']);
                $this->assignToManager($document);
            }
        } elseif ($document->department === 'finance') {
            // Finance documents have different rules
            if ($document->amount > 10000) {
                $workflow->update(['current_step' => 'cfo_review']);
                $this->assignToCFO($document);
            } elseif ($document->amount > 1000) {
                $workflow->update(['current_step' => 'finance_manager_review']);
                $this->assignToFinanceManager($document);
            } else {
                $workflow->update(['current_step' => 'supervisor_review']);
                $this->assignToSupervisor($document);
            }
        } elseif ($document->department === 'legal') {
            // Legal documents always need senior review
            $workflow->update(['current_step' => 'senior_legal_review']);
            $this->assignToSeniorLegal($document);
        } else {
            // Default workflow for other departments
            $workflow->update(['current_step' => 'manager_review']);
            $this->assignToManager($document);
        }
        
        $this->sendNotifications($workflow, $document);
    }
    
    public function processApproval(WorkflowInstance $workflow, User $approver, string $action, ?string $comments = null): void
    {
        // More hardcoded state transitions
        $document = $workflow->subject;
        
        DB::transaction(function () use ($workflow, $approver, $action, $comments, $document) {
            // Record the action
            $workflow->actions()->create([
                'user_id' => $approver->id,
                'action' => $action,
                'comments' => $comments,
                'performed_at' => now(),
            ]);
            
            if ($action === 'approve') {
                // Determine next step based on current step and document type
                switch ($workflow->current_step) {
                    case 'compliance_review':
                        $workflow->update(['current_step' => 'manager_review']);
                        $this->assignToManager($document);
                        break;
                        
                    case 'manager_review':
                        if ($document->department === 'hr' && $document->type === 'policy_change') {
                            $workflow->update(['current_step' => 'senior_management_review']);
                            $this->assignToSeniorManagement($document);
                        } elseif ($document->department === 'finance' && $document->amount > 5000) {
                            $workflow->update(['current_step' => 'cfo_review']);
                            $this->assignToCFO($document);
                        } else {
                            $workflow->update(['current_step' => 'final_approval', 'status' => 'approved']);
                            $this->finalizeApproval($document);
                        }
                        break;
                        
                    case 'cfo_review':
                        if ($document->amount > 50000) {
                            $workflow->update(['current_step' => 'board_review']);
                            $this->assignToBoard($document);
                        } else {
                            $workflow->update(['current_step' => 'final_approval', 'status' => 'approved']);
                            $this->finalizeApproval($document);
                        }
                        break;
                        
                    case 'senior_legal_review':
                        if ($document->risk_level === 'high') {
                            $workflow->update(['current_step' => 'external_counsel_review']);
                            $this->assignToExternalCounsel($document);
                        } else {
                            $workflow->update(['current_step' => 'final_approval', 'status' => 'approved']);
                            $this->finalizeApproval($document);
                        }
                        break;
                        
                    case 'board_review':
                    case 'external_counsel_review':
                    case 'senior_management_review':
                        $workflow->update(['current_step' => 'final_approval', 'status' => 'approved']);
                        $this->finalizeApproval($document);
                        break;
                        
                    default:
                        throw new \Exception("Unknown workflow step: {$workflow->current_step}");
                }
            } elseif ($action === 'reject') {
                $workflow->update(['status' => 'rejected', 'current_step' => 'rejected']);
                $this->handleRejection($document, $comments);
            } elseif ($action === 'request_changes') {
                $workflow->update(['status' => 'changes_requested', 'current_step' => 'awaiting_changes']);
                $this->requestChanges($document, $comments);
            }
            
            $this->sendNotifications($workflow, $document);
        });
    }
    
    private function needsComplianceReview(Document $document): bool
    {
        // More hardcoded business logic
        return in_array($document->type, ['policy_change', 'employee_handbook', 'contract'])
            || $document->contains_personal_data
            || $document->regulatory_impact;
    }
    
    private function assignToComplianceTeam(Document $document): void
    {
        // Hardcoded assignment logic
        $complianceUsers = User::where('department', 'compliance')
            ->where('is_active', true)
            ->get();
            
        foreach ($complianceUsers as $user) {
            $user->notify(new DocumentReviewNotification($document, 'compliance_review'));
        }
    }
    
    // ... hundreds more lines of hardcoded workflow logic
}

🎯 Workflow Requirements Analysis

Let's identify the complex workflow patterns:

  1. Dynamic State Transitions: Next steps depend on document type, amount, department
  2. Conditional Branching: Different paths based on business rules
  3. Parallel Processing: Multiple reviewers, concurrent approvals
  4. Role-Based Assignment: Automatic task assignment based on roles/departments
  5. Real-Time Updates: Live status updates across distributed teams
  6. Audit Trail: Complete workflow history with event sourcing
  7. Escalation Logic: Time-based escalations and deadline management
  8. External Integrations: Third-party approvals and notifications
  9. Rollback/Retry: Handle failures and process corrections

Step 1: Generate Workflow Specifications

Using the Artisan command to create our workflow specifications:

bash
# Workflow state specifications
php artisan make:specification Workflow/WorkflowStateSpecification --model=WorkflowInstance
php artisan make:specification Workflow/TransitionEligibilitySpecification --model=WorkflowInstance
php artisan make:specification Workflow/StepCompletionSpecification --model=WorkflowInstance

# Business rule specifications for transitions
php artisan make:specification Workflow/ComplianceReviewRequiredSpecification --model=Document
php artisan make:specification Workflow/HighValueApprovalSpecification --model=Document
php artisan make:specification Workflow/SeniorReviewRequiredSpecification --model=Document
php artisan make:specification Workflow/ExternalApprovalRequiredSpecification --model=Document

# Assignment specifications
php artisan make:specification Workflow/ReviewerAssignmentSpecification --model=User
php artisan make:specification Workflow/EscalationRequiredSpecification --model=WorkflowInstance
php artisan make:specification Workflow/ParallelProcessingSpecification --model=WorkflowInstance

# Composite workflow specifications
php artisan make:specification Workflow/DocumentApprovalWorkflowSpecification --composite
php artisan make:specification Workflow/FinanceApprovalWorkflowSpecification --composite
php artisan make:specification Workflow/LegalReviewWorkflowSpecification --composite

Step 2: Core Workflow State Management

WorkflowStateSpecification

php
<?php

namespace App\Specifications\Workflow;

use App\Models\WorkflowInstance;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;

class WorkflowStateSpecification extends AbstractSpecification
{
    public function __construct(
        private readonly string|array $validStates,
        private readonly ?string $expectedPreviousState = null
    ) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof WorkflowInstance) {
            return false;
        }
        
        $validStates = is_array($this->validStates) ? $this->validStates : [$this->validStates];
        
        $currentStateValid = in_array($candidate->status, $validStates);
        
        if (!$currentStateValid) {
            return false;
        }
        
        // Check previous state if specified
        if ($this->expectedPreviousState) {
            $lastAction = $candidate->actions()->latest()->first();
            return $lastAction && $lastAction->previous_state === $this->expectedPreviousState;
        }
        
        return true;
    }
    
    public function toQuery(Builder $query): Builder
    {
        $validStates = is_array($this->validStates) ? $this->validStates : [$this->validStates];
        
        return $query->whereIn('status', $validStates);
    }
    
    public function canTransitionTo(WorkflowInstance $workflow, string $targetState): bool
    {
        $allowedTransitions = $this->getAllowedTransitions($workflow->status);
        
        return in_array($targetState, $allowedTransitions);
    }
    
    private function getAllowedTransitions(string $currentState): array
    {
        return match($currentState) {
            'pending' => ['in_progress', 'cancelled'],
            'in_progress' => ['completed', 'paused', 'rejected', 'escalated'],
            'paused' => ['in_progress', 'cancelled'],
            'escalated' => ['in_progress', 'completed', 'rejected'],
            'completed' => ['archived'],
            'rejected' => ['pending', 'archived'], // Allow restart
            'cancelled' => ['archived'],
            'archived' => [], // Terminal state
            default => []
        };
    }
}

TransitionEligibilitySpecification

php
<?php

namespace App\Specifications\Workflow;

use App\Models\WorkflowInstance;
use App\Models\User;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;

class TransitionEligibilitySpecification extends AbstractSpecification
{
    public function __construct(
        private readonly string $targetState,
        private readonly User $actor,
        private readonly ?array $requiredRoles = null,
        private readonly ?array $requiredPermissions = null
    ) {}
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof WorkflowInstance) {
            return false;
        }
        
        return $this->hasValidState($candidate) &&
               $this->hasRequiredRole($candidate) &&
               $this->hasRequiredPermissions($candidate) &&
               $this->isAssignedReviewer($candidate) &&
               $this->meetsBusinessRules($candidate);
    }
    
    public function toQuery(Builder $query): Builder
    {
        // Complex business logic - handle at collection level
        return $query;
    }
    
    private function hasValidState(WorkflowInstance $workflow): bool
    {
        $stateSpec = new WorkflowStateSpecification(['in_progress', 'escalated']);
        return $stateSpec->canTransitionTo($workflow, $this->targetState);
    }
    
    private function hasRequiredRole(WorkflowInstance $workflow): bool
    {
        if (!$this->requiredRoles) {
            return true;
        }
        
        return $this->actor->hasAnyRole($this->requiredRoles);
    }
    
    private function hasRequiredPermissions(WorkflowInstance $workflow): bool
    {
        if (!$this->requiredPermissions) {
            return true;
        }
        
        foreach ($this->requiredPermissions as $permission) {
            if (!$this->actor->can($permission, $workflow->subject)) {
                return false;
            }
        }
        
        return true;
    }
    
    private function isAssignedReviewer(WorkflowInstance $workflow): bool
    {
        // Check if user is assigned to current step
        return $workflow->currentAssignments()
            ->where('user_id', $this->actor->id)
            ->exists();
    }
    
    private function meetsBusinessRules(WorkflowInstance $workflow): bool
    {
        // Additional business-specific rules
        if ($this->targetState === 'completed' && $workflow->type === 'document_approval') {
            // Ensure all required approvals are obtained
            return $workflow->hasAllRequiredApprovals();
        }
        
        if ($this->targetState === 'escalated') {
            // Can only escalate if deadline is approaching or exceeded
            return $workflow->isEscalationEligible();
        }
        
        return true;
    }
}

Step 3: Business Rule Specifications

ComplianceReviewRequiredSpecification

php
<?php

namespace App\Specifications\Workflow;

use App\Models\Document;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;

class ComplianceReviewRequiredSpecification extends AbstractSpecification
{
    private array $complianceRequiredTypes = [
        'policy_change',
        'employee_handbook', 
        'contract',
        'data_processing_agreement',
        'privacy_policy'
    ];
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof Document) {
            return false;
        }
        
        return $this->requiresComplianceByType($candidate) ||
               $this->requiresComplianceByContent($candidate) ||
               $this->requiresComplianceByImpact($candidate);
    }
    
    public function toQuery(Builder $query): Builder
    {
        return $query->where(function ($q) {
            $q->whereIn('type', $this->complianceRequiredTypes)
              ->orWhere('contains_personal_data', true)
              ->orWhere('regulatory_impact', true)
              ->orWhere('risk_level', '>=', 7);
        });
    }
    
    private function requiresComplianceByType(Document $document): bool
    {
        return in_array($document->type, $this->complianceRequiredTypes);
    }
    
    private function requiresComplianceByContent(Document $document): bool
    {
        return $document->contains_personal_data || 
               $document->contains_financial_data ||
               $document->contains_confidential_info;
    }
    
    private function requiresComplianceByImpact(Document $document): bool
    {
        return $document->regulatory_impact ||
               $document->risk_level >= 7 ||
               $document->affects_multiple_departments;
    }
    
    public function getComplianceRequirements(Document $document): array
    {
        $requirements = [];
        
        if ($this->requiresComplianceByType($document)) {
            $requirements[] = 'Document type requires compliance review';
        }
        
        if ($document->contains_personal_data) {
            $requirements[] = 'Contains personal data - GDPR compliance check required';
        }
        
        if ($document->regulatory_impact) {
            $requirements[] = 'Regulatory impact assessment required';
        }
        
        if ($document->risk_level >= 7) {
            $requirements[] = 'High risk level requires compliance approval';
        }
        
        return $requirements;
    }
}

HighValueApprovalSpecification

php
<?php

namespace App\Specifications\Workflow;

use App\Models\Document;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;

class HighValueApprovalSpecification extends AbstractSpecification
{
    private array $approvalThresholds = [
        'finance' => [
            'supervisor' => 1000,
            'manager' => 10000,
            'cfo' => 50000,
            'board' => 100000,
        ],
        'hr' => [
            'manager' => 5000,
            'hr_director' => 25000,
            'ceo' => 50000,
        ],
        'legal' => [
            'senior_counsel' => 0, // All legal docs need senior review
            'external_counsel' => 25000,
        ],
        'it' => [
            'it_manager' => 10000,
            'cto' => 50000,
            'board' => 200000,
        ],
    ];
    
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof Document) {
            return false;
        }
        
        return $this->requiresHighValueApproval($candidate);
    }
    
    public function toQuery(Builder $query): Builder
    {
        // Build dynamic query based on department thresholds
        return $query->where(function ($q) {
            foreach ($this->approvalThresholds as $department => $thresholds) {
                $minThreshold = min(array_values($thresholds));
                $q->orWhere(function ($deptQuery) use ($department, $minThreshold) {
                    $deptQuery->where('department', $department)
                             ->where('amount', '>', $minThreshold);
                });
            }
        });
    }
    
    private function requiresHighValueApproval(Document $document): bool
    {
        $thresholds = $this->approvalThresholds[$document->department] ?? [];
        
        if (empty($thresholds)) {
            return false;
        }
        
        $minThreshold = min(array_values($thresholds));
        return $document->amount > $minThreshold;
    }
    
    public function getRequiredApprovalLevel(Document $document): string
    {
        $thresholds = $this->approvalThresholds[$document->department] ?? [];
        $amount = $document->amount;
        
        $requiredLevel = 'supervisor'; // Default
        
        foreach ($thresholds as $level => $threshold) {
            if ($amount > $threshold) {
                $requiredLevel = $level;
            }
        }
        
        return $requiredLevel;
    }
    
    public function getApprovalChain(Document $document): array
    {
        $thresholds = $this->approvalThresholds[$document->department] ?? [];
        $amount = $document->amount;
        $chain = [];
        
        foreach ($thresholds as $level => $threshold) {
            if ($amount > $threshold) {
                $chain[] = $level;
            }
        }
        
        return $chain;
    }
}

Step 4: Workflow Engine Core

Create a sophisticated workflow engine using specifications:

php
<?php

namespace App\Services;

use App\Models\WorkflowInstance;
use App\Models\WorkflowDefinition;
use App\Models\WorkflowStep;
use App\Models\User;
use App\Events\WorkflowStateChanged;
use App\Events\WorkflowStepAssigned;
use App\Events\WorkflowCompleted;
use App\Specifications\Workflow\TransitionEligibilitySpecification;
use App\Specifications\Workflow\ComplianceReviewRequiredSpecification;
use App\Specifications\Workflow\HighValueApprovalSpecification;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Collection;

class WorkflowEngineService
{
    public function __construct(
        private readonly ComplianceReviewRequiredSpecification $complianceSpec,
        private readonly HighValueApprovalSpecification $highValueSpec
    ) {}
    
    public function startWorkflow(string $workflowType, $subject, User $initiator, array $context = []): WorkflowInstance
    {
        return DB::transaction(function () use ($workflowType, $subject, $initiator, $context) {
            $definition = WorkflowDefinition::where('type', $workflowType)
                ->where('is_active', true)
                ->first();
                
            if (!$definition) {
                throw new \Exception("No active workflow definition found for type: {$workflowType}");
            }
            
            $workflow = WorkflowInstance::create([
                'workflow_definition_id' => $definition->id,
                'type' => $workflowType,
                'subject_id' => $subject->getKey(),
                'subject_type' => get_class($subject),
                'status' => 'pending',
                'context' => $context,
                'started_by' => $initiator->id,
                'started_at' => now(),
            ]);
            
            // Determine first step based on business rules
            $firstStep = $this->determineFirstStep($workflow, $subject);
            
            $this->transitionToStep($workflow, $firstStep);
            
            Log::info('Workflow started', [
                'workflow_id' => $workflow->id,
                'type' => $workflowType,
                'subject' => get_class($subject) . ':' . $subject->getKey(),
                'first_step' => $firstStep->name,
            ]);
            
            event(new WorkflowStateChanged($workflow, null, 'pending'));
            
            return $workflow;
        });
    }
    
    public function processTransition(
        WorkflowInstance $workflow, 
        User $actor, 
        string $action, 
        array $data = []
    ): bool {
        return DB::transaction(function () use ($workflow, $actor, $action, $data) {
            
            // Validate transition eligibility
            $targetState = $this->getTargetState($workflow->current_step, $action);
            
            $transitionSpec = new TransitionEligibilitySpecification(
                $targetState, 
                $actor,
                $this->getRequiredRoles($workflow->current_step, $action),
                $this->getRequiredPermissions($workflow->current_step, $action)
            );
            
            if (!$transitionSpec->isSatisfiedBy($workflow)) {
                throw new \Exception('User not eligible for this transition');
            }
            
            // Record the action
            $workflowAction = $workflow->actions()->create([
                'user_id' => $actor->id,
                'action' => $action,
                'step' => $workflow->current_step,
                'data' => $data,
                'performed_at' => now(),
            ]);
            
            $previousState = $workflow->status;
            
            // Process the transition
            if ($action === 'approve') {
                $this->processApproval($workflow, $actor, $data);
            } elseif ($action === 'reject') {
                $this->processRejection($workflow, $actor, $data);
            } elseif ($action === 'request_changes') {
                $this->processChangeRequest($workflow, $actor, $data);
            } elseif ($action === 'escalate') {
                $this->processEscalation($workflow, $actor, $data);
            }
            
            event(new WorkflowStateChanged($workflow, $previousState, $workflow->status));
            
            return true;
        });
    }
    
    private function determineFirstStep(WorkflowInstance $workflow, $subject): WorkflowStep
    {
        $definition = $workflow->definition;
        $firstSteps = $definition->steps()->where('is_initial', true)->get();
        
        if ($firstSteps->count() === 1) {
            return $firstSteps->first();
        }
        
        // Multiple possible first steps - use specifications to decide
        foreach ($firstSteps as $step) {
            if ($this->stepConditionsMet($step, $subject)) {
                return $step;
            }
        }
        
        throw new \Exception('No suitable first step found for workflow');
    }
    
    private function stepConditionsMet(WorkflowStep $step, $subject): bool
    {
        $conditions = $step->conditions ?? [];
        
        foreach ($conditions as $condition) {
            $specClass = $condition['specification'];
            $spec = app($specClass, $condition['parameters'] ?? []);
            
            if (!$spec->isSatisfiedBy($subject)) {
                return false;
            }
        }
        
        return true;
    }
    
    private function processApproval(WorkflowInstance $workflow, User $actor, array $data): void
    {
        $currentStep = $workflow->currentStep();
        $nextSteps = $this->determineNextSteps($workflow, $currentStep, 'approved');
        
        if (empty($nextSteps)) {
            // No next steps - workflow complete
            $workflow->update([
                'status' => 'completed',
                'completed_at' => now(),
            ]);
            
            event(new WorkflowCompleted($workflow));
        } else {
            // Transition to next step(s)
            foreach ($nextSteps as $nextStep) {
                $this->transitionToStep($workflow, $nextStep);
            }
        }
    }
    
    private function processRejection(WorkflowInstance $workflow, User $actor, array $data): void
    {
        $workflow->update([
            'status' => 'rejected',
            'current_step' => null,
        ]);
        
        // Clear all assignments
        $workflow->assignments()->update(['completed_at' => now()]);
    }
    
    private function processChangeRequest(WorkflowInstance $workflow, User $actor, array $data): void
    {
        $workflow->update([
            'status' => 'changes_requested',
            'current_step' => 'awaiting_changes',
        ]);
        
        // Notify document owner
        $this->notifySubjectOwner($workflow, 'changes_requested', $data);
    }
    
    private function processEscalation(WorkflowInstance $workflow, User $actor, array $data): void
    {
        $escalationStep = $this->getEscalationStep($workflow);
        
        if ($escalationStep) {
            $workflow->update(['status' => 'escalated']);
            $this->transitionToStep($workflow, $escalationStep);
        }
    }
    
    private function determineNextSteps(WorkflowInstance $workflow, WorkflowStep $currentStep, string $outcome): array
    {
        $subject = $workflow->subject;
        $transitions = $currentStep->transitions()
            ->where('trigger', $outcome)
            ->get();
        
        $nextSteps = [];
        
        foreach ($transitions as $transition) {
            $targetStep = $transition->targetStep;
            
            // Check if transition conditions are met
            if ($this->stepConditionsMet($targetStep, $subject)) {
                $nextSteps[] = $targetStep;
                
                // If not parallel processing, take first valid step
                if (!$transition->parallel) {
                    break;
                }
            }
        }
        
        return $nextSteps;
    }
    
    private function transitionToStep(WorkflowInstance $workflow, WorkflowStep $step): void
    {
        $workflow->update([
            'current_step' => $step->name,
            'status' => 'in_progress',
        ]);
        
        // Assign reviewers for this step
        $assignees = $this->getStepAssignees($workflow, $step);
        
        foreach ($assignees as $assignee) {
            $workflow->assignments()->create([
                'step_name' => $step->name,
                'user_id' => $assignee->id,
                'assigned_at' => now(),
                'due_at' => now()->addHours($step->sla_hours ?? 24),
            ]);
            
            event(new WorkflowStepAssigned($workflow, $step, $assignee));
        }
    }
    
    private function getStepAssignees(WorkflowInstance $workflow, WorkflowStep $step): Collection
    {
        $assignmentRules = $step->assignment_rules ?? [];
        $assignees = collect();
        
        foreach ($assignmentRules as $rule) {
            $ruleAssignees = match($rule['type']) {
                'role' => User::role($rule['role'])->get(),
                'department' => User::where('department', $rule['department'])->get(),
                'user' => User::whereIn('id', $rule['user_ids'])->get(),
                'dynamic' => $this->getDynamicAssignees($workflow, $rule),
                default => collect()
            };
            
            $assignees = $assignees->merge($ruleAssignees);
        }
        
        return $assignees->unique('id');
    }
    
    private function getDynamicAssignees(WorkflowInstance $workflow, array $rule): Collection
    {
        $specClass = $rule['specification'];
        $spec = app($specClass, $rule['parameters'] ?? []);
        
        return User::all()->filter(fn($user) => $spec->isSatisfiedBy($user));
    }
}

Step 5: Real-Time Updates with Broadcasting

Implement real-time workflow updates:

php
<?php

namespace App\Events;

use App\Models\WorkflowInstance;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class WorkflowStateChanged implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;
    
    public function __construct(
        public readonly WorkflowInstance $workflow,
        public readonly ?string $previousState,
        public readonly string $newState
    ) {}
    
    public function broadcastOn(): array
    {
        return [
            new PresenceChannel('workflow.' . $this->workflow->id),
            new Channel('workflows'),
            new Channel('user.' . $this->workflow->started_by),
        ];
    }
    
    public function broadcastWith(): array
    {
        return [
            'workflow_id' => $this->workflow->id,
            'type' => $this->workflow->type,
            'previous_state' => $this->previousState,
            'new_state' => $this->newState,
            'current_step' => $this->workflow->current_step,
            'subject' => [
                'type' => $this->workflow->subject_type,
                'id' => $this->workflow->subject_id,
            ],
            'timestamp' => now()->toISOString(),
        ];
    }
    
    public function broadcastAs(): string
    {
        return 'workflow.state.changed';
    }
}

Step 6: Command Pattern Integration

Create workflow commands for complex operations:

php
<?php

namespace App\Commands\Workflow;

use App\Models\WorkflowInstance;
use App\Models\User;
use App\Services\WorkflowEngineService;
use Illuminate\Support\Facades\Log;

abstract class WorkflowCommand
{
    protected WorkflowInstance $workflow;
    protected User $actor;
    protected array $context;
    
    public function __construct(WorkflowInstance $workflow, User $actor, array $context = [])
    {
        $this->workflow = $workflow;
        $this->actor = $actor;
        $this->context = $context;
    }
    
    abstract public function execute(): bool;
    
    public function canExecute(): bool
    {
        return true;
    }
    
    public function getWorkflow(): WorkflowInstance
    {
        return $this->workflow;
    }
}
php
<?php

namespace App\Commands\Workflow;

use App\Specifications\Workflow\TransitionEligibilitySpecification;

class ApproveDocumentCommand extends WorkflowCommand
{
    public function canExecute(): bool
    {
        $spec = new TransitionEligibilitySpecification(
            'approved',
            $this->actor,
            ['reviewer', 'manager', 'admin'],
            ['approve_documents']
        );
        
        return $spec->isSatisfiedBy($this->workflow);
    }
    
    public function execute(): bool
    {
        if (!$this->canExecute()) {
            throw new \Exception('User not authorized to approve this document');
        }
        
        $engineService = app(WorkflowEngineService::class);
        
        return $engineService->processTransition(
            $this->workflow,
            $this->actor,
            'approve',
            $this->context
        );
    }
}

Step 7: Event Sourcing Integration

Implement event sourcing for complete audit trail:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class WorkflowEvent extends Model
{
    protected $fillable = [
        'workflow_instance_id',
        'event_type',
        'event_data',
        'actor_id',
        'occurred_at',
        'sequence_number',
    ];
    
    protected $casts = [
        'event_data' => 'array',
        'occurred_at' => 'datetime',
    ];
    
    public function workflow(): BelongsTo
    {
        return $this->belongsTo(WorkflowInstance::class, 'workflow_instance_id');
    }
    
    public function actor(): BelongsTo
    {
        return $this->belongsTo(User::class, 'actor_id');
    }
}
php
<?php

namespace App\Services;

use App\Models\WorkflowInstance;
use App\Models\WorkflowEvent;
use App\Models\User;

class WorkflowEventStore
{
    public function record(
        WorkflowInstance $workflow,
        string $eventType,
        array $eventData,
        ?User $actor = null
    ): WorkflowEvent {
        $lastSequence = WorkflowEvent::where('workflow_instance_id', $workflow->id)
            ->max('sequence_number') ?? 0;
        
        return WorkflowEvent::create([
            'workflow_instance_id' => $workflow->id,
            'event_type' => $eventType,
            'event_data' => $eventData,
            'actor_id' => $actor?->id,
            'occurred_at' => now(),
            'sequence_number' => $lastSequence + 1,
        ]);
    }
    
    public function replayWorkflow(WorkflowInstance $workflow): WorkflowInstance
    {
        $events = WorkflowEvent::where('workflow_instance_id', $workflow->id)
            ->orderBy('sequence_number')
            ->get();
        
        // Reset workflow to initial state
        $workflow->status = 'pending';
        $workflow->current_step = null;
        
        // Replay all events
        foreach ($events as $event) {
            $this->applyEvent($workflow, $event);
        }
        
        return $workflow;
    }
    
    private function applyEvent(WorkflowInstance $workflow, WorkflowEvent $event): void
    {
        match($event->event_type) {
            'workflow.started' => $workflow->status = 'pending',
            'step.assigned' => $workflow->current_step = $event->event_data['step_name'],
            'transition.completed' => $workflow->status = $event->event_data['new_state'],
            'workflow.completed' => $workflow->status = 'completed',
            'workflow.rejected' => $workflow->status = 'rejected',
            default => null
        };
    }
}

Step 8: Comprehensive Testing

Integration Testing

php
<?php

namespace Tests\Feature;

use App\Models\WorkflowDefinition;
use App\Models\WorkflowStep;
use App\Models\Document;
use App\Models\User;
use App\Services\WorkflowEngineService;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class WorkflowEngineIntegrationTest extends TestCase
{
    use RefreshDatabase;
    
    /** @test */
    public function complete_document_approval_workflow_with_compliance_review()
    {
        // Arrange
        $this->createDocumentApprovalWorkflow();
        
        $hrManager = User::factory()->create(['role' => 'hr_manager']);
        $complianceOfficer = User::factory()->create(['role' => 'compliance_officer']);
        $ceo = User::factory()->create(['role' => 'ceo']);
        
        $document = Document::factory()->create([
            'type' => 'policy_change',
            'department' => 'hr',
            'contains_personal_data' => true,
            'created_by' => $hrManager->id,
        ]);
        
        $workflowService = app(WorkflowEngineService::class);
        
        // Act & Assert
        
        // 1. Start workflow
        $workflow = $workflowService->startWorkflow('document_approval', $document, $hrManager);
        
        $this->assertEquals('compliance_review', $workflow->current_step);
        $this->assertEquals('in_progress', $workflow->status);
        
        // 2. Compliance approval
        $this->assertTrue($workflowService->processTransition(
            $workflow->fresh(),
            $complianceOfficer,
            'approve',
            ['comments' => 'GDPR compliance verified']
        ));
        
        $workflow->refresh();
        $this->assertEquals('manager_review', $workflow->current_step);
        
        // 3. Manager approval
        $this->assertTrue($workflowService->processTransition(
            $workflow,
            $hrManager,
            'approve',
            ['comments' => 'Policy changes look good']
        ));
        
        $workflow->refresh();
        $this->assertEquals('senior_management_review', $workflow->current_step);
        
        // 4. Senior management approval
        $this->assertTrue($workflowService->processTransition(
            $workflow,
            $ceo,
            'approve',
            ['comments' => 'Final approval granted']
        ));
        
        $workflow->refresh();
        $this->assertEquals('completed', $workflow->status);
        $this->assertNotNull($workflow->completed_at);
    }
    
    /** @test */
    public function workflow_handles_rejection_correctly()
    {
        // Arrange
        $this->createDocumentApprovalWorkflow();
        
        $manager = User::factory()->create(['role' => 'manager']);
        $document = Document::factory()->create(['department' => 'general']);
        
        $workflowService = app(WorkflowEngineService::class);
        
        // Act
        $workflow = $workflowService->startWorkflow('document_approval', $document, $manager);
        
        $this->assertTrue($workflowService->processTransition(
            $workflow,
            $manager,
            'reject',
            ['comments' => 'Document needs significant revisions']
        ));
        
        // Assert
        $workflow->refresh();
        $this->assertEquals('rejected', $workflow->status);
        $this->assertNull($workflow->current_step);
    }
    
    /** @test */
    public function workflow_enforces_role_based_access_control()
    {
        // Arrange
        $this->createDocumentApprovalWorkflow();
        
        $regularUser = User::factory()->create(['role' => 'user']);
        $document = Document::factory()->create();
        
        $workflowService = app(WorkflowEngineService::class);
        $workflow = $workflowService->startWorkflow('document_approval', $document, $regularUser);
        
        // Act & Assert
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('User not eligible for this transition');
        
        $workflowService->processTransition(
            $workflow,
            $regularUser, // User without reviewer role
            'approve'
        );
    }
    
    private function createDocumentApprovalWorkflow(): void
    {
        $definition = WorkflowDefinition::create([
            'name' => 'Document Approval',
            'type' => 'document_approval',
            'version' => 1,
            'is_active' => true,
        ]);
        
        // Create workflow steps
        $complianceStep = WorkflowStep::create([
            'workflow_definition_id' => $definition->id,
            'name' => 'compliance_review',
            'display_name' => 'Compliance Review',
            'is_initial' => true,
            'conditions' => [
                ['specification' => 'App\Specifications\Workflow\ComplianceReviewRequiredSpecification']
            ],
            'assignment_rules' => [
                ['type' => 'role', 'role' => 'compliance_officer']
            ],
            'sla_hours' => 48,
        ]);
        
        $managerStep = WorkflowStep::create([
            'workflow_definition_id' => $definition->id,
            'name' => 'manager_review',
            'display_name' => 'Manager Review',
            'is_initial' => true,
            'assignment_rules' => [
                ['type' => 'role', 'role' => 'manager']
            ],
            'sla_hours' => 24,
        ]);
        
        $seniorStep = WorkflowStep::create([
            'workflow_definition_id' => $definition->id,
            'name' => 'senior_management_review',
            'display_name' => 'Senior Management Review',
            'assignment_rules' => [
                ['type' => 'role', 'role' => 'ceo']
            ],
            'sla_hours' => 72,
        ]);
        
        // Create transitions
        $complianceStep->transitions()->create([
            'trigger' => 'approved',
            'target_step_id' => $managerStep->id,
        ]);
        
        $managerStep->transitions()->create([
            'trigger' => 'approved',
            'target_step_id' => $seniorStep->id,
            'conditions' => [
                ['specification' => 'App\Specifications\Workflow\SeniorReviewRequiredSpecification']
            ]
        ]);
    }
}

Key Learnings

This advanced example demonstrates:

Sophisticated State Management: Complex workflow transitions with business rules
Specification-Driven Logic: Dynamic workflow paths based on specifications
Real-Time Updates: Live workflow status broadcasting to distributed teams
Event Sourcing: Complete audit trail with event replay capabilities
Command Pattern: Encapsulated workflow operations with authorization
Role-Based Access: Fine-grained permissions for workflow actions
Enterprise Scalability: Distributed processing with queue integration

Next Steps

With your distributed workflow engine:

  • Add workflow analytics to track performance and bottlenecks
  • Implement machine learning for automatic assignment optimization
  • Create visual workflow designer for business users
  • Add integration APIs for external system workflow triggers

Congratulations! You've mastered the most advanced specification patterns for enterprise-grade Laravel applications.

Return to Examples Overview →

Released under the MIT License.