Building a Fallback Routing System for Patent Dockets

A fallback routing system for patent dockets is a deterministic escalation pipeline that reroutes a docket event to a predefined backup tier the moment a primary route emits an explicit failure signal — never a heuristic timeout — so that statutory response windows keep advancing even while the delivery path is degraded.

When primary docket routing fails, the office action clock, the maintenance-fee window, and the PCT National Phase Entry Rules 30/31-month period do not pause. A missed handoff therefore converts an infrastructure hiccup into abandonment risk and malpractice exposure. This page specifies the exact failure signals that must trigger a reroute, a minimal reproducible router in Python, the jurisdiction-aware deadline recomputation that has to survive the outage, and the audit procedures that keep the reroute legally defensible. It is the degradation path referenced by the parent Security & Access Control Boundaries module.

Technical Specification: Deterministic Triggers and the Statutory Backstop

Fallback routing must activate on explicit, measurable signals rather than subjective queue observations. A reroute is a compliance event, so its trigger conditions must be enumerated and version-pinned exactly like a deadline rule. The following signals are the mandatory activation contract for patent docket automation:

Failure Mode Detection Signal Patent-Specific Impact
Primary API degradation HTTP 5xx, 429 rate limits, or TLS handshake > 3s USPTO Patent Center / EPO Register sync stalls; office-action response windows drift
Schema drift Missing mandatory fields (application_number, filing_date, event_code) National phase events misclassified; wrong jurisdiction calendar applied
Permission denial 401/403 on the assignment table or routing-matrix lookup Docket stranded in the unassigned queue; paralegal SLA breach
Queue saturation Redis/RabbitMQ backlog over threshold, or consumer lag > 60s High-volume batches (e.g. post-RCE filings) drop silently

The statutory backstop is what makes determinism non-negotiable. A rerouted deadline must still satisfy the same governing rules the primary path would have applied: the weekend/holiday forward shift under 37 CFR § 1.7 for the USPTO, Rule 134(1) EPC for the EPO, and PCT Rule 80.5 for periods that expire on a day the receiving office is closed. Fallback activation must remain stateless and idempotent: each trigger routes to a predefined escalation tier without mutating the original docket payload until the downstream system confirms a successful handoff. That preserves the integrity of the Core Docketing Architecture & Deadline Taxonomy ledger while isolating transient faults from statutory obligations.

Escalation Chain and Circuit-Breaker Contract

The routing pipeline is a directed acyclic graph with explicit circuit breakers. Primary routing targets the assigned prosecution team; secondary routing targets the practice-group lead; tertiary routing targets the firm-wide docketing supervisor; and a quaternary tier triggers manual intervention with immutable audit logging. A docket event may only ever move forward through this chain, never in a loop, so a single event cannot ping-pong between two degraded endpoints.

Circuit breakers stop cascading failure by halting retries to a degraded endpoint after consecutive threshold breaches. The breaker opens after three consecutive failures, diverts traffic to the next tier, and enters a half-open probe state after a configurable cool-down. This prevents a recovering endpoint from being overwhelmed while every cross-tier handoff continues to honor the access rules defined in the Security & Access Control Boundaries module — an escalation target inherits the original docket’s classification tags and privilege markers rather than acquiring broad reach.

Tiered fallback routing chain with per-tier circuit breakers and a hash-chained audit ledger A docket event carrying an immutable payload enters the ordered escalation chain. Tier 1 (PRIMARY, the assigned prosecution team) is guarded by a circuit breaker; after three consecutive failures the breaker opens and the event is diverted forward to Tier 2 (SECONDARY, the practice-group lead), whose breaker also opens and diverts to Tier 3 (TERTIARY, the docketing supervisor), shown half-open while it sends a single probe. If every automated tier is exhausted the event lands fail-closed in Tier 4 (QUATERNARY), the manual-escalation queue with a human SLA. The chain is directed and acyclic — an event only ever moves forward, never loops. Every tier decision emits one entry into an append-only audit ledger on the right, and each entry carries a SHA-256 digest chained to the previous entry's hash. A state-machine legend at the bottom shows the breaker cycle: CLOSED (traffic flows) opens after three consecutive failures to OPEN (diverts to the next tier), then after a cool-down moves to HALF-OPEN (single probe), and a successful probe resets it to CLOSED. Docket event enters the escalation chain immutable payload · payload_hash pinned APPEND-ONLY AUDIT LEDGER SHA-256, hash-chained 1 TIER 1 · PRIMARY Assigned prosecution team primary route OPEN 2 TIER 2 · SECONDARY Practice-group lead first escalation OPEN 3 TIER 3 · TERTIARY Docketing supervisor second escalation HALF-OPEN 4 TIER 4 · QUATERNARY Manual-escalation queue human SLA · fail-closed backstop FAIL-CLOSED 3× fail → divert 3× fail → divert if exhausted → manual TIER: PRIMARY status: FAILURE · breaker→open sha256 3f9a… ⟵ prev TIER: SECONDARY status: FAILURE · breaker→open sha256 b71c… ⟵ 3f9a… TIER: TERTIARY status: PROBE · half-open sha256 e0d4… ⟵ b71c… MANUAL_ESCALATION_REQUIRED status: queued · SLA started sha256 aa27… ⟵ e0d4… emit emit emit emit prev_hash prev_hash prev_hash PER-TIER CIRCUIT-BREAKER STATE MACHINE CLOSED traffic flows OPEN diverts to next tier HALF-OPEN single probe 3× fail cool-down probe returns 200 → reset to CLOSED (probe fails → reopen)

Minimal Reproducible Implementation

The router below walks the endpoint chain in order, guards each endpoint with an independent circuit breaker, applies bounded retry with exponential backoff via tenacity, and writes a structured audit entry for every attempt. It never mutates the payload, and it returns False only after every tier — including the manual-escalation record — has been exhausted.

import asyncio
import hashlib
import logging
import time
from datetime import datetime, timezone
from typing import Any
from dataclasses import dataclass
from tenacity import (
    retry, stop_after_attempt, wait_exponential, retry_if_exception_type,
)
import httpx

logging.basicConfig(
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
    level=logging.INFO,
)
logger = logging.getLogger("patent_docket_fallback")


@dataclass
class CircuitBreaker:
    """One breaker per endpoint. Opens after `failure_threshold`
    consecutive failures, then half-opens after `recovery_timeout`."""
    failure_threshold: int = 3
    recovery_timeout: float = 30.0
    failures: int = 0
    last_failure_time: float = 0.0
    state: str = "closed"  # closed | open | half-open

    def record_failure(self) -> None:
        self.failures += 1
        self.last_failure_time = time.monotonic()
        if self.failures >= self.failure_threshold:
            self.state = "open"
            logger.warning("Circuit breaker opened after %d failures", self.failures)

    def reset(self) -> None:
        self.failures = 0
        self.state = "closed"

    def allow_request(self) -> bool:
        if self.state == "closed":
            return True
        # Cool-down elapsed → allow one probe (half-open).
        if self.state == "open" and (time.monotonic() - self.last_failure_time) > self.recovery_timeout:
            self.state = "half-open"
            return True
        return False


class FallbackRouter:
    def __init__(self, endpoints: list[str]) -> None:
        # `endpoints` is the ordered escalation chain: primary → quaternary.
        self.endpoints = endpoints
        self.circuit_breakers = {url: CircuitBreaker() for url in endpoints}
        self.client = httpx.AsyncClient(timeout=3.0, follow_redirects=False)
        self.audit_log: list[dict[str, Any]] = []

    def _payload_hash(self, payload: dict[str, Any]) -> str:
        # Deterministic digest — not Python's salted hash() — so the same
        # docket event always yields the same audit fingerprint.
        canonical = str(sorted(payload.items()))
        return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

    async def _route_with_retry(self, url: str, payload: dict[str, Any]) -> bool:
        breaker = self.circuit_breakers[url]
        if not breaker.allow_request():
            logger.info("Circuit breaker open for %s; skipping tier.", url)
            return False

        @retry(
            stop=stop_after_attempt(2),
            wait=wait_exponential(multiplier=0.5, min=0.5, max=2),
            retry=retry_if_exception_type((httpx.HTTPStatusError, httpx.TimeoutException)),
            reraise=True,
        )
        async def _attempt_delivery() -> bool:
            response = await self.client.post(url, json=payload)
            response.raise_for_status()
            return response.status_code == 200

        try:
            success = await _attempt_delivery()
            breaker.reset()
            self._log_audit(url, payload, "SUCCESS", success)
            return success
        except Exception as exc:  # noqa: BLE001 — record every failure class
            breaker.record_failure()
            self._log_audit(url, payload, "FAILURE", False, str(exc))
            return False

    def _log_audit(self, target: str, payload: dict[str, Any], status: str,
                   success: bool, error: str | None = None) -> None:
        entry = {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "target_endpoint": target,
            "payload_hash": self._payload_hash(payload),
            "status": status,
            "success": success,
            "error": error,
        }
        self.audit_log.append(entry)
        logger.info("Audit: %s", entry)

    async def route_docket(self, payload: dict[str, Any]) -> bool:
        for url in self.endpoints:
            logger.info("Attempting routing to %s", url)
            if await self._route_with_retry(url, payload):
                return True
        # Every tier exhausted → the payload MUST land in a human queue.
        logger.critical("All tiers exhausted for %s", self._payload_hash(payload))
        self._log_audit("QUATERNARY_FALLBACK", payload, "MANUAL_ESCALATION_REQUIRED", False)
        return False

Operational Action: Store the ordered endpoints chain and the breaker thresholds in the same version-pinned rule registry as your deadline rules, and gate every change through review by patent counsel — a routing-tier change silently alters who is legally accountable for a docket.

Jurisdiction-Aware Deadline Recomputation

A reroute must never bypass calendar validation. When an endpoint fails and the event is re-dispatched, the receiving tier recomputes the absolute deadline from the originating jurisdiction rather than trusting a value that may have been produced against a stale calendar. Resolve the office from filing_country or event_code, apply the correct zoneinfo offset and statutory holiday set, then walk business days forward — looping so that rolling off a Sunday onto a holiday Monday shifts again.

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import holidays

def calculate_statutory_deadline(filing_date: str, days_offset: int, jurisdiction: str) -> str:
    """Return an ISO-8601 deadline shifted over weekends and office closures.

    `holidays` covers US federal closures (37 CFR 1.7). For the EPO,
    substitute the curated closure list from the EPO Official Journal
    (Rule 134(1) EPC) — the generic DE set below is a placeholder only.
    """
    # Bound the holiday lookup to the years the deadline can land in.
    base_year = datetime.fromisoformat(filing_date).year
    span = [base_year, base_year + 1, base_year + 2]

    if jurisdiction == "US":
        tz, cal = ZoneInfo("America/New_York"), holidays.US(years=span)
    else:  # EPO / European deadlines
        tz, cal = ZoneInfo("Europe/Brussels"), holidays.country_holidays("DE", years=span)

    current = datetime.fromisoformat(filing_date).replace(tzinfo=tz)
    added = 0
    while added < days_offset:
        current += timedelta(days=1)
        if current.weekday() < 5 and current.date() not in cal:  # 5=Sat, 6=Sun
            added += 1

    # Re-test the final landing day: a forward shift can hit another closure.
    while current.weekday() >= 5 or current.date() in cal:
        current += timedelta(days=1)

    return current.replace(hour=23, minute=59, second=59).isoformat()

Because this recomputation runs on the fallback path, it keeps a rerouted deadline legally defensible even while primary routing is degraded. For production EPO figures, replace the placeholder holiday map with the annual closure schedule published in the EPO Official Journal, and treat that calendar as version-pinned input, not a constant.

Known Gotchas and Compliance Traps

  • Failing open instead of closed. The single most dangerous defect is a router that, on total exhaustion, silently drops the event or marks it delivered. It must instead emit the MANUAL_ESCALATION_REQUIRED audit record and land the payload in a human queue with an explicit SLA — a dropped office-action reroute is indistinguishable from a missed deadline after the fact.
  • Payload mutation before confirmed handoff. If a tier rewrites the docket payload (normalizing dates, stamping a new owner) before the downstream endpoint confirms receipt, a subsequent reroute carries corrupted state and the payload_hash no longer matches the original event. Keep the payload immutable until a 200 is returned, then let the receiving system persist its own derived record.
  • Retrying past a 429. A backoff loop that keeps hammering the USPTO or EPO after a rate-limit response is both a reliability bug and a terms-of-service violation. The breaker must count 429 as a failure and open the tier; reuse the disciplined exponential backoff patterns for patent APIs rather than a tight retry.
  • Schema-drift reroute onto the wrong calendar. When an office renames or drops a field the router keys on (for example filing_country), naive code may default to a US calendar and misclassify a European deadline. Treat a missing mandatory attribute as a hard failure that opens the breaker and escalates, never a best-effort guess — the same fail-loud discipline the USPTO Data Schema Mapping layer enforces at ingestion.

Immutable Audit Trail and Access Preservation

Every fallback activation must generate an immutable chain-of-custody record, because a malpractice reviewer will reconstruct the reroute after the fact. Four properties are mandatory. Each audit entry carries a SHA-256 payload digest so post-incident tampering claims can be refuted. Entries are written to append-only, write-once storage or a hash-chained ledger, never edited in place. Every tier transition (PRIMARY → SECONDARY → TERTIARY → MANUAL) is logged with a precise UTC timestamp and the breaker state at the moment of the decision. And the reroute never bypasses access control: escalation targets inherit the original docket’s classification tags and attorney-client privilege markers. This is the same append-only trail the Core Docketing Architecture & Deadline Taxonomy ledger uses for computed deadlines, so routing decisions and deadline computations reconcile against one tamper-evident record.

Integration Point

This router is one node in the docketing pipeline, not a standalone service. Upstream, docket events arrive already normalized from portal synchronization — the EPO Register Sync Architecture and WIPO PATENTSCOPE Integration feeds supply the event_code and jurisdiction attributes the router reads. Downstream, a successful handoff feeds the reminder-dispatch layer, while every payload_hash is written to the audit trail so a compliance dashboard can replay exactly which tier delivered a given event. The recomputation step defers its month arithmetic to the Automated Deadline Calculation & Rule Engines module, and who may read or override a rerouted deadline is governed by the parent Security & Access Control Boundaries module.

When a reroute is reported, the recovery sequence is deterministic: run lightweight GET /health probes against the primary endpoint every 15 seconds and require two consecutive 200s before resuming; compare consumer lag against the pre-failure baseline and confirm zero message duplication; cross-reference the fallback audit log against the primary docket database and flag any MANUAL_ESCALATION_REQUIRED entry for paralegal review within two hours; and transition the breaker back to closed only after synthetic-transaction testing confirms stability. Never patch a historical audit record in place — append a correction entry with explicit versioning.

Frequently Asked Questions

Should the fallback router fail open or fail closed when every tier is exhausted?
Fail closed. On total exhaustion the router must emit a MANUAL_ESCALATION_REQUIRED audit record and place the untouched payload in a human queue with an explicit SLA, never mark the event delivered or drop it. A silently dropped reroute is indistinguishable from a missed statutory deadline once the response window lapses.
Why use explicit failure signals instead of a simple timeout to trigger fallback?
Timeouts are ambiguous — a slow response and a dead endpoint look identical, and a naive timeout retries under load. Keying the reroute to explicit signals (HTTP 5xx/429, TLS handshake over 3s, missing mandatory fields, consumer lag over 60s) makes activation deterministic and auditable, which is what a malpractice review requires when reconstructing why a docket was rerouted.
Does rerouting a docket change the statutory deadline?
No. The deadline is fixed by statute regardless of delivery path. The fallback tier recomputes the absolute date from the originating jurisdiction — applying the 37 CFR 1.7 or Rule 134(1) EPC weekend/holiday forward shift and PCT Rule 80.5 — so a rerouted deadline is identical to the one the primary path would have produced, and the recomputation is logged.
How does fallback routing avoid violating patent-office rate limits?
The circuit breaker counts an HTTP 429 as a failure and opens the tier rather than retrying into the limit. Backoff uses bounded exponential waits, and machine clients respect each portal's published fair-use policy. Retrying past a 429 is treated as a defect because it is both a reliability bug and a terms-of-service violation of the USPTO and EPO APIs.

↑ Back to Security & Access Control Boundaries