How Color-Based Routing Works in Fabrica-Kit: A Complete Technical Guide
Color-based routing in Fabrica-Kit uses deterministic 32-bit integer colors derived from request metadata to index into a route table, achieving O(1) request-to-service mapping that maintains cache affinity without external coordination.
Fabrica-Kit implements an efficient request routing mechanism through profile colors, providing deterministic traffic distribution across service instances. This approach, implemented in the go-pantheon/fabrica-kit repository, enables predictable request-to-backend assignment using lightweight hash-based indexing rather than complex load-balancing algorithms.
Core Architecture of Color-Based Routing
Fabrica-Kit's routing system operates through three interconnected components that transform request metadata into concrete routing decisions.
Profile Color Extraction
The foundation of the system is the profile color, a 32-bit unsigned integer stored in the request's profile.Context. According to profile/color.go, colors are generated deterministically from request attributes like user IDs, session tokens, or custom headers using helper functions such as profile.ColorFromInt64() and profile.ColorFromString(). These helpers hash input values into consistent uint32 values, ensuring identical inputs always produce identical colors.
Route Table Indexing
The router implements O(1) lookup performance through direct indexing. As defined in router/routetable/routetable.go, the system maintains an in-memory slice of Route structs and calculates the target index using modulo arithmetic: idx := int(color) % len(rt.routes). This approach eliminates the need for complex tree traversals or hash maps during the hot path of request processing.
Balancer Picker Integration
After route selection, the balancer picker (defined in router/balancer/picker.go) can refine the choice. The picker receives the color through the request context, allowing it to maintain color affinity while applying health-check awareness or latency-based policies. This layered approach separates routing (which instance class) from balancing (which specific instance).
Step-by-Step Routing Flow
Understanding the complete request lifecycle reveals how colors propagate through the system.
1. Color Injection in Middleware
Early in the request-handling chain, middleware extracts identifiers and stores the computed color in the context. The profile.WithColor() function attaches the color, while profile.ColorFromContext() retrieves it downstream.
package middleware
import (
"net/http"
"time"
"github.com/go-pantheon/fabrica-kit/profile"
)
func ColorFromHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uid := r.Header.Get("X-User-ID")
var col uint32
if uid != "" {
col = profile.ColorFromString(uid)
} else {
col = profile.ColorFromInt64(time.Now().UnixNano())
}
ctx := profile.WithColor(r.Context(), col)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
2. Route Selection
When the request reaches router.Router, the system retrieves the color and performs the modulo operation against the route table length. The routetable.New() function initializes the table at service startup, holding backend endpoints as Route structs.
package main
import (
"net/http"
"github.com/go-pantheon/fabrica-kit/router"
"github.com/go-pantheon/fabrica-kit/router/routetable"
)
func main() {
rt := routetable.New([]router.Route{
{Target: "http://svc-01.internal"},
{Target: "http://svc-02.internal"},
{Target: "http://svc-03.internal"},
})
r := router.New(rt)
r.WithBalancer(router.NewRoundRobinBalancer())
http.Handle("/", r)
http.ListenAndServe(":8080", nil)
}
3. Backend Dispatch
The final handler can inspect the color for custom logic such as database sharding or cache key generation.
func handler(w http.ResponseWriter, r *http.Request) {
col := profile.ColorFromContext(r.Context())
// Use color for shard selection or logging
log.Printf("handling request for color: %d", col)
}
Why Use Color-Based Routing?
- Cache Affinity: Requests sharing a color consistently route to the same backend instance, maximizing local cache hit rates and session-state locality.
- Deterministic Sharding: The hash-based distribution requires no external coordination services; the modulus operation alone provides even traffic distribution.
- Graceful Scaling: Adding or removing routes changes only the modulus divisor, causing bounded reshuffling similar to consistent hashing but with minimal computational overhead.
- Flexible Attribution: Developers can base colors on any request attribute—user IDs, tenant identifiers, or geographic regions—enabling custom sharding semantics.
Summary
- Profile colors are 32-bit integers derived from request metadata via
profile/color.go, providing deterministic request classification. - Route tables in
router/routetable/routetable.gouse modulo indexing (color % len(routes)) for O(1) backend selection. - Balancer pickers in
router/balancer/picker.gorespect color affinity while enabling health-aware and latency-aware refinement. - The middleware pattern using
profile.WithColor()andprofile.ColorFromContext()enables transparent propagation of routing hints through the request lifecycle. - Color-based routing eliminates external coordination while maintaining predictable request-to-backend mapping during scale events.
Frequently Asked Questions
What is a profile color in Fabrica-Kit?
A profile color is a 32-bit unsigned integer (uint32) that serves as a deterministic routing key derived from request attributes. Stored in the request context via profile.WithColor() and retrieved via profile.ColorFromContext(), it enables consistent request-to-backend mapping without session state or external databases.
How does color-based routing improve cache affinity?
Because identical colors always route to the same backend instance (assuming the route table remains stable), repeated requests from the same user or tenant hit the same server’s local cache. This predictable routing, implemented in router/routetable/routetable.go, eliminates cache warming across multiple instances and reduces backend load.
Can I use custom attributes to generate colors?
Yes. The profile/color.go file exposes ColorFromString(), ColorFromInt64(), and similar helpers that hash any input value into a consistent 32-bit color. You can derive colors from headers, query parameters, JWT claims, or composite attributes, enabling flexible sharding strategies based on business requirements.
What happens when backend instances are added or removed?
When the route table changes (triggered by the renewal logic in router/routetable/routetable.go), the modulus divisor (len(routes)) changes, causing colors to remap to new indices. This produces bounded reshuffling—only a subset of colors migrate to different backends—rather than complete cache invalidation, allowing graceful scaling with minimal disruption to cache locality.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →