Busflow Docs

Internal documentation portal

Skip to content

ADR-001: Cross-Context Strategy β€” BoardingPoint (Zustiegsstelle) ​

Problem Statement ​

The domain model lacks a first-class entity for passenger-facing pickup locations (Zustiegsstellen). The concept appears in user journeys (seat + boarding point selection, delay broadcasts to waiting passengers) and in the app-structure (pickup-sequence optimization), but has no backing entity.

RouteWaypoint in Operations represents GPS execution paths β€” not selectable, scheduled pickup stops.

Decision ​

BoardingPoint is a distinct domain concept per bounded context, not a shared entity.

Per-Context Representations ​

ContextEntity / ProjectionRole
Backofficeboarding_point_library (entity, authoritative)Operator-level library of pickup locations. Assigned to TourTemplate via template_boarding_point_assignments. Departures inherit template assignments at V0.1.
CommercePickupOption (read projection)Synced from Backoffice via TripPublished event. Passenger selects one during checkout.
OperationsServiceLeg with leg_type: PICKUPOperational execution of the stop β€” crew assignment, GPS routing, ETA tracking.

Backoffice Entity: boarding_point_library ​

Owned by schema: backoffice. Core fields:

  • name β€” Passenger-facing label (e.g. "KΓΆln Hbf, Bussteig 3")
  • address, geo_coordinates β€” Physical location
  • zone_label β€” Grouping label for UI (e.g. "Nachbardorf")
  • surcharge β€” Stop pickup surcharge in cents
  • door_pickup_available, door_pickup_surcharge, door_pickup_radius_km β€” Optional door pickup feature
  • passenger_instructions β€” Optional guidance ("South exit, look for BusFlow sign")

NOTE

V0.2 additions: boarding_order (pickup sequence) and scheduled_departure_time are deferred to V0.2 as dispatch-side concerns.

Commerce Reference ​

Passenger in schema: commerce stores a boarding_point_id (soft FK to backoffice.boarding_point_library) representing the traveler's chosen pickup location. An is_door_pickup boolean indicates the pickup mode (stop vs. door). See schema-commerce.md Β§passengers.

Cross-Context Data Access ​

Reads: CQRS via Hasura ​

Backoffice does not sync passenger counts into its own schema. The dispatcher dashboard uses a read-only SQL view joining across schemas β€” consistent with the database boundary rules:

sql
CREATE VIEW dispatcher.boarding_point_occupancy AS
SELECT
  bpl.id, bpl.name,
  COUNT(p.id) AS confirmed_passengers
FROM backoffice.boarding_point_library bpl
JOIN backoffice.template_boarding_point_assignments tba ON tba.boarding_point_id = bpl.id
LEFT JOIN commerce.passengers p
  ON p.boarding_point_id = bpl.id AND p.status = 'ACTIVE'
GROUP BY bpl.id;

NOTE

V0.2 update needed: This view will expand with scheduled_departure_time and max_capacity columns once boarding order and per-stop capacity land.

IMPORTANT

Write-side coupling (context A mutates context B) is always prohibited. Read-side coupling via SQL views for UI display is explicitly allowed per our CQRS strategy.

Writes: Saga Pattern for Destructive Operations ​

Deleting a BoardingPoint that the system references by active bookings requires a cross-context choreography:

  1. Backoffice marks the point PENDING_REMOVAL and emits BoardingPointRemovalRequested
  2. Commerce assesses impact (affected passenger count) and responds:
    • BoardingPointRemovalApproved β€” no references, safe to delete
    • BoardingPointRemovalRejected β€” returns affected count + suggested alternative
  3. Dispatcher confirms reassignment β†’ Backoffice emits BoardingPointReassignmentOrdered
  4. Commerce migrates passengers to the new boarding_point_id, emits PassengersReassigned
  5. Backoffice deletes the BoardingPoint, emits BoardingPointDeleted β†’ Commerce drops its PickupOption projection

NOTE

In the modular monolith, this saga can implement as a synchronous domain service in the application layer (calling each module's public API in sequence) instead of requiring async message queues. Both are semantically equivalent.

Domain Events ​

EventEmitterConsumerPurpose
TripPublishedBackofficeCommerceSyncs BoardingPoint β†’ PickupOption projection
BoardingPointRemovalRequestedBackofficeCommerceInitiates safe-delete assessment
BoardingPointRemovalApprovedCommerceBackofficeConfirms zero references
BoardingPointRemovalRejectedCommerceBackofficeReturns affected passengers + alternative
BoardingPointReassignmentOrderedBackofficeCommerceTriggers passenger migration
PassengersReassignedCommerceBackofficeConfirms migration complete
BoardingPointDeletedBackofficeCommerce, OperationsFinal cleanup signal

Rationale: Why Not Extend ServiceLeg? ​

Adding passenger-facing fields (display_name, is_bookable) directly to ServiceLeg was considered and rejected:

  • Violates bounded context boundaries β€” Commerce would need to read Operations data for the booking widget
  • Conflates operational and commercial concerns β€” ServiceLeg's purpose is vehicle movement execution, not checkout UX
  • Different lifecycles β€” BoardingPoints are defined at planning time on the TourDeparture; ServiceLegs are created at dispatch time

Internal documentation β€” Busflow