Aircraft
Packages: packages/domain/aircraft, packages/application/aircraft
Purpose
The Aircraft context manages the fleet: what aircraft exist, their details, and their operational state. It is the authoritative source of whether an aircraft is available or grounded. The Scheduling context consults Aircraft state before allowing a booking.
Entities
Aircraft (aggregate root)
Represents a single aircraft in the fleet.
Key attributes:
id— unique identifierregistration— official registration mark (e.g.ZK-ABC); unique and immutable after creationtype— aircraft type / model (e.g.Cessna 172)status—available|grounded(administrative dimension)groundedReason— required when status isgrounded; null otherwisegroundedAt— timestamp of when grounding was appliedgroundedBy—PersonIdof the staff person who applied the groundingusageStatus—available|in_use(real-time operational dimension, orthogonal tostatus— see MeterReading below)currentHobbs/currentTach— the aircraft’s current Hobbs/tach meter values; null until first recorded
Value objects
- AircraftId — typed identifier wrapping a UUID.
- Registration — validated aircraft registration string; enforces format (e.g. alphanumeric, country prefix).
- GroundedReason — non-empty string describing why the aircraft is grounded.
- MeterReading — a
{ meterType: 'hobbs' | 'tach', value, recordedAt }reading. Recording one updatescurrentHobbs/currentTachdirectly; a reading below the current value is rejected.
Domain rules
- Registration must be unique within the organisation.
- An aircraft cannot be booked when its status is
grounded. - Grounding requires a
groundedReason; it may not be empty. - Only authorised personnel (Staff type) can ground or unground an aircraft.
- An aircraft cannot be hard-deleted if it has associated Bookings or Flights; deactivation or status annotation is used instead.
- An aircraft cannot be dispatched (
usageStatus→in_use) whilegrounded, or while alreadyin_use. - A recorded meter reading must be a finite, non-negative number, and must not be lower than the aircraft’s current value for that meter type.
MVP limitation: if an in-use aircraft is grounded via Return with a reported defect, and it was already grounded for an unrelated reason before that flight, the existing
groundedReasonis kept rather than overwritten — the new defect is still recorded on the Flight (hasDefect/defectDescription), just not reflected in the Aircraft’s own grounding reason. SeeAircraftDispatcherD1.release().Concurrent dispatch/release race (issue #40) — resolved:
dispatch/releaseused to perform an unguarded read-modify-write (findById→ domain transition →update) with no concurrency check, so two near-simultaneous calls against the same aircraft could both readusageStatus: 'available', both pass validation, and both persist — two simultaneously “active” Flights on one physical aircraft. Fixed via a conditional update with an affected-rows check:AircraftRepositoryD1.updateIfUsageStatus(aircraft, expectedPriorUsageStatus)issuesUPDATE aircraft SET ... WHERE id = ? AND usage_status = ?and reports whether any row matched (D1’smeta.changes). If the aircraft’susage_statuschanged between the read and the write — i.e. another request won the race — zero rows match, andAircraftDispatcherD1returns a typedaircraft_unavailableerror instead of silently double-persisting.Rejected alternatives: a version/optimistic-lock column (needs a schema migration; the conditional
WHEREgets the same compare-and-swap guarantee for free, since D1 evaluates it against current row state, not the stale read); a full transaction wrapping the read and write (not available across the independently-constructed repository instance this class holds, and unnecessary given the conditional update already provides the correctness guarantee); and accepting the race as a documented MVP limitation (rejected because a fix this cheap — same statement shape as the prior unconditionalupdate(), just one addedWHEREpredicate — was available, and the gap had already been independently flagged twice).
Key use cases
addAircraft— register a new aircraft in the fleet.updateAircraft— update aircraft details (type, notes).groundAircraft— transition status togroundedwith a mandatory reason.ungroundAircraft— transition status back toavailable; clears grounding fields.listAircraft— return all aircraft with current status (filterable by status).getAircraft— return a single aircraft by ID or registration.dispatchAircraft— transitionusageStatustoin_use; the domain-layer half of Scheduling’s Checkout transaction.releaseAircraft— transitionusageStatusback toavailable; the domain-layer half of Scheduling’s Return transaction.recordMeterReading— record a Hobbs or tach reading, rejecting a non-finite, negative, or below-previous value.
Cross-context relationships
| Context | Usage |
|---|---|
| Scheduling | Checks aircraft status/usageStatus before allowing a Booking or Checkout; dispatches/releases the aircraft via the AircraftDispatcher port (Checkout/Return); holds AircraftId on a Booking and Flight |
| Compliance | References AircraftId for airworthiness document tracking |
| Reporting | Reads aircraft utilisation data (infrastructure layer only) |
The Aircraft context does not import from Scheduling or People. It owns fleet state; other contexts reference it by AircraftId. Scheduling reaches Aircraft only through the AircraftDispatcher port (packages/application/scheduling, implemented by AircraftDispatcherD1 in packages/infrastructure/repositories) — never by importing domain/aircraft’s mutation functions directly.