Skip to content

Architecture and Core Concepts

Pragmatic.Gateway is the reverse-proxy edge for Pragmatic.Design applications. Built on YARP, augmented with Agent-driven dynamic routing, tenant-aware proxying, maintenance mode, and per-cluster resilience.

Status: implemented preview subsystem. Functional inside the repo; operational surface still evolving.


A multi-service Pragmatic deployment needs an edge:

  • route /api/booking/* to the booking hosts, /api/billing/* to billing
  • present a single public endpoint while services scale and move internally
  • fail fast (circuit-break) when a backend is unhealthy, without taking the whole edge down
  • support tenant-aware routing — different tenants to different backend pools
  • handle maintenance mode — bleed traffic from hosts before deploy
  • offer standard edge features (compression, CORS, JWT auth, rate limiting)

You can deploy nginx / Traefik / Envoy, and for many teams that’s fine. But you pay:

  • a second configuration language (nginx.conf, Traefik labels) separate from your .NET code
  • no compile-time coupling to your topology — config drift is a real thing
  • limited access to your Pragmatic runtime types for custom middleware

Pragmatic.Gateway is the .NET-native alternative: same process model as your app hosts, driven by the same Pragmatic.Agent operational state, extensible with the full ASP.NET middleware stack.


YARP is a Microsoft-built reverse-proxy toolkit for .NET. It handles:

  • request forwarding with keep-alive pooling and HTTP/2
  • response streaming without buffering
  • load balancing (round-robin, random, least-request, power-of-two)
  • SSE and WebSocket upgrades
  • header transforms

Pragmatic.Gateway doesn’t re-invent these. It uses YARP for the transport and adds the layers YARP doesn’t cover: dynamic config from the Agent, maintenance middleware, tenant extraction, resilience policies built on Pragmatic.Resilience.


incoming request
┌─────────────────────────────────────┐
│ /health /ready endpoints │ ← short-circuit for Kubernetes probes
├─────────────────────────────────────┤
│ Response compression │
├─────────────────────────────────────┤
│ CORS │
├─────────────────────────────────────┤
│ Maintenance middleware │ ← drains if agent.gateway/maintenance=on
├─────────────────────────────────────┤
│ Rate limiting │ ← per client / per route
├─────────────────────────────────────┤
│ Authentication + Authorization │ ← JWT bearer, cookie, anonymous allow
├─────────────────────────────────────┤
│ Tenant routing │ ← extract tenant from header / subdomain
├─────────────────────────────────────┤
│ YARP reverse proxy │ ← resilience wrappers per cluster
├─────────────────────────────────────┤
│ forward to backend │
└─────────────────────────────────────┘

Every middleware is opt-in via GatewayOptions. A minimal gateway is “just the proxy”; a production one typically enables all of them.


The route table can come from two places:

{
"Gateway": {
"HttpUrl": "http://*:8080",
"Routes": [
{
"RouteId": "booking",
"Path": "/booking/{**catch-all}",
"Backend": "http://localhost:5010"
}
]
}
}

Baked-in, loaded at startup, changes require a restart. Fine for simple deployments.

When AgentSocketPath is set, the gateway subscribes to gateway/routes/* and gateway/clusters/* keys in the Agent’s KV store. When an operator updates a route via pragmatic-agent config set gateway/routes/booking ..., the gateway picks up the change within a gossip round (~seconds) and reconfigures YARP in-place — zero downtime.

Precedence: Agent routes override static routes with the same RouteId. Static routes act as a bootstrap and fallback.


Each backend cluster gets its own resilience policy:

{
"Clusters": {
"booking": {
"Destinations": { "b1": "http://booking-1:80", "b2": "http://booking-2:80" },
"Resilience": {
"TimeoutMs": 5000,
"CircuitBreaker": {
"FailureThreshold": 10,
"BreakDurationMs": 30000,
"MinimumThroughput": 20
}
}
},
"billing": { ... }
}
}

Why per-cluster: if billing is unhealthy, booking should keep serving. A global circuit-breaker would trip for both.

The policies are the same Pragmatic.Resilience primitives you use inside your app — configured via the same shapes, with the same failure counting semantics.


When TenantResolver is enabled, the gateway extracts the tenant before forwarding so:

  • the downstream service sees a X-Tenant-Id header (or configured equivalent)
  • routing can use the tenant to pick a different backend cluster (tenant=acmecluster=acme-pool)
  • auditing captures the tenant on the edge, not 5 hops in

Resolution strategies available:

  • Header (X-Tenant-Id)
  • Subdomain (acme.example.comacme)
  • JWT claim (tenant_id from the bearer token)
  • Route (prefix like /t/acme/...)

Multiple strategies can be composed with a priority order.


Agent key gateway/maintenance drives a gateway-wide or per-cluster drain:

Terminal window
pragmatic-agent config set gateway/maintenance "enabled:true,reason:deploy"

The maintenance middleware then:

  • accepts new requests on health paths (/health, /ready)
  • returns 503 with Retry-After for application routes
  • allows in-flight requests to complete (configurable grace period)

To drain one cluster (typical before a rolling deploy):

Terminal window
pragmatic-agent config set gateway/maintenance/booking "enabled:true"

Only the booking cluster returns 503; other clusters keep serving normally.


The gateway itself handles JWT Bearer authentication — tokens are validated once at the edge rather than at every downstream service.

{
"Authentication": {
"JwtBearer": {
"Authority": "https://auth.example.com",
"Audience": "api.example.com",
"RequireHttpsMetadata": true
}
}
}

Validated principals are forwarded as trusted headers (X-User-Id, X-User-Roles) to downstream services, configured via YARP transforms.

For fully public routes, set AllowAnonymous on the route.


  • Global load balancing across regions — use a cloud provider’s global LB for that
  • L4 TCP proxying — HTTP/1.1, HTTP/2, HTTP/3 only
  • TLS termination with cert-manager integration — configure TLS via Kestrel as you would for any ASP.NET app
  • WAF-style request inspection — deploy a WAF upstream
  • Service mesh sidecar — this is an edge gateway, not a sidecar

For those, keep your existing tooling (Cloudflare, Istio, Envoy) and point it at the Pragmatic.Gateway as an origin.