Baseleg Docs
Domain · v1 spine / Aircraft · Baseleg Docs
v1 primary

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 identifier
  • registration — official registration mark (e.g. ZK-ABC); unique and immutable after creation
  • type — aircraft type / model (e.g. Cessna 172)
  • statusavailable | grounded (administrative dimension)
  • groundedReason — required when status is grounded; null otherwise
  • groundedAt — timestamp of when grounding was applied
  • groundedByPersonId of the staff person who applied the grounding
  • usageStatusavailable | in_use (real-time operational dimension, orthogonal to status — 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 updates currentHobbs/currentTach directly; a reading below the current value is rejected.

Domain rules

  1. Registration must be unique within the organisation.
  2. An aircraft cannot be booked when its status is grounded.
  3. Grounding requires a groundedReason; it may not be empty.
  4. Only authorised personnel (Staff type) can ground or unground an aircraft.
  5. An aircraft cannot be hard-deleted if it has associated Bookings or Flights; deactivation or status annotation is used instead.
  6. An aircraft cannot be dispatched (usageStatusin_use) while grounded, or while already in_use.
  7. 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 groundedReason is 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. See AircraftDispatcherD1.release().

Concurrent dispatch/release race (issue #40) — resolved: dispatch/release used 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 read usageStatus: '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) issues UPDATE aircraft SET ... WHERE id = ? AND usage_status = ? and reports whether any row matched (D1’s meta.changes). If the aircraft’s usage_status changed between the read and the write — i.e. another request won the race — zero rows match, and AircraftDispatcherD1 returns a typed aircraft_unavailable error instead of silently double-persisting.

Rejected alternatives: a version/optimistic-lock column (needs a schema migration; the conditional WHERE gets 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 unconditional update(), just one added WHERE predicate — 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 to grounded with a mandatory reason.
  • ungroundAircraft — transition status back to available; clears grounding fields.
  • listAircraft — return all aircraft with current status (filterable by status).
  • getAircraft — return a single aircraft by ID or registration.
  • dispatchAircraft — transition usageStatus to in_use; the domain-layer half of Scheduling’s Checkout transaction.
  • releaseAircraft — transition usageStatus back to available; 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

ContextUsage
SchedulingChecks 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
ComplianceReferences AircraftId for airworthiness document tracking
ReportingReads 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.