← Back to Blog

The Policy Execution Pipeline: Phases, Failure Modes, and Short-Circuits

A request enters the gateway. Policies execute. A response exits.

What happens in between? Hexarch models this as a pipeline with distinct phases, ordered execution, and explicit failure semantics.

The Pipeline Phases

The PolicyPhase enum defines three stages:

export enum PolicyPhase {
  PRE_REQUEST = 'Pre-Request',
  POST_REQUEST = 'Post-Request',
  ERROR = 'Error Handling'
}

Pre-Request: Security checks, rate limiting, request transformation. If a policy denies here, the request never reaches the backend.

Post-Request: Response transformation, PII masking, caching. The backend has responded; these policies modify what the client sees.

Error: Terminal error handling. When something goes wrong, these policies decide what the client sees.

The Policy Interface

Each policy specifies its position in the pipeline:

interface Policy {
  id: string;
  name: string;
  type: PolicyType;
  description: string;
  enabled: boolean;
  scope: PolicyScope;

  // Pipeline position
  phase: PolicyPhase;
  order: number;

  // Failure behavior
  failureMode: FailureMode;
  shortCircuit: boolean;

  // Scope binding
  targetRef?: PolicyTargetRef;

  // Type-specific settings
  config: any;
}

The order field determines execution sequence within a phase. Policy 1 runs before policy 2.

Failure Modes

The FailureMode enum defines what happens when a policy can’t execute:

export enum FailureMode {
  FAIL_OPEN = 'Fail Open',
  FAIL_CLOSED = 'Fail Closed'
}

Fail-Open: The policy is advisory. If it can’t run (timeout, exception), the request proceeds without it. Use for caching, analytics, non-critical transformations.

Fail-Closed: The policy is mandatory. If it can’t run, the request fails. Use for authentication, authorization, compliance controls.

The default for security policies is always FAIL_CLOSED. If the auth check can’t run, you don’t assume the request is authorized.

Short-Circuit Behavior

The shortCircuit flag enables early exit:

interface Policy {
  shortCircuit: boolean;  // If true, stop processing on match
}

Example: Edge caching policy with shortCircuit: true. If the cache has the response, return it immediately—don’t execute remaining policies or call the backend.

In the UI, short-circuit policies show a special badge:

{policy.shortCircuit && (
  <Badge variant="warning">Short-circuits</Badge>
)}

The ExecutionFlowView

The Policies.tsx component includes an ExecutionFlowView that visualizes the pipeline:

function ExecutionFlowView({ policies }: { policies: Policy[] }) {
  const policiesByPhase = useMemo(() => ({
    PRE_REQUEST: policies
      .filter(p => p.phase === 'PRE_REQUEST')
      .sort((a, b) => a.order - b.order),
    POST_REQUEST: policies
      .filter(p => p.phase === 'POST_REQUEST')
      .sort((a, b) => a.order - b.order),
    ERROR: policies
      .filter(p => p.phase === 'ERROR')
      .sort((a, b) => a.order - b.order)
  }), [policies]);

  return (
    <div className="execution-flow">
      <Stage label="Inbound" />
      <PhaseColumn phase="PRE_REQUEST" policies={policiesByPhase.PRE_REQUEST} />
      <Stage label="Target" />
      <PhaseColumn phase="POST_REQUEST" policies={policiesByPhase.POST_REQUEST} />
      <Stage label="Outbound" />
      <PhaseColumn phase="ERROR" policies={policiesByPhase.ERROR} />
    </div>
  );
}

The visual representation: Inbound → Pre-Request → Target → Post-Request → Outbound, with Error handling as a separate branch.

Policy Types

The PolicyType enum categorizes behavior:

export enum PolicyType {
  SECURITY = 'Security',
  TRANSFORMATION = 'Transformation',
  TRAFFIC = 'Traffic Control',
  MEDIATION = 'Protocol Mediation',
  CUSTOM = 'Custom Extension'
}

In the current dashboard demo, policy cards also pick type-specific icons/classes:

<div
  className={`policy-icon ${
    policy.type === PolicyType.SECURITY
      ? 'policy-icon--security'
      : policy.type === PolicyType.TRAFFIC
        ? 'policy-icon--traffic'
        : 'policy-icon--transform'
  }`}
>
  <i className={`fas ${
    policy.type === PolicyType.SECURITY
      ? 'fa-lock-shield'
      : policy.type === PolicyType.TRAFFIC
        ? 'fa-bolt'
        : 'fa-shuffle'
  }`} />
</div>

Scope Binding

The PolicyScope enum defines where a policy applies:

export enum PolicyScope {
  GLOBAL = 'Global',
  API = 'API',
  VERSION = 'Version',
  PLAN = 'Plan',
  ROUTE = 'Route'
}

More specific scopes override less specific ones. A route-level rate limit overrides an API-level rate limit for that route.

When scope isn’t GLOBAL, the policy includes a targetRef:

interface PolicyTargetRef {
  type: PolicyScope;
  id: string;
}

Example: The Initial Policy Set

The app ships with demo policies:

const INITIAL_POLICIES: Policy[] = [
  {
    id: 'p1',
    name: 'OAuth2 Introspection',
    type: PolicyType.SECURITY,
    description: 'Validates bearer tokens against an external OAuth2 server.',
    enabled: true,
    phase: PolicyPhase.PRE_REQUEST,
    order: 1,
    failureMode: FailureMode.FAIL_CLOSED,
    scope: PolicyScope.GLOBAL,
    shortCircuit: false,
    config: { timeout: 500, introspectionEndpoint: '...' }
  },
  {
    id: 'p2',
    name: 'Edge Cache',
    type: PolicyType.TRAFFIC,
    description: 'Serves cached responses to reduce backend load.',
    enabled: true,
    phase: PolicyPhase.PRE_REQUEST,
    order: 0,
    failureMode: FailureMode.FAIL_OPEN,
    shortCircuit: true,
    scope: PolicyScope.API,
    targetRef: { type: PolicyScope.API, id: 'a1' },
    config: { ttl: 300, keyPattern: 'uri' }
  },
  {
    id: 'p3',
    name: 'Spike Arrest',
    type: PolicyType.TRAFFIC,
    description: 'Protects backend from sudden traffic surges.',
    enabled: true,
    phase: PolicyPhase.PRE_REQUEST,
    order: 2,
    failureMode: FailureMode.FAIL_CLOSED,
    shortCircuit: false,
    scope: PolicyScope.API,
    targetRef: { type: PolicyScope.API, id: 'a1' },
    config: { ratePerMinute: 1000, burstBuffer: 50 }
  },
  {
    id: 'p4',
    name: 'Sensitive Data Mask',
    type: PolicyType.TRANSFORMATION,
    description: 'Scrubs PII from outbound response payloads.',
    enabled: true,
    phase: PolicyPhase.POST_REQUEST,
    order: 0,
    failureMode: FailureMode.FAIL_CLOSED,
    shortCircuit: false,
    scope: PolicyScope.GLOBAL,
    config: { patterns: ['creditCard', 'ssn'] }
  }
];

Execution order for a request to API a1:

  1. OAuth2 Introspection (security check)
  2. Edge Cache (if hit, short-circuit—return cached response)
  3. Spike Arrest (if over limit, deny)
  4. → Backend call
  5. Sensitive Data Mask (scrub PII from response)
  6. → Return to client

The Execution Guarantee

When Hexarch evaluates a request:

  1. Policies are sorted by phase, then by order
  2. Each policy executes in sequence
  3. If shortCircuit is true and the policy matches, stop
  4. If a policy fails and failureMode is FAIL_CLOSED, terminate
  5. If a policy fails and failureMode is FAIL_OPEN, skip and continue

This is deterministic. Given the same request and policy set, you get the same execution path.

Try It

The Policies page at /policies offers two views:

  • Grid View: Browse policies as cards, see type/phase/scope at a glance
  • Flow View: See the execution pipeline with policies positioned by phase

Toggle between them to understand both the policy catalog and the execution model.