Files
thrillwiki_django_no_react/docs/architecture/adr-003-state-machine-pattern.md
pacnpal edcd8f2076 Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
2025-12-23 16:41:42 -05:00

6.2 KiB

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

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

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

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

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

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