Core Architecture & Regulatory Mapping

Aviation flight operations demand a precise alignment between regulatory mandates and software architecture. For flight operations managers, crew schedulers, compliance teams, and Python automation builders, the gap between published duty time regulations and executable code represents both a compliance risk and an engineering challenge. Core architecture in this domain is not merely about data storage or scheduling algorithms; it is a deterministic translation of legal text into validated computational workflows. Production-ready systems must ingest heterogeneous flight data, normalize temporal and operational variables, apply jurisdiction-specific validation logic, and surface actionable scheduling constraints without introducing latency or ambiguity.

The engineering difficulty is that regulations are written for human interpretation, not machine evaluation. A single sentence in 14 CFR Part 117 can encode a table lookup, a conditional branch, a lookback window, and an exception clause simultaneously. §117.13 alone maps a crew member’s flight duty period (FDP) limit to a two-dimensional table keyed on report time and number of flight segments, then §117.19 layers extension provisions on top, and §117.23 imposes cumulative caps that only become visible when duty periods are aggregated across rolling windows of 168 and 672 hours. No individual pairing can be validated in isolation; every scheduling decision is a function of the crew member’s entire recent history. This is why a compliant system is best understood as a chain of specialized subsystems — ingestion, normalization, classification, rule evaluation, and audit — each with a narrow contract and a testable output, rather than a monolithic “scheduler” that hides regulatory logic inside operational code.

End-to-end compliance pipeline Heterogeneous feeds — ACARS/IATA, crew management systems and scheduler input — are normalized to UTC, classified into a duty-time taxonomy, then evaluated by a rule engine against FAA Part 117 and EASA FTL rule sets to produce a compliance verdict that is written to a hash-chained audit log. A dashed degraded path routes evaluation through conservative fallback limits. Ingestion ACARS / IATA feeds Crew mgmt systems Scheduler input Normalize → UTC validate & tag zone Classify duty-time taxonomy Rule engine one contract Compliance verdict Hash-chained audit log Conservative fallback limits Part 117 · EASA FTL degraded
End-to-end compliance pipeline: heterogeneous feeds are normalized, classified, and evaluated against jurisdiction-specific rule sets through a single contract, with a conservative fallback path for degraded inputs.

The Compliance Architecture Problem

Flight operations software sits between two unforgiving stakeholders: regulators who audit for exact conformance to published limits, and dispatchers who need answers in seconds so an aircraft can push back on time. A design that satisfies one at the expense of the other fails in production. If the rule engine is slow, schedulers route around it and compliance becomes advisory. If it is fast but wrong, the operator accumulates silent violations that surface only during a Federal Aviation Administration or European Union Aviation Safety Agency ramp check. The core architecture must therefore be simultaneously deterministic, low-latency, fully auditable, and versioned against the exact regulatory revision in force on the date of the operation.

Three properties distinguish a serious compliance architecture from an ad-hoc scheduler. First, regulatory logic is data, not code buried in a scheduling loop — every limit, table, and exception is expressed as a versioned rule set that can be diffed, tested, and rolled back independently of the application. Second, time is treated as a first-class, jurisdiction-aware domain — every timestamp carries an explicit zone, an acclimatization state, and a regulatory classification, because the same wall-clock instant can be legal for one crew member and a violation for another depending on where they are acclimatized. Third, every verdict is reproducible — given the same inputs and the same rule version, the engine must always return an identical result, and that result must be reconstructable months later from the audit log for an inspector.

This section is the map for the rest of the domain. Each subsystem introduced below has a dedicated area of the site that develops its schema, code, and edge cases in depth: the FAA Part 117 rule schema design, the EASA FTL compliance frameworks, the crew duty time taxonomy mapping, and the system security and access boundaries. Upstream of all of them sits the flight data ingestion subsystem, and downstream sits the duty time validation rule engines that consume the normalized, classified events this architecture produces.

Data Model Fundamentals

Every reliable compliance calculation rests on a small set of well-defined entities. The four that matter most are the duty event (a report, block-out, block-in, or release with a UTC timestamp and a station), the duty period (an ordered span of events bounded by rest), the flight duty period (the subset of a duty period that determines FDP limits), and the rest period (the protected interval whose length and quality gate the next duty). Around these sit reference entities: the crew member and their acclimatization state, the aircraft rotation, and the applicable regulatory rule set keyed by effective date and jurisdiction.

Temporal precision is the axis on which correctness turns. Regulations are written in local time — a report at 06:00 has a different FDP limit than one at 17:00 under §117.13 — but no two stations share a clock, and daylight saving transitions move the boundaries twice a year. The only defensible approach is to store every instant in UTC with second precision and to carry the originating zone as explicit metadata, deriving local wall-clock time only at the moment a rule needs it. A timestamp that arrives without a zone is not “probably local”; it is a validation failure and must be rejected at the ingestion boundary rather than silently coerced.

from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from zoneinfo import ZoneInfo


class DutyEventKind(Enum):
    REPORT = "report"
    BLOCK_OUT = "block_out"
    BLOCK_IN = "block_in"
    RELEASE = "release"


@dataclass(frozen=True, slots=True)
class DutyEvent:
    kind: DutyEventKind
    instant_utc: datetime          # always timezone-aware, UTC
    station: str                   # ICAO code, e.g. "KJFK"
    station_zone: str              # IANA zone, e.g. "America/New_York"

    def __post_init__(self) -> None:
        if self.instant_utc.tzinfo is not timezone.utc:
            raise ValueError("instant_utc must be timezone-aware UTC")
        # Validate the zone eagerly so a bad feed fails at the boundary.
        ZoneInfo(self.station_zone)

    def local(self) -> datetime:
        """Wall-clock time at the station, derived on demand."""
        return self.instant_utc.astimezone(ZoneInfo(self.station_zone))

The frozen=True and slots=True combination makes each event immutable and cheap, so the same object can flow through ingestion, classification, and evaluation without any stage mutating a field another stage relies on. Deriving local time on demand — rather than storing it — guarantees there is exactly one source of truth for the instant and eliminates the class of bug where a stored local time and its UTC counterpart drift out of agreement across a DST boundary. The full entity-relationship model, including how these events aggregate into duty periods and how rest quality is scored, is developed in the crew duty time taxonomy mapping.

Regulatory Framework Integration

The architecture must map two major regulatory regimes onto the same core model without letting either leak into the other. FAA 14 CFR Part 117 governs domestic and flag operations for U.S. certificate holders; EASA CS-FTL.1 and the ORO.FTL rules in Regulation (EU) No 965/2012 govern European operators. They share a conceptual vocabulary — report time, FDP, rest, cumulative caps — but differ sharply in the numbers and in how acclimatization is determined.

Under Part 117, the unaugmented FDP limit comes from the Table B lookup in §117.13, keyed on the scheduled report time in the crew member’s acclimatized time and the number of flight segments; §117.11 caps flight time at 8 or 9 hours depending on report time; §117.19 permits limited extensions; and §117.23 imposes the cumulative limits — 100 flight hours in any 672 consecutive hours, 60 FDP hours in any 168 consecutive hours, and 190 FDP hours in any 672 consecutive hours. Rest under §117.25 requires a minimum of 10 hours with an opportunity for 8 uninterrupted hours of sleep, plus 30 consecutive hours free from duty in any 168 consecutive hours. The precise schema for encoding these tables and windows is the subject of the FAA Part 117 rule schema design.

EASA expresses the equivalent constraints differently: the basic maximum daily FDP tables live in CS FTL.1.205, cumulative duty is capped at 60 hours in 7 consecutive days, 110 hours in 14 consecutive days, and 190 hours in any 28 consecutive days, while flight time is limited to 100 hours in any 28 consecutive days and 1000 hours in any 12 consecutive calendar months. Crucially, acclimatization is resolved through the CS FTL.1.235 tables that depend on elapsed time and time-zone difference since the crew member left their reference time — a stateful calculation with no direct Part 117 analogue. Modeling those state transitions is the focus of the EASA FTL compliance frameworks.

The integration principle is strict separation with a shared interface. Both regimes are compiled into rule sets that expose the same evaluation contract — given a classified duty period and a crew history, return a verdict — so the rest of the system never branches on jurisdiction. Jurisdiction is a property of the rule set that is selected at the boundary, not an if statement scattered through the scheduler.

FAA Part 117 vs EASA CS-FTL.1 limit comparison A five-row matrix comparing FAA 14 CFR Part 117 and EASA CS-FTL.1 across daily flight-duty-period basis, flight-time cap, cumulative duty windows, minimum rest, and acclimatization method. Both regimes share the same concepts but express them with different numbers and, for EASA, a stateful acclimatization calculation. FAA — 14 CFR Part 117 EASA — CS-FTL.1 / ORO.FTL Daily FDP basis Flight-time cap Cumulative duty Minimum rest Acclimatization Table B: report time × segments (§117.13) 8–9 h flight/duty; 100 h in any 672 h 60 FDP h/168 h; 190 FDP h/672 h 10 h (8 h sleep opp.); 30 h off/168 h Acclimatized report time (stateless) CS FTL.1.205 tables: report time × sectors 100 h/28 d; 900 h/12 mo; 1000 h/yr 60 h/7 d; 110 h/14 d; 190 h/28 d 12 h at base / 10 h down-route CS FTL.1.235 state: Δ time-zone × elapsed
Shared vocabulary, different numbers: both regimes are compiled into rule sets exposing one evaluation contract, so the rest of the system never branches on jurisdiction.

Python Implementation Patterns

Three patterns recur across every subsystem: typed models at the boundary, a state machine for duty progression, and asynchronous evaluation for throughput. Typed models enforce invariants at construction so that malformed data cannot propagate. Pydantic is the pragmatic choice at the ingestion edge because it validates and coerces external input, while frozen dataclasses are preferable deeper in the core where inputs are already trusted and immutability matters more than coercion.

Duty progression is naturally a state machine: a crew member moves from RESTED to REPORTED to ON_DUTY to RELEASED, and each transition triggers a recalculation of remaining FDP and cumulative headroom. Encoding this explicitly prevents the invalid combinations — an active FDP with no report event, a release before block-in — that are a leading source of miscalculated limits.

from enum import Enum


class DutyState(Enum):
    RESTED = "rested"
    REPORTED = "reported"
    ON_DUTY = "on_duty"
    RELEASED = "released"


_ALLOWED = {
    DutyState.RESTED: {DutyState.REPORTED},
    DutyState.REPORTED: {DutyState.ON_DUTY, DutyState.RELEASED},
    DutyState.ON_DUTY: {DutyState.RELEASED},
    DutyState.RELEASED: {DutyState.RESTED},
}


def transition(current: DutyState, target: DutyState) -> DutyState:
    if target not in _ALLOWED[current]:
        raise ValueError(f"illegal duty transition {current.value} -> {target.value}")
    return target

Because a fleet-wide validation sweep evaluates thousands of pairings against slow upstream systems, evaluation should be asynchronous and bounded. An asyncio task group with a semaphore lets the engine fan out over many crew histories without exhausting database connections, and it keeps a single slow rule set from stalling the whole sweep. The state machine, the typed models, and the evaluation loop together form the contract that the duty time validation rule engines build upon.

import asyncio


async def evaluate_all(pairings, evaluate, *, max_concurrency: int = 16):
    sem = asyncio.Semaphore(max_concurrency)

    async def _one(pairing):
        async with sem:
            return await evaluate(pairing)

    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(_one(p)) for p in pairings]
    return [t.result() for t in tasks]

Compliance Validation Strategy

Testing a compliance engine against a handful of hand-picked rosters proves almost nothing, because the violations that matter live at the boundaries. The discipline that works is property-based testing: generate large populations of synthetic duty histories and assert invariants that must hold for every one of them. With hypothesis, a strategy that emits sequences of duty events lets the test suite discover the midnight crossings, back-to-back FDPs, and cumulative-window boundaries that a human author would never enumerate.

from hypothesis import given, strategies as st


@given(
    report_minutes=st.integers(min_value=0, max_value=24 * 60 - 1),
    segments=st.integers(min_value=1, max_value=7),
)
def test_fdp_limit_is_monotonic_in_segments(report_minutes, segments):
    limit_fewer = fdp_limit(report_minutes, segments)
    limit_more = fdp_limit(report_minutes, min(segments + 1, 7))
    # More segments can never raise the FDP limit under Table B.
    assert limit_more <= limit_fewer

Three edge-case families deserve dedicated regression suites. Midnight and date-line crossings break naive local-time arithmetic; the fix is to compute every duration in UTC and derive local time only for table lookups. Daylight saving transitions create a 23-hour and a 25-hour day, so a rest period that looks like 10 hours on the wall clock can be 9 real hours — always measure rest in elapsed UTC. Leap days and the exact rolling-window boundaries matter because §117.23 and the EASA 28-day caps are defined on any consecutive window, not calendar months, so the aggregation must slide continuously. Beyond generated cases, a regulatory regression suite pins the engine’s output against a matrix of manually verified reference scenarios, so that any rule-set change that alters a known-good verdict fails the build and forces an explicit review.

Security and Audit Requirements

A compliance system holds crew personal data, operational schedules, and the audit trail an inspector will demand, which makes it a high-value target and a regulated data store in its own right. Access must be governed by role — a scheduler may propose pairings, a compliance officer may approve documented exceptions, and an auditor may read everything and change nothing — and every override must be attributed. The system security and access boundaries area develops the full role model and its enforcement points.

The audit log itself is the load-bearing artifact. Every scheduling override, manual adjustment, and regulatory exception must be recorded with the operator identity, a UTC timestamp, the prior value, the new value, and a justification — and the record must be tamper-evident. A hash chain, where each entry commits to the hash of its predecessor, makes any retroactive edit detectable because it breaks every downstream digest.

import hashlib
import json


def append_audit(chain_tip: str, record: dict) -> tuple[str, str]:
    """Return (entry_hash, serialized) for an append-only, hash-chained log."""
    payload = json.dumps(record, sort_keys=True, separators=(",", ":"))
    entry_hash = hashlib.sha256((chain_tip + payload).encode("utf-8")).hexdigest()
    return entry_hash, payload

Pairing this hash chain with append-only storage and structured logging yields compliance-grade telemetry: the verdict for any historical operation can be reconstructed, the exact rule version that produced it identified, and any tampering surfaced by re-walking the chain.

Hash-chained, tamper-evident audit log Three sequential audit entries, each recording operator, UTC timestamp, prior value, new value, and a SHA-256 digest computed over the previous entry's hash plus the current payload. Because every entry commits to its predecessor's digest, editing any field re-computes that entry's hash and breaks every downstream digest, making tampering detectable. Entry n−1 operator: sched_07 utc: 2026-07-03T08:41Z prior: FDP 09:20 new: FDP 09:40 sha256 = 8c2f…a11 = hash(genesis + payload) Entry n operator: cmpl_02 utc: 2026-07-03T09:12Z prior: FDP 09:40 new: FDP 10:15 sha256 = 4f9a…c1e = hash(8c2f…a11 + payload) Entry n+1 operator: audit_ro utc: 2026-07-03T09:55Z prior: FDP 10:15 new: exception noted sha256 = b307…e92 = hash(4f9a…c1e + payload) Edit any field → that entry's digest changes → every later digest breaks. Re-walking the chain surfaces the tampering.
A hash chain makes the audit log tamper-evident: each entry commits to its predecessor's SHA-256 digest, so any retroactive edit invalidates every downstream hash.

Graceful Degradation and Fallback Logic

Operational continuity cannot depend on uninterrupted connectivity or a healthy primary rule engine. When an upstream feed goes stale or the evaluation service degrades, the system must keep giving schedulers actionable guidance without ever quoting a limit that is more permissive than the regulation allows. The governing rule is that degradation always moves toward the more restrictive interpretation: an unknown acclimatization state is treated as the worst case, a missing recent-history feed is treated as maximal prior duty, and a rule set that fails to load blocks approval rather than defaulting to “compliant.”

Dependency injection makes this switch clean. The evaluator depends on a rule-set provider interface, and a health check swaps the production provider for a conservative fallback that returns the tightest limits in the table when inputs are incomplete. Schedulers still get a usable, safe answer; the answer is simply pessimistic until full data returns.

from contextlib import contextmanager


@contextmanager
def rule_source(provider, *, healthy: bool):
    """Yield the live provider when healthy, else a conservative fallback."""
    if healthy:
        yield provider
    else:
        yield ConservativeFallback(provider)  # tightest limits, blocks on unknowns

The tuning of the thresholds that decide when to trip into fallback — how stale a feed may be, how much latency is tolerable — is an operational concern developed alongside the duty time validation rule engines, which own the live evaluation path this architecture feeds.

Explore the Core Architecture Domains

Each subsystem introduced above is developed in depth in its own area. Together they form the regulatory-mapping backbone of the site:

Back to Home.

For authoritative reference on current duty time limitations, consult the FAA Part 117 official text and the Python datetime documentation.

Explore this section