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 β
| Context | Entity / Projection | Role |
|---|---|---|
| Backoffice | boarding_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. |
| Commerce | PickupOption (read projection) | Synced from Backoffice via TripPublished event. Passenger selects one during checkout. |
| Operations | ServiceLeg with leg_type: PICKUP | Operational 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 locationzone_labelβ Grouping label for UI (e.g. "Nachbardorf")surchargeβ Stop pickup surcharge in centsdoor_pickup_available,door_pickup_surcharge,door_pickup_radius_kmβ Optional door pickup featurepassenger_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:
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:
- Backoffice marks the point
PENDING_REMOVALand emitsBoardingPointRemovalRequested - Commerce assesses impact (affected passenger count) and responds:
BoardingPointRemovalApprovedβ no references, safe to deleteBoardingPointRemovalRejectedβ returns affected count + suggested alternative
- Dispatcher confirms reassignment β Backoffice emits
BoardingPointReassignmentOrdered - Commerce migrates passengers to the new
boarding_point_id, emitsPassengersReassigned - Backoffice deletes the
BoardingPoint, emitsBoardingPointDeletedβ Commerce drops itsPickupOptionprojection
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 β
| Event | Emitter | Consumer | Purpose |
|---|---|---|---|
TripPublished | Backoffice | Commerce | Syncs BoardingPoint β PickupOption projection |
BoardingPointRemovalRequested | Backoffice | Commerce | Initiates safe-delete assessment |
BoardingPointRemovalApproved | Commerce | Backoffice | Confirms zero references |
BoardingPointRemovalRejected | Commerce | Backoffice | Returns affected passengers + alternative |
BoardingPointReassignmentOrdered | Backoffice | Commerce | Triggers passenger migration |
PassengersReassigned | Commerce | Backoffice | Confirms migration complete |
BoardingPointDeleted | Backoffice | Commerce, Operations | Final 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