Read-Only Route Table Patterns and Use Cases in fabrica-kit
The read-only route table provides a secure, backend-agnostic foundation for service discovery in fabrica-kit, implementing a layered architecture that separates pure read operations from renewal and write capabilities.
The read-only route table is a core abstraction within go-pantheon/fabrica-kit that enables safe, deterministic lookups of service instance addresses without exposing mutation primitives. By restricting the public API to retrieval methods only, this pattern allows client applications, load balancers, and monitoring systems to consume routing data while enforcing strict separation of concerns between readers and writers.
Architectural Design of the Read-Only Route Table
Interface Definition
The contract for read-only access is defined in router/routetable/routetable.go at lines 42-46, specifying three essential operations:
BuildKey(color, oid string) string– Generates deterministic storage keys following the patternr_<name>_{<color>}_{<oid>}Get(ctx context.Context, color, oid string) (string, error)– Retrieves a single address entryBatchGet(ctx context.Context, color string, oids []int64) ([]string, error)– Performs multi-key lookups in a single round-trip
Concrete Implementation
The implementation resides in router/routetable/readonly.go (lines 13-60), where NewReadOnlyRouteTable(rtd Data, name string) constructs a struct holding three critical dependencies: the underlying Data store interface, the table name, and the key-building function (lines 21-30).
The Get and BatchGet methods (lines 37-59) delegate directly to the Data layer while wrapping errors with contextual metadata. This design makes the table backend-agnostic, functioning identically whether backed by Redis, PostgreSQL, or custom storage implementations.
Layered Composition Pattern
The read-only table serves as the foundation for a progressive enhancement hierarchy:
ReadOnlyRouteTable ← ReNewalRouteTable ← RouteTable (master)
ReNewalRouteTable (router/routetable/renewal.go, lines 34-66) embeds the read-only implementation and adds RenewSelf, which extends TTL only when the stored value matches the caller's expectation (lines 59-66). This allows service instances to refresh their own registrations without write access to peer entries.
RouteTable (master) (router/routetable/master.go, lines 11-68) further embeds the renewal layer, exposing full write APIs including Set, SetNxOrGet, GetSet, and GetEx (lines 19-27).
This composition yields three distinct security boundaries:
- Read-only clients depend solely on
ReadOnlyRouteTable, eliminating accidental mutation risks - Self-renewing services use
ReNewalRouteTablefor TTL maintenance without broader write privileges - Control plane components utilize the full
RouteTablefor administrative operations
Practical Use Cases for Read-Only Route Tables
The readonly abstraction supports five primary operational patterns:
-
Client-side service discovery – Microservices query peer addresses via
Get/BatchGetwith guaranteed side-effect-free operations, simplifying permission models and testing scenarios. -
Load-balancer routing – Proxies leverage
BatchGetto pull multiple backend addresses in a single network round-trip, reducing latency during request path initialization. -
Health-checker and monitoring – Periodic verification jobs read stored addresses to validate reachability; the read-only constraint prevents accidental deregistration during health probes.
-
Service instance self-renewal – Processes embed
ReNewalRouteTableto executeRenewSelf, extending their own TTL while maintaining isolation from other registry keys. -
Public API exposure – External tools query routing state through the
ReadOnlyRouteTableinterface, minimizing the attack surface by excluding all mutation endpoints.
Implementation Examples
Instantiating a Read-Only Table
package main
import (
"context"
"fmt"
"github.com/go-pantheon/fabrica-kit/router/routetable"
"github.com/go-pantheon/fabrica-kit/router/routetable/redis"
)
func createReadOnlyTable() error {
// Initialize Redis-backed Data implementation
rds := redis.NewRedisData(/* config */)
// Construct read-only view for the "order" service
roTable := routetable.NewReadOnlyRouteTable(rds, "order")
// Retrieve single instance address
ctx := context.Background()
addr, err := roTable.Get(ctx, "blue", "12345")
if err != nil {
return fmt.Errorf("lookup failed: %w", err)
}
fmt.Printf("Instance address: %s\n", addr)
return nil
}
Construction logic appears in readonly.go lines 21-30; retrieval methods at lines 37-45.
Batch Fetching Service Addresses
func lookupBackends(roTable routetable.ReadOnlyRouteTable) error {
ctx := context.Background()
instanceIDs := []int64{101, 102, 103, 104}
// Single round-trip retrieval
addresses, err := roTable.BatchGet(ctx, "green", instanceIDs)
if err != nil {
return err
}
for i, addr := range addresses {
fmt.Printf("ID %d → %s\n", instanceIDs[i], addr)
}
return nil
}
BatchGet implementation resides in readonly.go lines 47-59.
Extending with Self-Renewal Capabilities
func maintainRegistration(rt routetable.ReNewalRouteTable, uid int64, currentAddr string) error {
ctx := context.Background()
// Extends TTL only if stored value matches currentAddr
// Implementation: renewal.go lines 59-66
return rt.RenewSelf(ctx, "red", uid, currentAddr)
}
Full Master Table with Embedded Read-Only Access
func adminOperations(rtd routetable.Data) error {
ctx := context.Background()
// Master table with 24h default TTL
master := routetable.NewMasterRouteTable(
rtd,
"payment",
routetable.WithTTL(time.Hour),
)
// Write new entry
if err := master.Set(ctx, "yellow", 555, "10.0.9.12:8080"); err != nil {
return err
}
// Read via embedded read-only interface
addr, _ := master.Get(ctx, "yellow", 555)
fmt.Println(addr) // → 10.0.9.12:8080
return nil
}
Master table construction appears in master.go lines 19-27.
Summary
- The read-only route table implements the
ReadOnlyRouteTableinterface inrouter/routetable/routetable.go, providingGet,BatchGet, andBuildKeyoperations. - Concrete implementation in
router/routetable/readonly.go(lines 13-60) wraps the backend-agnosticDatainterface, supporting Redis, PostgreSQL, or custom storage. - Layered composition allows progressive privilege escalation: read-only base → renewal-capable middle tier → full master table with write access.
- Primary use cases include client-side discovery, load-balancer routing, health monitoring, and secure self-renewal of service instances.
- The pattern enforces separation of concerns by ensuring read-only consumers cannot accidentally modify registry state.
Frequently Asked Questions
What is the primary benefit of using a read-only route table over direct storage access?
The read-only route table enforces a strict contract that prevents accidental mutations while providing optimized methods like BatchGet for efficient multi-key lookups. By depending on the ReadOnlyRouteTable interface rather than the raw Data store, applications gain type-safe access with built-in key generation logic (using BuildKey) and consistent error handling, as implemented in router/routetable/readonly.go lines 37-59.
How does the renewal route table extend the read-only implementation without violating its constraints?
ReNewalRouteTable in router/routetable/renewal.go (lines 34-66) embeds the ReadOnlyRouteTable struct, inheriting all read methods while adding only RenewSelf. This method uses ExpireIfSame semantics (lines 59-66) to extend TTL exclusively when the stored value matches the caller's current address, effectively allowing self-maintenance without exposing general write capabilities to other keys.
Can the read-only route table work with storage backends other than Redis?
Yes. The implementation depends on the Data interface defined in router/routetable/routetable.go, not a concrete storage type. The NewReadOnlyRouteTable constructor accepts any implementation satisfying the Data interface—whether Redis (via router/routetable/redis/redis.go), PostgreSQL, or a custom in-memory store—making the read-only logic fully backend-agnostic as shown in readonly.go lines 21-30.
When should I use the master route table versus the read-only version?
Use the master route table (router/routetable/master.go, lines 11-68) only for control plane or administrative components that must create, update, or delete registry entries via methods like Set and GetSet. Use the read-only version for data plane components such as service clients, load balancers, or monitoring agents that require lookup capabilities without mutation risks, following the layered security model described in the architectural overview.
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 →