This guide covers the Infinispan Go client for the Hot Rod binary protocol.

Getting Started

Installation

go get infinispan.org/go-client

Quick Start

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"infinispan.org/go-client/hotrod"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	client, err := hotrod.NewClient(ctx, "hotrod://admin:password@localhost:11222")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	cache := client.Cache("my-cache")

	cache.Put(ctx, []byte("greeting"), []byte("hello world"))

	val, found, err := cache.Get(ctx, []byte("greeting"))
	if err != nil {
		log.Fatal(err)
	}
	if found {
		fmt.Println(string(val))
	}
}

Connecting to Infinispan

Connection URI

The client connects using a URI of the form:

hotrod://username:password@host:port
hotrods://username:password@host:port

The hotrods:// scheme enables TLS.

Client Options

You can pass functional options to hotrod.NewClient to customize connection behavior.

WithConnectTimeout(d time.Duration)

Sets the TCP connection timeout. If not specified, the system default applies.

WithSocketTimeout(d time.Duration)

Sets the timeout for individual read and write operations. When a context does not already carry a deadline, this timeout is applied automatically.

WithTCPNoDelay(b bool)

Enables or disables Nagle’s algorithm on connections. Defaults to true (no delay).

WithTCPKeepAlive(b bool)

Enables or disables TCP keep-alive probes on connections. Defaults to false.

WithClientIntelligence(level byte)

Sets the client intelligence level. See Client Intelligence for details.

WithLogger(l *slog.Logger)

Sets a custom structured logger. Defaults to slog.Default().

client, err := hotrod.NewClient(ctx, "hotrod://admin:password@localhost:11222",
	hotrod.WithClientIntelligence(hotrod.IntelligenceTopologyAware),
	hotrod.WithConnectTimeout(5*time.Second),
	hotrod.WithSocketTimeout(10*time.Second),
)

You can also set these options through URI query parameters:

hotrod://admin:password@localhost:11222?connect_timeout=5s&socket_timeout=10s&tcp_no_delay=true&tcp_keep_alive=false

Security

Authentication

The client supports the following SASL authentication mechanisms:

  • SCRAM-SHA-256 — Default when username and password are provided.

  • PLAIN — Simple username/password authentication.

  • EXTERNAL — Certificate-based authentication (mTLS).

  • OAUTHBEARER — OAuth2/JWT bearer token authentication (RFC 7628).

You can provide credentials directly in the connection URI:

client, err := hotrod.NewClient(ctx, "hotrod://admin:password@localhost:11222")

Alternatively, use the WithAuth option to specify the mechanism and credentials programmatically:

client, err := hotrod.NewClient(ctx, "hotrod://localhost:11222",
	hotrod.WithAuth("SCRAM-SHA-256", "admin", "password"),
)

When a username and password are present (either in the URI or via WithAuth), the client defaults to SCRAM-SHA-256. When a token is provided but no username, the client defaults to OAUTHBEARER. When a client certificate is configured but no username or token is provided, the client defaults to EXTERNAL.

OAuth2 Bearer Token Authentication

To authenticate with an OAuth2 or JWT bearer token, use the WithToken option:

client, err := hotrod.NewClient(ctx, "hotrod://localhost:11222",
	hotrod.WithToken("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."),
)

You can also pass the token and mechanism through URI query parameters:

hotrod://localhost:11222?sasl_mechanism=OAUTHBEARER&token=eyJhbGci...

You can explicitly select the mechanism with WithAuth combined with WithToken:

client, err := hotrod.NewClient(ctx, "hotrod://localhost:11222",
	hotrod.WithAuth("OAUTHBEARER", "", ""),
	hotrod.WithToken(token),
)
Note

OAUTHBEARER requires the Infinispan server to be configured with a token realm, for example backed by Keycloak or another OAuth2 provider.

TLS and Mutual TLS

Use the hotrods:// URI scheme or provide TLS options to enable encrypted connections.

WithTLS(c *tls.Config)

Sets a custom tls.Config for full control over the TLS configuration.

WithTrustStore(path string)

Sets the path to a PEM-encoded CA certificate file for server verification.

WithClientCert(certPath, keyPath string)

Sets the client certificate and private key for mutual TLS (mTLS) authentication.

WithSNIHostName(name string)

Sets the TLS Server Name Indication (SNI) hostname.

WithSSLHostnameValidation(b bool)

Enables or disables TLS hostname verification. Set to false only for development environments.

You can also configure TLS through URI query parameters:

hotrods://admin:password@localhost:11222?trust_store_file_name=/path/to/ca.pem&sni_host_name=infinispan.example.com
hotrods://localhost:11222?client_cert=/path/to/cert.pem&client_key=/path/to/key.pem

The supported query parameters are trust_store_file_name (or trust_ca), client_cert (or key_store_file_name), client_key (or key_store_password), and sni_host_name (or sni_host).

Cache Operations

Basic Operations

Cache operations use []byte keys and values. Call client.Cache("name") to obtain a RemoteCache handle, then use Put, Get, and Remove:

cache := client.Cache("my-cache")

// Store an entry.
err := cache.Put(ctx, []byte("key"), []byte("value"))

// Retrieve an entry.
val, found, err := cache.Get(ctx, []byte("key"))

// Delete an entry.
err = cache.Remove(ctx, []byte("key"))

Use GetAndPut and GetAndRemove to obtain the previous value in a single round-trip:

// Store and get the previous value.
prev, err := cache.GetAndPut(ctx, []byte("key"), []byte("new-value"))

// Remove and get the previous value.
prev, existed, err := cache.GetAndRemove(ctx, []byte("key"))

Additional conditional operations are available:

PutIfAbsent(ctx, key, value)

Stores the entry only if the key does not already exist. Returns true if stored, false if the key was already present.

Replace(ctx, key, value)

Replaces the value only if the key already exists. Returns true if replaced, false if the key was not found.

ContainsKey(ctx, key)

Returns true if the cache contains an entry for the given key.

Clear(ctx)

Removes all entries from the cache.

Size(ctx)

Returns the number of entries in the cache.

Stats(ctx)

Returns a map of server-side cache statistics such as timeSinceStart and currentNumberOfEntries.

Versioned and Metadata Operations

Use GetWithMetadata to retrieve the value along with server-side metadata (version, creation time, lifespan, last-used time, and max idle time):

mv, found, err := cache.GetWithMetadata(ctx, []byte("key"))
if found {
	fmt.Printf("value=%s version=%d created=%d lifespan=%d\n",
		mv.Value, mv.Version, mv.Created, mv.Lifespan)
}

Use the version for optimistic locking with ReplaceIfUnmodified and RemoveIfUnmodified:

ok, err := cache.ReplaceIfUnmodified(ctx, []byte("key"), []byte("new"), mv.Version)
ok, err = cache.RemoveIfUnmodified(ctx, []byte("key"), mv.Version)

Bulk Operations

PutAll stores multiple entries in a single operation. When the client is hash-distribution-aware, entries are grouped by owner and dispatched in parallel:

entries := map[string][]byte{
	"key1": []byte("val1"),
	"key2": []byte("val2"),
}
err := cache.PutAll(ctx, entries)

GetAll retrieves multiple keys in a single operation:

result, err := cache.GetAll(ctx, []string{"key1", "key2"})

Expiration

Use WithLifespan and WithMaxIdle to control entry expiration:

WithLifespan(d time.Duration)

Sets the maximum time an entry can live in the cache before expiring, regardless of access.

WithMaxIdle(d time.Duration)

Sets the maximum idle time. The entry expires if it is not accessed within this duration.

cache.Put(ctx, []byte("key"), []byte("value"),
	hotrod.WithLifespan(30*time.Second),
	hotrod.WithMaxIdle(10*time.Second),
)

Both options apply to Put, PutIfAbsent, Replace, PutAll, and ReplaceIfUnmodified.

Operation Flags

Operation flags modify server-side behavior for individual operations. Pass them with WithPutFlag, WithGetFlag, or WithRemoveFlag:

FlagForceReturnValue

Forces the server to return the previous value. This flag is set automatically by GetAndPut and GetAndRemove.

FlagDefaultLifespan

Uses the cache’s default lifespan instead of the value specified in the operation.

FlagDefaultMaxIdle

Uses the cache’s default max idle time instead of the value specified in the operation.

FlagSkipCacheLoad

Skips loading the entry from a cache store (persistence).

FlagSkipIndexing

Skips indexing the entry.

FlagSkipListenerNotification

Skips notifying cache listeners about the operation.

cache.Put(ctx, []byte("key"), []byte("value"),
	hotrod.WithPutFlag(hotrod.FlagSkipListenerNotification),
)

Typed Caches with Protocol Buffers

Schema Registration

Before using typed caches with Protocol Buffers, register your .proto schemas with the server using the SchemaManager:

schemas := client.Schemas()

err := schemas.Register(ctx, "person.proto", `
syntax = "proto3";
package example;

message Person {
    string name = 1;
    int32 age = 2;
}
`)

Retrieve a previously registered schema with Get:

content, found, err := schemas.Get(ctx, "person.proto")

Creating a Typed Cache

Use NewTypedCache with a ProtoStreamMarshaller to work with Go types instead of raw bytes. The marshaller handles serialization to and from the ProtoStream format that the Infinispan server expects.

marshaller := hotrod.NewProtoStreamMarshaller("example.Person")

cache := hotrod.NewTypedCache[string, *Person](
	client, "people", marshaller,
	func() *Person { return &Person{} },
)

err := cache.Put(ctx, "john", &Person{Name: "John", Age: 30})

person, found, err := cache.Get(ctx, "john")

The fourth argument is a factory function that creates a new zero-value instance of the value type for unmarshalling.

Custom Marshaller Interface

You can implement the Marshaller interface to use a custom serialization format:

type Marshaller interface {
	MarshalKey(key any) ([]byte, error)
	UnmarshalKey(data []byte, target any) error
	MarshalValue(value any) ([]byte, error)
	UnmarshalValue(data []byte, target any) error
	MediaType() int32
}

The MediaType() method returns the media type identifier sent to the server so it can interpret the encoded bytes correctly.

Near Caching

Overview

Near caching stores recently accessed cache entries in a local bounded cache to avoid server round-trips on repeated reads. The Go client uses the Infinispan server bloom filter optimization: the server maintains a bloom filter of keys the client has cached and only sends invalidation events for keys that may be present locally.

Configuration

nc, err := hotrod.NewNearCache(ctx, client, "my-cache",
	hotrod.WithMaxNearCacheEntries(1000),
)
defer nc.Close(ctx)

WithMaxNearCacheEntries(n int) sets the maximum number of entries in the local LRU cache. The default is 1000 entries.

Behavior

The near cache intercepts Get, Put, and Remove operations:

  • Get: If the key is in the local cache, the value is returned immediately without a server round-trip. If the key is not present locally, the client fetches it from the server and stores it in the local cache.

  • Put and Remove: The operation is sent to the server first, then the local entry is eagerly invalidated.

When other clients modify entries, the server sends invalidation events through a bloom-filter-based listener. The client automatically removes invalidated entries from the local cache.

The bloom filter is periodically updated as entries are evicted from the LRU cache. After approximately one sixteenth of the maximum entries have been evicted, the client sends an updated bloom filter to the server to maintain accurate invalidation.

Event Listeners

Cache Entry Events

You can register listeners to receive notifications when cache entries are created, modified, removed, or expired. Call AddListener on a RemoteCache to register a listener:

listener, err := cache.AddListener(ctx)
if err != nil {
	log.Fatal(err)
}
defer cache.RemoveListener(ctx, listener)

for event := range listener.Events {
	fmt.Printf("type=%d key=%s\n", event.Type, event.Key)
}

The Events channel delivers CacheEntryEvent values with the following fields:

  • Type — The event type: EventCreated, EventModified, EventRemoved, or EventExpired.

  • Key — The key of the affected entry.

  • IsRetried — Whether this is a retried event delivery.

Listener Options

WithListenerInterests(types …​EventType)

Restricts the listener to only the specified event types. By default, all event types are delivered.

WithIncludeCurrentState()

Causes the listener to receive synthetic events for all existing entries upon registration. This is useful for building an initial snapshot of the cache state.

WithChannelSize(n int)

Sets the buffer size of the event channel. Defaults to 64.

listener, err := cache.AddListener(ctx,
	hotrod.WithListenerInterests(hotrod.EventCreated, hotrod.EventRemoved),
	hotrod.WithChannelSize(128),
)

Queries

Overview

The Go client supports the Ickle query language for searching indexed caches. Call Query on a RemoteCache to execute a query and receive results:

result, err := cache.Query(ctx, "FROM example.Person WHERE age >= 30")

Queries require a cache with an indexed protobuf schema registered on the server.

Example Schemas

The examples in this section use the following protobuf schemas.

Simple Person

syntax = "proto3";
package example;

/** @Indexed */
message Person {
    /** @Keyword(projectable = true, sortable = true) */
    string name = 1;
    /** @Basic(projectable = true, sortable = true) */
    int32 age = 2;
}

Person with Address (Nested Type)

syntax = "proto3";
package example;

message Address {
    /** @Keyword(projectable = true) */
    string street = 1;
    /** @Keyword(projectable = true) */
    string city = 2;
    /** @Keyword(projectable = true) */
    string country = 3;
}

/** @Indexed */
message PersonWithAddress {
    /** @Keyword(projectable = true, sortable = true) */
    string name = 1;
    /** @Basic(projectable = true, sortable = true) */
    int32 age = 2;
    /** @Indexed */
    repeated Address addresses = 3;
}

Restaurant (Spatial Type)

syntax = "proto3";
package example;

/**
 * @Indexed
 * @GeoPoint(fieldName = "location", projectable = true, sortable = true)
 */
message Restaurant {
    /** @Keyword(projectable = true, sortable = true) */
    string name = 1;
    /** @Text */
    string description = 2;
    /** @Latitude(fieldName = "location") */
    double latitude = 3;
    /** @Longitude(fieldName = "location") */
    double longitude = 4;
}

The @GeoPoint annotation creates a composite spatial field called location from the separate @Latitude and @Longitude fields.

Book (Vector Type)

syntax = "proto3";
package example;

/** @Indexed */
message Book {
    /** @Keyword(projectable = true, sortable = true) */
    string title = 1;
    /** @Keyword(projectable = true, normalizer = "lowercase") */
    string genre = 2;
    /** @Text */
    string description = 3;
    /** @Vector(dimension = 3, similarity = COSINE) */
    repeated float embedding = 4;
}

The @Vector annotation requires a dimension parameter and an optional similarity parameter. Supported similarity algorithms are L2, COSINE, INNER_PRODUCT, and MAX_INNER_PRODUCT.

Basic Queries

Use FROM to query all entries of a type:

result, err := cache.Query(ctx, "FROM example.Person")

Use WHERE with named parameters to filter results. Bind parameter values with WithQueryParam:

result, err := cache.Query(ctx, "FROM example.Person WHERE age > :minAge",
    hotrod.WithQueryParam("minAge", int32(25)),
)

Use SELECT to project specific fields:

result, err := cache.Query(ctx, "SELECT name, age FROM example.Person WHERE age > :minAge",
    hotrod.WithQueryParam("minAge", int32(25)),
)

Use ORDER BY to sort results:

result, err := cache.Query(ctx, "FROM example.Person ORDER BY age DESC")

Supported parameter types are string, int32, int, int64, float64, float32, []float32, and bool.

Pagination

Use WithQueryMaxResults and WithQueryStartOffset to paginate through large result sets:

result, err := cache.Query(ctx, "FROM example.Person ORDER BY age",
    hotrod.WithQueryMaxResults(10),
    hotrod.WithQueryStartOffset(0),
)
fmt.Printf("returned=%d total=%d exact=%v\n",
    result.NumResults, result.HitCount, result.HitCountExact)

The QueryResult struct contains:

NumResults

The number of results returned in this page.

HitCount

The total number of matching entries across all pages.

HitCountExact

Whether HitCount is an exact count or an approximation.

Entries

The decoded result entries as []QueryEntry.

ProjectionSize

The number of projected fields per result, or zero if no projection is used.

Decoding Results

The client automatically unwraps query results into QueryEntry values. Each entry in result.Entries contains either entity data or projection columns, depending on the query type.

Entity Queries

For queries without SELECT, each QueryEntry has TypeName (the protobuf type name) and Value (the raw protobuf bytes):

result, err := cache.Query(ctx, "FROM example.Person")
if err != nil {
    log.Fatal(err)
}
for _, entry := range result.Entries {
    fmt.Printf("type: %s\n", entry.TypeName)
    // Decode entry.Value with a protobuf library or manually.
    // Field 1 (name) is a length-delimited string.
    // Field 2 (age) is a varint.
}

Projection Queries

For queries with SELECT, each QueryEntry has Projections, a slice of Go-typed values corresponding to each projected column:

result, err := cache.Query(ctx,
    "SELECT name, age FROM example.Person WHERE age > :minAge",
    hotrod.WithQueryParam("minAge", int32(25)),
)
for _, entry := range result.Entries {
    fmt.Printf("name=%s age=%d\n", entry.Projections[0], entry.Projections[1])
}

Projection values are automatically unwrapped to native Go types: string, int32, int64, float32, float64, bool, or []byte for nested messages.

See the query and vector-search tutorials for complete working examples.

Nested Type Queries

You can query across nested message fields using dot notation:

result, err := cache.Query(ctx,
    "FROM example.PersonWithAddress WHERE addresses.city = :city",
    hotrod.WithQueryParam("city", "London"),
)

You can also combine conditions across the nested type:

result, err := cache.Query(ctx,
    "FROM example.PersonWithAddress WHERE addresses.city = :city AND age > :minAge",
    hotrod.WithQueryParam("city", "London"),
    hotrod.WithQueryParam("minAge", int32(25)),
)

Spatial Queries

Spatial queries search for entries based on geographic coordinates. The following predicates are available for fields annotated with @GeoPoint.

Within Circle

Find entries within a radius from a center point. The default distance unit is meters:

result, err := cache.Query(ctx,
    "FROM example.Restaurant r WHERE r.location within circle(41.91, 12.46, :distance)",
    hotrod.WithQueryParam("distance", float64(100)),
)

Specify a unit (km, mi, yd, nm) after the distance value:

result, err := cache.Query(ctx,
    "FROM example.Restaurant r WHERE r.location within circle(41.91, 12.46, :distance km)",
    hotrod.WithQueryParam("distance", float64(0.1)),
)

Within Box

Find entries within a bounding rectangle defined by top-left and bottom-right corners:

result, err := cache.Query(ctx,
    "FROM example.Restaurant r WHERE r.location within box(41.91, 12.45, 41.90, 12.46)",
)

The arguments are top-left latitude, top-left longitude, bottom-right latitude, and bottom-right longitude.

Within Polygon

Find entries within an arbitrary polygon defined by a list of vertices:

result, err := cache.Query(ctx,
    "FROM example.Restaurant r WHERE r.location within polygon((41.91, 12.45), (41.91, 12.46), (41.90, 12.46), (41.90, 12.45))",
)

Distance Projection and Ordering

Use distance() to calculate the distance from a point, for sorting or projection:

result, err := cache.Query(ctx,
    "SELECT r.name, distance(r.location, 41.91, 12.46) FROM example.Restaurant r ORDER BY distance(r.location, 41.91, 12.46)",
)

Vector queries find the nearest neighbors to a query vector using the <→ operator.

Basic kNN Query

result, err := cache.Query(ctx,
    "FROM example.Book b WHERE b.embedding <-> [:v]~:k",
    hotrod.WithQueryParam("v", []float32{0.9, 0.1, 0.1}),
    hotrod.WithQueryParam("k", int32(3)),
)

The [:v] parameter is the query vector, and :k is the number of nearest neighbors to return.

Score Projection

Use score() to include the similarity score in the results:

result, err := cache.Query(ctx,
    "SELECT b.title, b.genre, score(b) FROM example.Book b WHERE b.embedding <-> [:v]~:k",
    hotrod.WithQueryParam("v", []float32{0.9, 0.1, 0.1}),
    hotrod.WithQueryParam("k", int32(3)),
)

Hybrid: Vector with Filtering

Combine kNN search with traditional filters using the filtering clause:

result, err := cache.Query(ctx,
    "SELECT score(b), b.title, b.genre FROM example.Book b WHERE b.embedding <-> [:v]~:k filtering (b.genre = 'Science Fiction')",
    hotrod.WithQueryParam("v", []float32{0.9, 0.1, 0.1}),
    hotrod.WithQueryParam("k", int32(3)),
)

Hybrid: Vector with Full-Text

Use the : operator inside a filtering clause for full-text search combined with vector similarity:

result, err := cache.Query(ctx,
    "SELECT score(b), b.title FROM example.Book b WHERE b.embedding <-> [:v]~:k filtering b.description : 'adventure'",
    hotrod.WithQueryParam("v", []float32{0.1, 0.1, 0.9}),
    hotrod.WithQueryParam("k", int32(5)),
)

Continuous Queries

Overview

Continuous queries use the Ickle query language to receive events when cache entries match or stop matching a query predicate.

Note

Continuous queries require an indexed cache with a registered protobuf schema.

Call ContinuousQuery on a RemoteCache to register a continuous query:

cq, err := cache.ContinuousQuery(ctx, "FROM example.Person WHERE age >= :minAge",
	hotrod.WithCQParam("minAge", int32(18)),
)
if err != nil {
	log.Fatal(err)
}
defer cache.RemoveContinuousQuery(ctx, cq)

for event := range cq.Events {
	switch event.Type {
	case hotrod.CQJoining:
		fmt.Printf("joined: key=%s\n", event.Key)
	case hotrod.CQUpdated:
		fmt.Printf("updated: key=%s\n", event.Key)
	case hotrod.CQLeaving:
		fmt.Printf("left: key=%s\n", event.Key)
	}
}

Each CQEvent contains:

  • Type — One of CQJoining (entry now matches), CQUpdated (matching entry was modified), or CQLeaving (entry no longer matches).

  • Key — The key of the affected entry.

  • Value — The serialized value (present for joining and updated events).

  • Projections — Projected field values, if the query uses a projection.

Continuous Query Options

WithCQParam(name string, value any)

Adds a named parameter binding to the Ickle query. Supported value types are string, int32, and int64.

WithCQChannelSize(n int)

Sets the buffer size of the continuous query event channel. Defaults to 64.

Client Intelligence

Intelligence Levels

  • Basic (IntelligenceBasic) — Connects to a single server. No topology awareness.

  • Topology-aware (IntelligenceTopologyAware) — Discovers cluster members via topology updates.

  • Hash-distribution-aware (IntelligenceHashDistAware) — Routes operations to the primary owner of the key based on consistent hashing. This is the default.

Set the intelligence level with WithClientIntelligence:

client, err := hotrod.NewClient(ctx, uri,
	hotrod.WithClientIntelligence(hotrod.IntelligenceTopologyAware),
)

Topology Inspection

You can inspect the current cluster topology known to the client:

TopologyServers()

Returns the list of server addresses known from the current cluster topology.

HasConsistentHash(cacheName string)

Reports whether the client has received a consistent hash topology for the named cache.

ConsistentHashOwnerCount(cacheName string)

Returns the number of distinct owners in the consistent hash for the named cache.

servers := client.TopologyServers()
fmt.Printf("cluster members: %v\n", servers)

if client.HasConsistentHash("my-cache") {
	owners := client.ConsistentHashOwnerCount("my-cache")
	fmt.Printf("owner count: %d\n", owners)
}

Multimap Caches

A multimap cache allows multiple values to be associated with a single key. Call client.Multimap("name") to obtain a MultimapCache handle.

Creating a Multimap Handle

mm := client.Multimap("my-multimap")

By default, duplicate key-value pairs are not allowed. Use WithSupportsDuplicates to permit storing the same value multiple times under the same key:

mm := client.Multimap("my-multimap",
	hotrod.WithSupportsDuplicates(true),
)

Storing and Retrieving Values

Use Put to add one or more values under a key. Each value is stored as a separate entry:

err := mm.Put(ctx, []byte("colors"), []byte("red"), []byte("blue"), []byte("green"))

Use Get to retrieve all values for a key:

values, err := mm.Get(ctx, []byte("colors"))
for _, v := range values {
	fmt.Println(string(v))
}

Use PutWithOptions to add values with expiration settings:

err := mm.PutWithOptions(ctx, []byte("colors"),
	[][]byte{[]byte("red"), []byte("blue")},
	[]hotrod.PutOption{hotrod.WithLifespan(30 * time.Second)},
)

Metadata

Use GetWithMetadata to retrieve values along with server-side metadata:

coll, found, err := mm.GetWithMetadata(ctx, []byte("colors"))
if found {
	fmt.Printf("values=%d version=%d\n", len(coll.Values), coll.Version)
}

Removing Entries

RemoveKey(ctx, key)

Removes all values associated with a key. Returns true if the key existed.

RemoveEntry(ctx, key, value)

Removes a specific key-value pair. Returns true if the pair existed.

removed, err := mm.RemoveKey(ctx, []byte("colors"))
removed, err = mm.RemoveEntry(ctx, []byte("colors"), []byte("red"))

Checking Contents

ContainsKey(ctx, key)

Reports whether the multimap contains any values for the given key.

ContainsValue(ctx, value)

Reports whether the multimap contains the given value under any key.

ContainsEntry(ctx, key, value)

Reports whether the multimap contains the specific key-value pair.

Size(ctx)

Returns the total number of key-value pairs across all keys.

hasKey, err := mm.ContainsKey(ctx, []byte("colors"))
hasEntry, err := mm.ContainsEntry(ctx, []byte("colors"), []byte("red"))
total, err := mm.Size(ctx)

Cache Administration

The CacheAdmin API provides cache lifecycle operations. These operations are executed as server tasks through the Hot Rod Exec operation.

admin := client.Administration()

// Create a cache with XML configuration.
err := admin.CreateCache(ctx, "my-cache", `<local-cache/>`)

// Create a cache only if it does not already exist.
err = admin.GetOrCreateCache(ctx, "my-cache", `<local-cache/>`)

// Remove a cache and all its data.
err = admin.RemoveCache(ctx, "my-cache")

Use CreateCacheWithFlags to pass admin flags that control creation behavior:

  • AdminFlagPermanent — The cache configuration survives server restarts.

  • AdminFlagVolatile — The cache configuration is not persisted.

err := admin.CreateCacheWithFlags(ctx, "my-cache", `<local-cache/>`,
	hotrod.AdminFlagPermanent,
)

Distributed Counters

The CounterManager API provides operations for managing distributed counters. Counters are cluster-wide and not associated with any specific cache.

Counter Types

  • Strong (CounterStrong) — Supports all operations including compare-and-swap and get-and-set.

  • Weak (CounterWeak) — Optimized for high write throughput. Does not support compare-and-swap or get-and-set.

Counter Storage

  • Volatile (StorageVolatile) — The counter value is lost on server restart.

  • Persistent (StoragePersistent) — The counter value survives server restarts.

Defining Counters

counters := client.Counters()

created, err := counters.Define(ctx, "my-counter", &hotrod.CounterConfiguration{
	Type:         hotrod.CounterStrong,
	Storage:      hotrod.StoragePersistent,
	Bounded:      true,
	LowerBound:   0,
	UpperBound:   1000,
	InitialValue: 0,
})

Managing Counters

// List all counter names.
names, err := counters.Names(ctx)

// Check if a counter exists.
exists, err := counters.IsDefined(ctx, "my-counter")

// Get the configuration of a counter.
config, err := counters.GetConfiguration(ctx, "my-counter")

// Remove a counter. It is re-created if accessed again.
err = counters.Remove(ctx, "my-counter")

Counter Operations

Obtain a Counter handle to perform operations on a specific counter:

counter := counters.Counter("my-counter")

// Get the current value.
val, err := counter.Get(ctx)

// Add a delta and get the new value.
newVal, err := counter.AddAndGet(ctx, 5)

// Set a new value and get the previous value.
prev, err := counter.GetAndSet(ctx, 100)

// Atomic compare-and-swap.
oldVal, swapped, err := counter.CompareAndSwap(ctx, 100, 200)

// Reset to the initial value.
err = counter.Reset(ctx)

Counter Listeners

You can register listeners to receive notifications when a counter value changes:

listener, err := counter.AddListener(ctx)
if err != nil {
	log.Fatal(err)
}
defer counter.RemoveListener(ctx, listener)

for event := range listener.Events {
	fmt.Printf("counter=%s old=%d new=%d\n", event.Name, event.OldValue, event.NewValue)
}

Each CounterEvent contains:

  • Name — The counter name.

  • OldValue and NewValue — The previous and current counter values.

  • OldState and NewState — The previous and current counter states (CounterStateValid, CounterStateLowerBound, or CounterStateUpperBound).

Iteration

You can iterate over all entries in a cache using the Iterator method. The iterator fetches entries in batches from the server.

iter, err := cache.Iterator(ctx)
if err != nil {
	log.Fatal(err)
}
defer iter.Close()

for iter.Next() {
	key, value := iter.Entry()
	fmt.Printf("key=%s value=%s\n", key, value)
}
if err := iter.Err(); err != nil {
	log.Fatal(err)
}

Iterator Options

WithIteratorBatchSize(n int)

Sets the number of entries fetched per server round-trip. Defaults to 1000.

WithIteratorMetadata()

Requests that the server include entry metadata (creation time, lifespan, version) with each entry.

iter, err := cache.Iterator(ctx,
	hotrod.WithIteratorBatchSize(500),
	hotrod.WithIteratorMetadata(),
)

for iter.Next() {
	key, value := iter.Entry()
	md := iter.Metadata()
	if md != nil {
		fmt.Printf("key=%s version=%d created=%d\n", key, md.Version, md.Created)
	}
}

Building and Testing

Building

go build ./...

Unit Tests

go test -short ./...

Integration Tests

Integration tests require Docker for testcontainers:

go test -timeout 120s ./test/...