# How Route Table Renewal Enables Dynamic Updates in Fabrica-Kit

> Discover how fabrica-kit's route table renewal uses compare-and-expire for lock-free dynamic updates. Services renew entries intelligently, avoiding full table rewrites for efficient changes.

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

---

**Fabrica-Kit implements route table renewal through a compare-and-expire mechanism where services periodically call `RenewSelf` to extend their entry's TTL only if they still own the record, enabling lock-free dynamic updates without rewriting the entire routing table.**

Fabrica-Kit provides a sophisticated routing infrastructure that supports dynamic service discovery through automatic route table renewal. This mechanism allows running instances to extend their registration lifetime atomically, ensuring stale entries expire automatically while healthy services maintain their presence. The implementation relies on a read-only view combined with a renewal component that handles TTL management without requiring full record rewrites.

## Understanding the Renewal Interface

The renewal capability is defined in [`router/routetable/routetable.go`](https://github.com/go-pantheon/fabrica-kit/blob/main/router/routetable/routetable.go) through the `ReNewalRouteTable` interface. This interface embeds the read-only API and adds two critical methods for self-maintenance:

```go
type ReNewalRouteTable interface {
    ReadOnlyRouteTable
    RenewSelf(ctx context.Context, color string, key int64, value string) error
    TTL() time.Duration
}

```

The `RenewSelf` method accepts a **color** (logical shard identifier), **key** (unique instance ID), and **value** (service address). It only succeeds when the stored value still matches the provided value, preventing stale services from accidentally extending a record that has already been taken over by another instance. The `TTL` method exposes the configured time-to-live duration for the entries.

## Core Implementation Architecture

Located in [`router/routetable/renewal.go`](https://github.com/go-pantheon/fabrica-kit/blob/main/router/routetable/renewal.go), the `renewalRouteTable` struct provides the concrete implementation of dynamic updates. It holds a reference to the underlying data store (`Data`) and a configurable TTL duration.

### Configurable TTL Management

By default, route table entries live for 24 hours (`defaultTTL`). You can override this using the `WithTTL` functional option during initialization:

```go
// WithTTL configures a custom TTL.
func WithTTL(dur time.Duration) Option {
    return func(r *renewalRouteTable) {
        if dur <= 0 {
            dur = defaultTTL
        }
        r.ttl = dur
    }
}

```

### The Compare-and-Expire Pattern

When a service calls `RenewSelf`, the implementation forwards to the storage backend's `ExpireIfSame` method:

```go
func (r *renewalRouteTable) RenewSelf(ctx context.Context, color string, uid int64, value string) error {
    if err := r.data.ExpireIfSame(ctx, r.BuildKey(color, uid), value, r.ttl); err != nil {
        return errors.WithMessage(err, "renewIfSame route table failed")
    }
    return nil
}

```

The `BuildKey` method generates deterministic keys using the format `r_<name>_{<color>}_{<uid>}`. The `ExpireIfSame` operation—implemented by the Redis backend in [`router/routetable/redis/redis.go`](https://github.com/go-pantheon/fabrica-kit/blob/main/router/routetable/redis/redis.go)—performs an atomic compare-and-expire: it verifies the stored value equals the provided value, and only then sets the new expiration timestamp.

## Dynamic Update Flow

The route table renewal process follows a self-healing, lock-free pattern:

1. **Registration**: A service registers itself via `masterRouteTable.Set` or `SetNxOrGet`, storing its address with the configured TTL.
2. **Periodic Renewal**: The service calls `RenewSelf` at regular intervals (typically shorter than the TTL).
3. **Ownership Verification**: The underlying storage checks if the current value matches the caller's value.
4. **TTL Extension**: If values match, the expiration timestamp refreshes to `current_time + TTL`.
5. **Conflict Detection**: If another instance has overwritten the entry, `ExpireIfSame` fails, forcing the stale service to re-register or halt operations.

This design ensures that each instance only touches its own record while automatic cleanup removes dead instances after the TTL expires.

## Practical Implementation Examples

### Creating a Redis-Backed Renewal Table

Initialize a renewal route table with a custom 5-minute TTL:

```go
import (
    "time"
    "github.com/go-pantheon/fabrica-kit/router/routetable"
    rtredis "github.com/go-pantheon/fabrica-kit/router/routetable/redis"
    "github.com/redis/go-redis/v9"
)

func newRenewalTable() routetable.ReNewalRouteTable {
    client := redis.NewUniversalClient(&redis.UniversalOptions{
        Addrs: []string{"localhost:6379"},
    })
    ds := rtredis.New(client)
    
    return routetable.NewRenewalRouteTable(ds, "serviceA",
        routetable.WithTTL(5*time.Minute))
}

```

### Registering a Service Instance

Store the address with the TTL defined in the renewal component:

```go
func registerSelf(rt routetable.RouteTable, uid int64, address string) error {
    return rt.Set(context.Background(), "blue", uid, address)
}

```

### Implementing Periodic Renewal

Run a background goroutine to keep the registration alive:

```go
func keepAlive(rt routetable.ReNewalRouteTable, uid int64, address string) {
    ticker := time.NewTicker(2 * time.Minute)
    defer ticker.Stop()

    for range ticker.C {
        if err := rt.RenewSelf(context.Background(), "blue", uid, address); err != nil {
            log.Printf("renewal failed: %v", err)
            // Handle failure: re-register, alert, or shutdown
        }
    }
}

```

### Complete Integration Example

Wire everything together in your main application:

```go
func main() {
    rt := newRenewalTable()
    uid := int64(12345)
    addr := "10.0.0.42:8080"

    if err := rt.Set(context.Background(), "green", uid, addr); err != nil {
        log.Fatalf("register failed: %v", err)
    }

    go keepAlive(rt, uid, addr)
    
    select {} // Keep application running
}

```

## Summary

- **Route table renewal** in Fabrica-Kit uses a `ReNewalRouteTable` interface defined in [`router/routetable/routetable.go`](https://github.com/go-pantheon/fabrica-kit/blob/main/router/routetable/routetable.go) that extends read-only capabilities with self-renewal methods.
- The `renewalRouteTable` implementation in [`router/routetable/renewal.go`](https://github.com/go-pantheon/fabrica-kit/blob/main/router/routetable/renewal.go) provides configurable TTL management through the `WithTTL` option, defaulting to 24 hours.
- **Atomic compare-and-expire** operations via `ExpireIfSame` ensure that only the current owner of a route entry can extend its lifetime, preventing race conditions during service takeover.
- Key generation follows the pattern `r_<name>_{<color>}_{<uid>}`, creating deterministic storage keys for consistent lookups.
- This architecture enables **lock-free dynamic updates** where services independently maintain their registrations without coordinating with other instances.

## Frequently Asked Questions

### What is the default TTL for route table entries?

The default TTL is **24 hours** (defined as `defaultTTL` in [`router/routetable/renewal.go`](https://github.com/go-pantheon/fabrica-kit/blob/main/router/routetable/renewal.go)). You can customize this duration using the `WithTTL` functional option when constructing the renewal table, allowing intervals as short as seconds or as long as days depending on your service stability requirements.

### How does `RenewSelf` prevent stale services from extending entries?

`RenewSelf` implements a **compare-and-expire** pattern through the underlying storage's `ExpireIfSame` method. Before extending the TTL, the system atomically verifies that the stored value matches the value provided by the caller. If another instance has overwritten the entry with a new address, the comparison fails and the renewal returns an error, preventing the stale service from extending the TTL.

### What happens when route table renewal fails?

When `RenewSelf` returns an error, it indicates that either the entry no longer exists, the service no longer owns the record (another instance has registered), or the storage backend is unavailable. The calling service should treat this as a signal to either re-register itself using `Set` or `SetNxOrGet`, or gracefully shut down if it cannot reclaim its identity.

### Which storage backends support the renewal mechanism?

The renewal mechanism relies on the `Data` interface, which requires an `ExpireIfSame` method for atomic compare-and-expire operations. The **Redis backend** in [`router/routetable/redis/redis.go`](https://github.com/go-pantheon/fabrica-kit/blob/main/router/routetable/redis/redis.go) provides a production-ready implementation of this interface. You can implement the `Data` interface for other storage systems (such as etcd or Consul) as long as they support atomic compare-and-swap or compare-and-expire operations.