The question that starts this. "We run one gateway for the whole platform. Banking, insurance and payments are separate teams in separate namespaces. How do we let each team own its own routes without a central file that everyone has to fight over, and without one team's typo breaking another team's traffic?" kgateway's answer is route delegation: a parent route owned by the platform team that delegates each path prefix, by label, to a child route the owning team controls.
Two kinds of HTTPRoute
The pattern uses two roles. Both are the same HTTPRoute
kind from the Gateway API, but they do very different jobs and they
are owned by different teams.
Parent route
owned by the platform / gateway team · namespace kgateway-system
Attached to the Gateway. It names no application. Each
rule says "anything under /banking is handled over
there, anything under /insurance over there." It picks
which team's route handles a prefix, nothing more.
Child route
owned by each vertical team · in that team's own namespace
Lives in the team's namespace and defines the real routes for that
vertical: /banking/accounts, /banking/cards,
and the backends they point at. The team merges changes here through
Git with no access to anyone else's routes.
The parent delegates a path prefix to a child, and the child resolves it to a real backend. That is the whole mechanism.
The routing decision, end to end
How to read it. A request to api.example.com
hits the parent route on the gateway. The parent matches the path
prefix and delegates by label to the child route in the owning team's
namespace. The child matches the full path and forwards through its own
backendRefs. Banking and insurance land on an in-cluster
Service; payments shows the same delegation resolving to an
out-of-cluster Backend.
Why split instead of one big route
You could write a single HTTPRoute with every route for
every vertical in it. It works. The reason not to is ownership and
blast radius.
What one shared route costs you
- It becomes a bottleneck. Every team's change goes through one file, so every change needs the platform team to review and merge. That central dependency is exactly what teams are trying to remove.
- Blast radius. A typo by the banking team in that shared file can break payments. One file, every team's traffic.
- It doesn't scale. Each new vertical makes the shared file bigger and the merge queue longer.
Splitting it gives the banking team the banking route in the banking namespace, the payments team theirs, and so on. Each team merges its own changes in its own namespace with no access to anyone else's routes. The platform team owns only the small parent route.
Delegation by label
Look at how the parent points at a child. The backendRefs
is not pointing at an application. It points at a label.
The parent says delegate /banking to whichever
HTTPRoute carries the label
delegation.kgateway.dev/label: banking-bff, in any
namespace (namespace: all). The child opts in
simply by wearing that label. Because the parent matches by label and
not by a specific route name, a team can deploy a new child with the
right label and it attaches automatically. The parent never has to be
edited.
parent backendRefs · points at a label, not an app
group + kind + name select the child route
backendRefs:
- group: delegation.kgateway.dev
kind: label
name: banking-bff
namespace: all # match the labelled child in any namespace
child metadata.labels · how a route opts in
wear the label the parent is delegating to
metadata:
name: banking-routes
namespace: banking
labels:
delegation.kgateway.dev/label: banking-bff
Adding insurance and payments
There are two cases when a new vertical shows up, and only one of them touches the parent.
Case A · the prefix already exists in the parent
Say the parent already delegates /insurance to label
insurance-bff with namespace: all. A new
insurance team just deploys their child route carrying
delegation.kgateway.dev/label: insurance-bff. No parent
edit needed. It attaches on its own.
Case B · a brand-new top-level prefix
For a prefix the parent has never seen, you add one rule to the parent (a five-line block) for the new prefix, then the owning team deploys their labelled child. So a whole new vertical is one small parent rule plus the team's own child route.
The YAML below is the parent with all three verticals, plus the payments child added.
HTTPRoute · parent with three verticals + payments child
platform owns the parent, payments owns the child
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: parent
namespace: kgateway-system
spec:
parentRefs:
- name: http
namespace: kgateway-system
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /banking
backendRefs:
- group: delegation.kgateway.dev
kind: label
name: banking-bff
namespace: all
- matches:
- path:
type: PathPrefix
value: /insurance
backendRefs:
- group: delegation.kgateway.dev
kind: label
name: insurance-bff
namespace: all
- matches:
- path:
type: PathPrefix
value: /payments
backendRefs:
- group: delegation.kgateway.dev
kind: label
name: payments-bff
namespace: all
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: payments-routes
namespace: payments
labels:
delegation.kgateway.dev/label: payments-bff
spec:
rules:
- matches:
- path:
type: PathPrefix
value: /payments/charge
backendRefs:
- name: payments-bff
port: 8080
- matches:
- path:
type: PathPrefix
value: /payments/refund
backendRefs:
- name: payments-bff
port: 8080
Where does the traffic actually land
Not another gateway. Delegation routes to another HTTPRoute,
and that child resolves to a real backend through its own
backendRefs. The destination is whatever the child points
at:
In-cluster app
the default · the normal BFF case
With no group/kind, a backendRef defaults
to a Kubernetes Service in the same namespace. The BFF
is a deployment, so the traffic goes to its Pods in this cluster.
Out-of-cluster service
a Backend with static hosts / DNS
Point the child at a kgateway Backend
(gateway.kgateway.dev) on port: 443. The
vertical's BFF can live in another cluster or region and you delegate
to it exactly the same way.
child backendRefs · in-cluster Service vs out-of-cluster Backend
same delegation, different destination
# in-cluster: defaults to a Service named payments-bff
backendRefs:
- name: payments-bff
port: 8080
# out-of-cluster: a Backend CRD with static hosts / DNS
backendRefs:
- group: gateway.kgateway.dev
kind: Backend
name: payments-external
port: 443
So the two halves stay clean. Delegation handles the routing decision,
which path goes to which team's route. The child's backendRefs
handles the destination, an app in this cluster or an external target.
References
Adjacent demos on this site
- Versioned routing with kgateway — claims-to-headers re-routing an
HTTPRouteon a JWT-derived header, the runnable kind lab. - How claimsToHeaders works on the wire — turning a verified JWT claim into the header a route keys off.
- kgateway vs Istio Ingress Gateway — where route delegation sits among the broader north-south capabilities, and the role-split governance model.