# ADR-003: State Machine Pattern ## Status Accepted ## Context Parks and rides in ThrillWiki go through various operational states: - Parks: Operating, Closed Temporarily, Closed Permanently, Under Construction - Rides: Operating, Closed, Under Construction, Removed, Relocated Managing these state transitions requires: - Valid state transition enforcement - Audit trail of state changes - Business logic tied to state changes (notifications, cache invalidation) ## Decision We implemented a **Finite State Machine (FSM) pattern** for managing entity states, using django-fsm with custom enhancements. ### State Model ```python from django_fsm import FSMField, transition class Park(models.Model): status = FSMField(default='OPERATING') @transition( field=status, source=['OPERATING', 'CLOSED_TEMP'], target='CLOSED_PERM' ) def close_permanently(self, reason=None): """Close the park permanently.""" self.closure_reason = reason self.closure_date = timezone.now() @transition( field=status, source='CLOSED_TEMP', target='OPERATING' ) def reopen(self): """Reopen a temporarily closed park.""" self.closure_reason = None ``` ### State Diagram ``` Park States: ┌──────────────┐ │ PLANNED │ └──────┬───────┘ │ ▼ ┌──────────────────────────────────────┐ │ UNDER_CONSTRUCTION │ └──────────────────┬───────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ OPERATING │◄────┐ └──────────────────┬───────────────────┘ │ │ │ ┌───────────┼───────────┐ │ │ │ │ │ ▼ ▼ ▼ │ ┌────────────┐ ┌────────┐ ┌────────────┐ │ │CLOSED_TEMP │ │SEASONAL│ │CLOSED_PERM │ │ └─────┬──────┘ └────────┘ └────────────┘ │ │ │ └───────────────────────────────────────┘ ``` ### Transition Validation ```python class ParkStateTransition(models.Model): """Audit log for park state transitions.""" park = models.ForeignKey(Park, on_delete=models.CASCADE) from_state = models.CharField(max_length=20) to_state = models.CharField(max_length=20) transition_date = models.DateTimeField(auto_now_add=True) transitioned_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) reason = models.TextField(blank=True) ``` ## Consequences ### Benefits 1. **Valid Transitions Only**: Invalid state changes are rejected at the model level 2. **Audit Trail**: All transitions are logged with timestamps and users 3. **Business Logic Encapsulation**: Transition methods contain related logic 4. **Testability**: State machines are easy to unit test 5. **Documentation**: State diagrams document valid workflows ### Trade-offs 1. **Learning Curve**: Developers need to understand FSM concepts 2. **Migration Complexity**: Adding new states requires careful migration 3. **Flexibility**: Rigid state transitions can be limiting for edge cases ### State Change Hooks ```python from django.db.models.signals import pre_save from django.dispatch import receiver @receiver(pre_save, sender=Park) def park_state_change(sender, instance, **kwargs): if instance.pk: old_instance = Park.objects.get(pk=instance.pk) if old_instance.status != instance.status: # Log transition ParkStateTransition.objects.create( park=instance, from_state=old_instance.status, to_state=instance.status, ) # Invalidate caches invalidate_park_caches(instance) # Send notifications notify_state_change(instance, old_instance.status) ``` ## Alternatives Considered ### Simple Status Field **Rejected because:** - No validation of state transitions - Business logic scattered across codebase - No built-in audit trail ### Event Sourcing **Rejected because:** - Overkill for current requirements - Significant complexity increase - Steeper learning curve ### Workflow Engine **Rejected because:** - External dependency overhead - More complex than needed - FSM sufficient for current use cases ## Implementation Details ### Ride Status States ```python class RideStatus(models.TextChoices): OPERATING = 'OPERATING', 'Operating' CLOSED_TEMP = 'CLOSED_TEMP', 'Temporarily Closed' CLOSED_PERM = 'CLOSED_PERM', 'Permanently Closed' UNDER_CONSTRUCTION = 'UNDER_CONSTRUCTION', 'Under Construction' REMOVED = 'REMOVED', 'Removed' RELOCATED = 'RELOCATED', 'Relocated' ``` ### Testing State Transitions ```python class ParkStateTransitionTest(TestCase): def test_cannot_reopen_permanently_closed_park(self): park = ParkFactory(status='CLOSED_PERM') with self.assertRaises(TransitionNotAllowed): park.reopen() def test_can_close_operating_park_temporarily(self): park = ParkFactory(status='OPERATING') park.close_temporarily(reason='Maintenance') self.assertEqual(park.status, 'CLOSED_TEMP') ``` ## References - [django-fsm Documentation](https://github.com/viewflow/django-fsm) - [State Machine Diagrams](../state_machines/diagrams.md) - [Finite State Machine Wikipedia](https://en.wikipedia.org/wiki/Finite-state_machine)