Skip to content

3. MVP Domain Model

3.1 Domain Model Overview

Note: See actual implementation in popupsim/backend/src/contexts/

The MVP domain model follows Domain-Driven Design principles across 4 bounded contexts.

The Configuration context uses Pydantic models/DTOs for input; the Retrofit Workflow context uses plain dataclass entities and aggregates for the simulation domain. The diagram below shows the main simulation-domain types (see the linked files for the authoritative definitions).

classDiagram
    class Scenario {
        +str id
        +datetime start_date
        +datetime end_date
        +list~WorkshopInputDTO~ workshops
        +Sequence~TrackInputDTO~ tracks
        +list~LocomotiveInputDTO~ locomotives
        +ProcessTimes process_times
    }

    class Wagon {
        +str id
        +float length
        +Coupler coupler_a
        +Coupler coupler_b
        +str train_id
        +WagonStatus status
    }

    class Workshop {
        +list~RetrofitBay~ bays
    }

    class RetrofitBay {
        +str id
        +str workshop_id
        +BayStatus status
    }

    Scenario ..> Wagon : configures
    Workshop --o RetrofitBay

Authoritative definitions: - Scenario, ProcessTimes, Topology: contexts/configuration/domain/models/ - Input DTOs (WorkshopInputDTO, TrackInputDTO, ...): contexts/configuration/application/dtos/ - Wagon, Workshop, RetrofitBay: contexts/retrofit_workflow/domain/entities/

3.2 Configuration Context Models

Actual implementation: popupsim/backend/src/contexts/configuration/domain/models/

Scenario (abridged)

The real Scenario model carries more than shown here (selection strategies, parking thresholds, locomotive strategies, and a task_priorities map). See scenario.py for the full definition.

from datetime import datetime
from pydantic import BaseModel


class Scenario(BaseModel):
    """Scenario configuration for simulation (abridged)."""

    id: str
    start_date: datetime
    end_date: datetime
    workshops: list[WorkshopInputDTO] | None = None
    tracks: Sequence[TrackInputDTO] = []
    locomotives: list[LocomotiveInputDTO] | None = None
    routes: list[RouteInputDTO] | None = None
    process_times: ProcessTimes | None = None
    trains: Any | None = None
    # ... plus selection strategies, parking thresholds, task_priorities, etc.

WorkshopInputDTO

Workshops are provided as input DTOs (application/dtos/workshop_input_dto.py):

class WorkshopInputDTO(BaseModel):
    """Workshop configuration input."""

    id: str
    track: str
    retrofit_stations: int

3.3 Retrofit Workflow Domain Services

Actual implementation: popupsim/backend/src/contexts/retrofit_workflow/domain/services/

Domain services are pure business logic (no SimPy dependencies). The signatures below are representative; see the linked files for the authoritative definitions.

Batch Formation Service

batch_formation_service.py builds batches for the different transport legs, e.g. form_batch_for_retrofit_track(...), form_batch_for_workshop(...), form_batch_for_parking_track(...), plus create_batch_aggregate(...) and can_form_batch(...):

class BatchFormationService:
    """Form wagon batches (no SimPy dependencies)."""

    def form_batch_for_workshop(self, wagons: list[Wagon], ...) -> Batch:
        """Form a batch of wagons for workshop processing."""
        ...

Workshop Scheduling Service

workshop_scheduling_service.py schedules a batch onto a workshop and returns a SchedulingResult:

class WorkshopSchedulingService:
    """Schedule wagon batches to workshops (no SimPy dependencies)."""

    def schedule_batch(self, wagons: list[Wagon], workshop: Workshop) -> SchedulingResult:
        """Schedule wagons for workshop processing."""
        ...

    def can_workshop_handle_batch(self, batch_size: int, workshop: Workshop) -> bool:
        """Check whether the workshop can accept a batch of this size."""
        ...

3.4 Validation Result

Configuration validation uses ValidationResult from shared/validation/base.py, which collects a list of ValidationIssues (each with a level) rather than separate error/warning string lists:

from dataclasses import dataclass, field


@dataclass
class ValidationResult:
    """Result of validation process."""

    is_valid: bool
    issues: list[ValidationIssue] = field(default_factory=list)

    def has_errors(self) -> bool:
        return any(i.level == ValidationLevel.ERROR for i in self.issues)

    def get_errors(self) -> list[ValidationIssue]:
        return [i for i in self.issues if i.level == ValidationLevel.ERROR]

The multi-layer validation pipeline lives under shared/validation/ (see ADR-002).

3.5 Simulation Result

The simulation result is defined in application/simulation_service.py:

from dataclasses import dataclass
from typing import Any


@dataclass
class SimulationResult:
    """Result of simulation execution."""

    metrics: dict[str, Any]
    duration: float
    success: bool

Aggregated KPIs (completion rate, throughput, workshop/locomotive statistics) are written to summary_metrics.json by the event collector and exporters; see Running the Simulation.

3.6 Type Hints

All code includes explicit type annotations per project rules (ADR-005); MyPy runs in strict mode (disallow_untyped_defs = true). Example:

def can_workshop_handle_batch(self, batch_size: int, workshop: Workshop) -> bool:
    """Check whether the workshop can accept a batch of this size."""
    ...

3.7 Migration Path

The simplified MVP domain model can be extended to full DDD implementation:

Phase 1 (Post-MVP): Rich Domain Model

  • Aggregate roots with invariants
  • Domain services for complex business logic
  • Repository pattern for persistence

Phase 2: Event Sourcing

  • Event store implementation
  • Event-driven state reconstruction
  • Temporal queries

Phase 3: Advanced DDD

  • Specification pattern for complex queries
  • Domain events with saga pattern
  • CQRS for read/write separation

Effort: Estimated 2-3 weeks for full DDD migration (to be validated)