# How fabrica-util's xid.BuildUID Combines Zone IDs for Cross-Zone Player Session Management

> Learn how fabrica-util's xid.BuildUID combines zone IDs for efficient cross-zone player session management. Track sessions atomically without extra database columns.

- Repository: [Pantheon/fabrica-util](https://github.com/go-pantheon/fabrica-util)
- Tags: how-to-guide
- Published: 2026-03-02

---

**fabrica-util's `xid.BuildUID` function packs a player ID and zone ID into a single 64-bit integer using bitwise operations, enabling atomic cross-zone session tracking without separate database columns.**

The `go-pantheon/fabrica-util` repository provides distributed game server utilities for managing player sessions across multiple zones. By encoding both the player identity and their current zone location into one immutable identifier, the system eliminates race conditions during zone transitions while simplifying database indexing and key lookups.

## Bit Layout Constants in xid/id.go

The encoding scheme is defined in [`xid/id.go`](https://github.com/go-pantheon/fabrica-util/blob/main/xid/id.go) using three constants that establish the bit boundaries for packing:

```go
const (
    gameIDSlotBit = 16                     // lower 16 bits reserved
    zoneSlotBit   = 8                      // zone occupies next 8 bits
    zoneMask      = (1 << zoneBit) - 1     // 0xFF, limits a zone to 0‑255
)

```

This layout allocates the 64-bit signed integer as follows:

- **Game ID**: Occupies bits 16 through 63 (48 bits), supporting values from `-MaxGameID` to `MaxGameID`
- **Zone ID**: Occupies bits 8 through 15 (8 bits), supporting 0–255 zones via `zoneMask`
- **Lower 8 bits**: Reserved (bits 0–7)

The `gameIDSlotBit` constant (16) defines the left-shift amount for the player identifier, while `zoneSlotBit` (8) positions the zone identifier in the upper middle byte.

## Composing IDs with BuildUID

The `BuildUID` function validates inputs before performing bitwise composition. It first checks that `gameID` fits within the allowable range to prevent overflow conditions:

- Returns **`ErrGameIDTooLarge`** if `gameID` exceeds `MaxGameID`
- Returns **`ErrGameIDTooSmall`** if `gameID` is less than `-MaxGameID`

Upon validation, the function constructs the packed identifier by left-shifting the game ID by `gameIDSlotBit` (16) and the zone ID by `zoneSlotBit` (8), then combining them with a bitwise OR:

```go
return (gameID << gameIDSlotBit) | int64(zone)<<zoneSlotBit, nil

```

This operation places the zone ID in bits 8–15 and the game ID in bits 16–63, producing a single `int64` value that uniquely identifies both the player and their current zone location.

## Decoding Sessions with SplitUID

To retrieve the original components from a packed UID, [`xid/id.go`](https://github.com/go-pantheon/fabrica-util/blob/main/xid/id.go) provides the `SplitUID` function. This reverses the bit manipulation through right-shift operations and masking:

```go
gameID = uid >> gameIDSlotBit
zone   = uint8(uid >> zoneSlotBit & zoneMask)

```

The game ID is recovered by shifting right 16 bits, while the zone ID requires a right shift of 8 bits followed by an AND operation with `zoneMask` (0xFF) to isolate the lower 8 bits of that segment.

## Practical Implementation Examples

The following example demonstrates creating a combined UID for player 12345 entering zone 17:

```go
package main

import (
    "fmt"
    "github.com/go-pantheon/fabrica-util/xid"
)

func main() {
    // Combine player #12345 with zone 17
    uid, err := xid.BuildUID(12345, 17)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Combined UID: %d\n", uid)

    // Decode the packed value
    gameID, zone := xid.SplitUID(uid)
    fmt.Printf("GameID: %d, Zone: %d\n", gameID, zone)
}

```

Error handling ensures out-of-range values are caught before packing:

```go
// Attempt to pack an invalid game ID
uid, err := xid.BuildUID(xid.MaxGameID+1, 0)
if err != nil {
    fmt.Println("Could not build UID:", err) // Outputs: ErrGameIDTooLarge
}

```

## Summary

- **[`xid/id.go`](https://github.com/go-pantheon/fabrica-util/blob/main/xid/id.go)** defines the bit layout using `gameIDSlotBit` (16) and `zoneSlotBit` (8) constants to partition a 64-bit integer into game and zone segments
- **`BuildUID`** validates game ID bounds against `MaxGameID` and packs both identifiers using left-shift and bitwise OR operations
- **`SplitUID`** reverses the encoding using right-shift and masking with `zoneMask` to recover the original components
- The packed UID format supports 48-bit game IDs and 8-bit zone IDs (0–255), enabling atomic 64-bit operations for distributed session management without separate columns

## Frequently Asked Questions

### What happens if the gameID exceeds the allowed range in BuildUID?

The function performs bounds checking before bit manipulation. If the game ID is greater than `MaxGameID`, it returns `ErrGameIDTooLarge`; if less than `-MaxGameID`, it returns `ErrGameIDTooSmall`. These exported errors from [`xid/id.go`](https://github.com/go-pantheon/fabrica-util/blob/main/xid/id.go) allow calling code to handle validation failures explicitly without panicking.

### How many zones can fabrica-util support per player session?

The zone ID occupies 8 bits as defined by `zoneSlotBit` and filtered through `zoneMask` (0xFF), supporting exactly 256 unique zones (0 through 255). This limit is hardcoded in the bit layout constants within [`xid/id.go`](https://github.com/go-pantheon/fabrica-util/blob/main/xid/id.go).

### Can SplitUID decode any 64-bit integer created by BuildUID?

Yes, `SplitUID` is the mathematical inverse of `BuildUID`. Any valid UID produced by the packing function can be decomposed back into its original game ID and zone components without data loss, assuming the UID was created using the same constant definitions from the fabrica-util source.

### Why does fabrica-util use bit packing instead of separate database columns?

Packing both identifiers into a single 64-bit integer allows atomic comparison and indexing operations across distributed systems. This design prevents race conditions when moving players between zones because the combined ID represents an immutable snapshot of player location, eliminating the need for transactional updates across multiple database columns or tables.