Fallback Routing Strategies

A payroll run cannot stop because one record is ambiguous, and it cannot guess its way past one either — both choices end in a Department of Labor finding. Fallback routing strategies, part of the Core Architecture & Compliance Mapping for Payroll Systems framework, are the controlled answer to that tension: a deterministic mechanism that intercepts records whose pay codes survive schema validation but fail semantic mapping, then routes each one down a known, auditable path instead of halting the engine or inventing a tax treatment. The hard requirement is that fallback never infers — it resolves to a statutory default or escalates to a review queue, and it writes an immutable record of which path it took and why.

This pattern lives at the normalization layer, downstream of structural validation and upstream of gross-to-net. It activates only after a record’s shape is known good but its meaning is not: a vendor renamed 401K to RET_PRETAX_V2, a jurisdiction code arrived as a free-text city name, or a garnishment code carries a suffix no canonical dictionary recognizes. Done correctly, fallback routing keeps ten thousand correct paychecks moving while quarantining the one that engineering and compliance must look at by hand.

Deterministic tiered fallback routing A payroll record whose shape is valid but whose meaning is unknown enters from the left and is evaluated by four tiers in order: tier one primary exact match against canonical_map, tier two secondary alias or heuristic match against alias_map, tier three jurisdictional statutory default resolved Municipal then State then Federal, and tier four quarantine escalation to a review queue. A miss at any tier falls through horizontally to the next; the first match routes its decision downward. The jurisdictional and quarantine tiers are flagged requires_review = true. Every tier writes record_id, tier, and resolved_rule into one append-only audit ledger keyed by a SHA-256 hash that is stable across retries. Payroll record shape valid, meaning unknown missmissmiss 1234 Primary exact match canonical_map Secondary alias · heuristic alias_map Jurisdictional statutory default Municipal ▸ State ▸ Federal Quarantine escalation review queue requires_review = true requires_review = true on match, route ↓ Append-only audit ledger SHA-256(record_id · pay_code · tier · resolved_rule) — immutable, replay-stable

Data Normalization & Boundary Enforcement

Fallback routing is only as safe as the boundary that precedes it. A record must already satisfy the canonical contract defined by Data Boundary Definitions before the router ever sees it; otherwise a malformed payload is indistinguishable from an unmapped-but-valid one, and the router will quarantine noise instead of resolving signal. The division of labor is strict: structural validation rejects records that are the wrong shape; fallback routing handles records that are the right shape but the wrong (or unknown) meaning.

Every record entering the router must therefore carry, at minimum:

  • record_id — a stable, deduplicated identifier so a retried run resolves to the same routing decision rather than a new one.
  • pay_code — the raw vendor code, preserved verbatim. Normalization (uppercasing, stripping non-alphanumerics) happens inside the router so the original survives in the audit trail.
  • jurisdiction — a resolved authority key (federal / state / county / municipal), not a free-text work location. An unresolved jurisdiction is itself a quarantine condition, never a silent default to federal.
  • amount — a Decimal, parsed from the source with Decimal(str(value)) so binary float rounding never enters monetary state. A record whose amount arrives as a native float must be rejected at the boundary, not coerced.

Quarantine conditions specific to this topic are explicit and enumerable: a pay_code that matches no tier, a jurisdiction that resolves to no known authority, an amount that breaches a statutory cap, or a benefits/measurement record that cannot be classified without risking ACA Tracking Logic continuity. Each condition routes to the same immutable holding queue, but the audit record names which condition fired so reconciliation is not a guessing exercise. The deduction-specific variant of these conditions — vendor suffixes, truncated codes, pre/post-tax ambiguity — is handled in depth in fallback routing for unclassified deductions.

Jurisdictional Resolution & Effective Dating

When a record falls through to the statutory-default tier, the router must pick the controlling rule, and “controlling” is a function of both place and time. The override hierarchy is most-protective-wins, evaluated municipal first, then state, then federal:

Municipal > State > Federal

A municipal minimum-wage or paid-leave rule supersedes a state rule, which supersedes the federal baseline — but only for the jurisdiction tied to the employee’s primary work location, and only for a rule whose effective window contains the pay period. This is the same precedence the FLSA Threshold Mapping gate applies when it resolves exempt/non-exempt status; fallback routing reuses it so an overtime default selected here can never contradict the threshold the calculation engine will apply later.

Effective dating uses half-open intervals so adjacent windows never both claim a date. A rule is in force for an evaluation date dd when:

startd<end\text{start} \le d < \text{end}

with end = None modeled as ++\infty. Resolution must select against pay_period_start, never datetime.now() — a record processed today for a prior period must bind to the rule that was in force then, or a retroactive correction will silently misclassify. The selection predicate in SQL is the canonical form:

SELECT rule_id
FROM statutory_defaults
WHERE jurisdiction = :jurisdiction
  AND code_prefix  = :code_prefix
  AND effective_start <= :pay_period_start
  AND (effective_end IS NULL OR :pay_period_start < effective_end)
ORDER BY authority_rank DESC   -- municipal=3, state=2, federal=1
LIMIT 1;

Overlap detection is a load-time concern, not a run-time one. Two rule versions that both claim a date — overlapping [start, end) windows for the same (jurisdiction, code_prefix) — indicate configuration drift, and the rule set must fail to load rather than letting the run pick arbitrarily. Detecting overlaps at load time turns a non-deterministic payroll bug into a deploy-time error, which is the only place it is cheap to fix.

Production Implementation Pattern

The router below is deterministic, uses Decimal for all monetary state, emits structured key=value logs that are copy-paste safe for production, and writes a SHA-256 audit hash for every decision. It folds jurisdictional resolution and effective dating into the default tier so a single route() call yields the controlling rule and its provenance. The code is runnable as-is and follows PEP 8.

"""Deterministic fallback router for payroll normalization."""
import hashlib
import logging
from dataclasses import dataclass, field
from datetime import date
from decimal import Decimal
from enum import Enum
from typing import Any, Optional

logger = logging.getLogger("payroll.fallback_router")


class RoutingTier(Enum):
    PRIMARY = "primary_match"
    SECONDARY = "secondary_heuristic"
    JURISDICTIONAL_DEFAULT = "jurisdictional_default"
    QUARANTINE = "quarantine_escalation"


@dataclass(frozen=True)
class StatutoryDefault:
    """An effective-dated default keyed by (jurisdiction, code prefix)."""
    rule_id: str
    jurisdiction: str
    code_prefix: str
    authority_rank: int          # municipal=3, state=2, federal=1
    effective_start: date
    effective_end: Optional[date] = None

    def in_force(self, on: date) -> bool:
        # Half-open window: start <= on < end (end=None => +infinity).
        if on < self.effective_start:
            return False
        return self.effective_end is None or on < self.effective_end


@dataclass(frozen=True)
class PayrollRecord:
    record_id: str
    employee_id: str
    pay_code: str
    jurisdiction: str
    amount: Decimal
    pay_period_start: date
    metadata: dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        # Reject float money at the boundary; never coerce silently.
        if not isinstance(self.amount, Decimal):
            raise TypeError(
                f"amount must be Decimal, got {type(self.amount).__name__}"
            )


@dataclass(frozen=True)
class RoutingResult:
    record_id: str
    tier: RoutingTier
    resolved_rule: str
    audit_hash: str
    requires_review: bool


class FallbackRouter:
    """Routes records through a strict tier hierarchy with audit logging."""

    def __init__(
        self,
        canonical_map: dict[str, str],
        alias_map: dict[str, str],
        statutory_defaults: list[StatutoryDefault],
    ) -> None:
        self.canonical_map = canonical_map
        self.alias_map = alias_map
        self.statutory_defaults = statutory_defaults

    @staticmethod
    def _normalize(pay_code: str) -> str:
        return "".join(c for c in pay_code.upper() if c.isalnum())

    def _audit_hash(
        self, record: PayrollRecord, tier: RoutingTier, rule: str
    ) -> str:
        payload = f"{record.record_id}:{record.pay_code}:{tier.value}:{rule}"
        return hashlib.sha256(payload.encode("utf-8")).hexdigest()

    def _primary(self, record: PayrollRecord) -> Optional[str]:
        return self.canonical_map.get(self._normalize(record.pay_code))

    def _secondary(self, record: PayrollRecord) -> Optional[str]:
        return self.alias_map.get(self._normalize(record.pay_code))

    def _jurisdictional_default(self, record: PayrollRecord) -> Optional[str]:
        prefix = self._normalize(record.pay_code)[:3]
        # Most-protective-wins: municipal > state > federal, in force on the
        # period start date — never on "now".
        candidates = [
            d for d in self.statutory_defaults
            if d.jurisdiction == record.jurisdiction
            and d.code_prefix == prefix
            and d.in_force(record.pay_period_start)
        ]
        if not candidates:
            return None
        return max(candidates, key=lambda d: d.authority_rank).rule_id

    def route(self, record: PayrollRecord) -> RoutingResult:
        tier = RoutingTier.QUARANTINE
        resolved_rule = "COMPLIANCE_REVIEW_REQUIRED"

        primary = self._primary(record)
        if primary is not None:
            tier, resolved_rule = RoutingTier.PRIMARY, primary
        elif (secondary := self._secondary(record)) is not None:
            tier, resolved_rule = RoutingTier.SECONDARY, secondary
        elif (default := self._jurisdictional_default(record)) is not None:
            tier, resolved_rule = RoutingTier.JURISDICTIONAL_DEFAULT, default

        requires_review = tier in (
            RoutingTier.JURISDICTIONAL_DEFAULT,
            RoutingTier.QUARANTINE,
        )
        audit_hash = self._audit_hash(record, tier, resolved_rule)

        logger.info(
            "event=fallback_routing record=%s emp=%s tier=%s rule=%s "
            "review=%s hash=%s",
            record.record_id,
            record.employee_id,
            tier.value,
            resolved_rule,
            requires_review,
            audit_hash,
        )
        return RoutingResult(
            record_id=record.record_id,
            tier=tier,
            resolved_rule=resolved_rule,
            audit_hash=audit_hash,
            requires_review=requires_review,
        )

Three properties make this safe in production. The amount is a Decimal and the constructor rejects anything else, so monetary state is exact from the boundary inward. The default tier resolves against pay_period_start and ranks by authority, so the Municipal > State > Federal hierarchy and effective dating are enforced in one place. And the audit hash is a pure function of (record_id, pay_code, tier, rule), so a retried run produces an identical hash — the determinism that downstream reconciliation depends on.

Compliance Verification & Fallback Routing

Deploying the router without a verification suite turns a safety valve back into a silent failure mode. The following checklist is the minimum gate set; run it in CI and against a daily reconciliation job.

  1. Tier ordering tests. Assert that a pay_code present in canonical_map always returns PRIMARY and never reaches a lower tier; that JURISDICTIONAL_DEFAULT fires only when both primary and secondary return None; and that an unmatched code lands in QUARANTINE with requires_review=True.
  2. Effective-date drift tests. Resolve the same (jurisdiction, code_prefix) for a date inside, on, and one day outside each window. Confirm half-open behavior — the effective_start date resolves, the effective_end date does not — and that selection binds to pay_period_start, not the run date.
  3. Override-hierarchy tests. With a municipal, state, and federal default all in force for one date, assert the router returns the municipal rule_id. Remove the municipal rule and assert it falls back to state, then to federal.
  4. Overlap detection at load. Feed the loader two defaults with overlapping windows for the same key and assert it raises rather than accepting them. This keeps a non-deterministic run-time bug out of production entirely.
  5. Decimal precision checks. Assert that constructing a PayrollRecord with a float amount raises TypeError, and that no code path coerces money through float(). Reconcile a synthetic batch to the cent.
  6. Audit determinism. Route an identical record twice and assert the audit_hash values match. Route all logs to a write-once store (e.g. S3 Object Lock) and map each audit_hash to NIST SP 800-53 AU-2 audit-event evidence.
  7. Quarantine SLA. Run a daily aggregation asserting no record sits in QUARANTINE beyond 48 hours without a documented exception, so escalations cannot rot into the next pay period.

Failure Modes & Gotchas

  • Quarantine used as a mapping shortcut. Engineers under deadline pressure route a whole vendor’s unmapped codes to quarantine and “clear them later,” and the queue grows until a DOL audit finds wages misclassified for months. Root cause: treating fallback as overflow rather than exception handling. Fix: the 48-hour quarantine SLA plus an alert when daily quarantine volume exceeds a baseline — a spike means a vendor schema changed and the alias map, not the queue, needs the update.
  • Silent default to federal on a missing jurisdiction. A blank jurisdiction is treated as “use federal,” which underwithholds in a state with a higher floor. Root cause: defaulting an unresolved key instead of quarantining it. Fix: an unresolved jurisdiction is a quarantine condition; the statutory-default tier only runs for a resolved authority.
  • Float money entering through JSON. A vendor amount parsed as a native float reintroduces binary rounding, and the run no longer reconciles to the cent. Root cause: json.loads without parse_float=Decimal. Fix: parse with parse_float=Decimal, and let the PayrollRecord constructor reject any non-Decimal amount at the boundary.
  • Resolving defaults against now(). A record reprocessed after a statute change binds to today’s rule instead of the rule in force during the pay period, silently rewriting history on a retroactive correction. Root cause: using the run clock instead of pay_period_start. Fix: effective-date selection takes the period start date as an explicit argument; the run clock never touches rule resolution.
  • Overlapping effective windows. Two rule versions both claim a date and the router picks arbitrarily between runs, so the same record resolves differently on a retry. Root cause: unvalidated configuration. Fix: detect overlapping half-open windows at rule-load time and fail the load, not the run.

Frequently Asked Questions

When should a record be quarantined versus routed to a statutory default?

Route to a statutory default only when the record’s jurisdiction is resolved and an effective-dated rule exists for its code prefix — that is a known, defensible treatment. Quarantine whenever resolution would require a guess: an unresolved jurisdiction, a code that matches no tier, an amount that breaches a cap, or a benefits/measurement record that could distort eligibility tracking. The litmus test is whether you could defend the routing decision to an auditor without inferring intent. If not, it belongs in the review queue.

Does fallback routing ever override an explicit employee election or a CBA term?

No. Statutory defaults are a floor for unresolved records, not a ceiling that replaces resolved ones. An explicit employee election (e.g. a chosen pre-tax deduction) or a collective-bargaining-agreement term is itself a primary match and never reaches the default tier. Verify this with a boundary test that injects an election alongside an unknown code and asserts the election resolves at PRIMARY while only the unknown code reaches the default tier.

How do I keep audit hashes stable across pipeline retries?

Derive the hash from a fixed tuple — (record_id, pay_code, tier, rule) — and nothing time-varying. Timestamps, run IDs, and worker hostnames must live in surrounding log fields, never in the hash payload, so a replayed record produces an identical hash. That stability is what lets reconciliation prove a retry did not change a routing decision. See the Python logging documentation for configuring structured formatters and rotation that meet retention requirements.

Why use half-open date windows instead of inclusive ranges?

Inclusive ranges (start <= d <= end) let adjacent windows both claim the boundary date, so a rule change effective the 15th can match both the old and new version on the 15th. Half-open windows (start <= d < end) make every date belong to exactly one window, which removes an entire class of overlap bugs. The convention must be consistent with how pay-period intervals are stored so a day is never double-counted across periods.

How does this relate to deduction-specific fallback routing?

This page covers the general tier hierarchy, jurisdictional defaults, and audit contract that apply to any unmapped pay code. Deductions add their own failure surface — vendor suffixes, truncated codes, pre-tax versus post-tax ambiguity, and garnishment caps — which the deduction-specific guide handles with similarity matching and threshold-guarded routing on top of this same hierarchy.