# Read-Only Route Table Patterns and Use Cases in fabrica-kit

> Discover readonly route table patterns and use cases in fabrica-kit. Learn how this secure, backend-agnostic foundation enables efficient service discovery with a layered architecture.

- Repository: [Pantheon/fabrica-kit](https://github.com/go-pantheon/fabrica-kit)
- Tags: deep-dive
- Published: 2026-03-02

---

**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`](https://github.com/go-pantheon/fabrica-kit/blob/main/router/routetable/routetable.go) at lines 42-46, specifying three essential operations:

- `BuildKey(color, oid string) string` – Generates deterministic storage keys following the pattern `r_<name>_{<color>}_{<oid>}`
- `Get(ctx context.Context, color, oid string) (string, error)` – Retrieves a single address entry
- `BatchGet(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`](https://github.com/go-pantheon/fabrica-kit/blob/main/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`](https://github.com/go-pantheon/fabrica-kit/blob/main/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`](https://github.com/go-pantheon/fabrica-kit/blob/main/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:

1. **Read-only clients** depend solely on `ReadOnlyRouteTable`, eliminating accidental mutation risks
2. **Self-renewing services** use `ReNewalRouteTable` for TTL maintenance without broader write privileges  
3. **Control plane components** utilize the full `RouteTable` for 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`/`BatchGet` with guaranteed side-effect-free operations, simplifying permission models and testing scenarios.

- **Load-balancer routing** – Proxies leverage `BatchGet` to 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 `ReNewalRouteTable` to execute `RenewSelf`, extending their own TTL while maintaining isolation from other registry keys.

- **Public API exposure** – External tools query routing state through the `ReadOnlyRouteTable` interface, minimizing the attack surface by excluding all mutation endpoints.

## Implementation Examples

### Instantiating a Read-Only Table

```go
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`](https://github.com/go-pantheon/fabrica-kit/blob/main/readonly.go) lines 21-30; retrieval methods at lines 37-45.*

### Batch Fetching Service Addresses

```go
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`](https://github.com/go-pantheon/fabrica-kit/blob/main/readonly.go) lines 47-59.*

### Extending with Self-Renewal Capabilities

```go
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

```go
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`](https://github.com/go-pantheon/fabrica-kit/blob/main/master.go) lines 19-27.*

## Summary

- The **read-only route table** implements the `ReadOnlyRouteTable` interface in [`router/routetable/routetable.go`](https://github.com/go-pantheon/fabrica-kit/blob/main/router/routetable/routetable.go), providing `Get`, `BatchGet`, and `BuildKey` operations.
- Concrete implementation in [`router/routetable/readonly.go`](https://github.com/go-pantheon/fabrica-kit/blob/main/router/routetable/readonly.go) (lines 13-60) wraps the backend-agnostic `Data` interface, 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`](https://github.com/go-pantheon/fabrica-kit/blob/main/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`](https://github.com/go-pantheon/fabrica-kit/blob/main/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`](https://github.com/go-pantheon/fabrica-kit/blob/main/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`](https://github.com/go-pantheon/fabrica-kit/blob/main/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`](https://github.com/go-pantheon/fabrica-kit/blob/main/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`](https://github.com/go-pantheon/fabrica-kit/blob/main/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.