JetStream KV parece un HashMap distribuido. No lo es.
Un bucket de KV es un stream de JetStream con una configuración especial (KV_<bucket>). Cada clave es un subject de NATS ($KV.<bucket>.<key>). El valor almacenado es el mensaje más reciente de ese subject. Para los sistemas orientados a eventos, el modelo mental más útil es: observar los cambios, diseñar las claves como jerarquías y representar los tipos de entidad como prefijos de clave, no como buckets separados.
No hay una cláusula WHERE. Las consultas se hacen por clave, mediante comodines en los prefijos de clave o con claves de índice mantenidas por la aplicación.
Requisitos previos: Core NATS (Parte 1), JetStream (Parte 2). Investigación basada en docs.nats.io y nats.go kv.go el July 6, 2026.
TL;DR
- Bucket
X→ streamKV_X→ subjects$KV.X.> - Clave
order.42→ subject$KV.orders.order.42→ mensaje más reciente = valor actual watches la vía nativa de lectura reactiva: entrega eventos PUT, DELETE y PURGE en tiempo real- Los tipos de entidad van en prefijos de clave (
orders-v1.<id>), no en buckets separados - KV garantiza lecturas y escrituras monotónicas por clave; no garantiza leer las propias escrituras en las solicitudes get atendidas por seguidores
- Los índices secundarios son un patrón de aplicación: mantén las claves de índice al escribir y límpialas al eliminar
Qué aprenderás aquí
- Cómo se asignan los buckets a streams y subjects
- Por qué KV es un stream de cambios, no una tabla consultable
- Cómo diseñar prefijos de clave para buckets con múltiples entidades
- Consultas sin SQL: comodines,
ListKeysFilteredy el patrón de índice secundario - Ciclo de vida de watch: snapshot, centinela y actualizaciones en vivo
- Criterios para elegir KV: cuándo usarlo y cuándo elegir otra opción
- Una implementación concreta con watch y claves de índice
Los buckets son streams
Cuando creas un bucket de KV, JetStream materializa un stream:
nats kv add app-objects --history 5 --ttl 1h
nats kv status app-objects
# JetStream Stream: KV_app-objects
nats stream info KV_app-objects
# Subjects: $KV.app-objects.>
| Concepto | Implementación |
|---|---|
| Nombre del bucket | app-objects |
| Stream subyacente | KV_app-objects |
| Patrón de subject | $KV.app-objects.> |
| Clave individual | $KV.app-objects.<key> |
| Valor más reciente | Mensaje más reciente de ese subject (MaxMsgsPerSubject, valor predeterminado 1) |
kv.Put("orders-v1.abc-123", json)
│
v
PUBLISH → $KV.app-objects.orders-v1.abc-123
│
v
Stream KV_app-objects (retention, replicas, limits apply)
│
v
kv.Get("orders-v1.abc-123") = last message on that subject
Regla de diseño de claves: Cada bucket es un dominio de consistencia. Coloca los tipos de entidad en prefijos de clave, no en buckets separados, a menos que tengas una razón de peso para aislarlos.
Bucket: app-objects (one stream, shared replication and limits)
orders-v1.{orderId} ← entity type as prefix
customers-v1.{customerId}
idx.status.active.{orderId} ← secondary index (app-maintained)
Observa todos los pedidos:
Subscribe / Watch: $KV.app-objects.orders-v1.>
Esto usa las reglas de comodines de Core NATS de la Parte 1: > coincide con todos los tokens restantes.
KV como stream de cambios
La API más importante para los sistemas reactivos es watch, no get.
watch se suscribe a los cambios en las claves que coinciden. Usa un consumidor ordenado y entrega:
- Snapshot: el último valor de cada clave que coincide (a menos que se use
UpdatesOnly) - Centinela nil: indica que el snapshot inicial está completo
- Actualizaciones en vivo: PUT, DELETE y PURGE a medida que ocurren
watcher, _ := kv.Watch(ctx, "orders-v1.>", jetstream.UpdatesOnly())
defer watcher.Stop()
for entry := range watcher.Updates() {
if entry == nil {
continue // initial snapshot complete
}
switch entry.Operation() {
case jetstream.KeyValuePut:
log.Printf("PUT %s rev=%d", entry.Key(), entry.Revision())
projectOrder(entry.Key(), entry.Value())
case jetstream.KeyValueDelete, jetstream.KeyValuePurge:
log.Printf("DEL %s rev=%d", entry.Key(), entry.Revision())
removeOrder(entry.Key())
}
}
JetStream KV Mental Model
========================
kv.Put(key, val) ──► subject $KV.bucket.key ──► stream KV_bucket
│
┌─────────────────────────────────────────┤
│ │
v v
kv.Get(key) kv.Watch("prefix.>")
"latest value view" "change stream view"
(point read) (reactive projection)
Opinión editorial: Si tu servicio consulta KV repetidamente en un bucle, estás yendo contra la abstracción. Watch es el punto de integración nativo para los proyectores y los procesos que precargan cachés.
Modelo de consistencia
Según la documentación oficial de KV:
- KV garantiza consistencia inmediata para escrituras y lecturas monotónicas por clave
- KV no garantiza leer las propias escrituras en solicitudes directas de
getatendidas por seguidores o mirrors - Para obtener lecturas con garantías más estrictas, dirige las solicitudes get al líder del stream
La consistencia entre KV y una base de datos primaria o cualquier fuente de verdad externa sigue siendo eventual: KV es una proyección, no la fuente de verdad.
Consultas sin WHERE
KV no tiene una API de consultas al estilo SQL. Tienes tres opciones prácticas:
1. Comodines en prefijos de clave
Diseña las claves como jerarquías separadas por puntos y observa o lista por prefijo:
// All active orders — if keys are designed as orders-v1.{status}.{id}
watcher, _ := kv.Watch(ctx, "orders-v1.*")
// nats.js
const keys = await kv.keys("orders-v1.*");
2. ListKeysFiltered (paquete jetstream de Go)
El paquete jetstream de Go (December 2024+) permite listar claves con filtros:
lister, _ := kv.ListKeysFiltered(ctx, "orders-v1.*")
for key := range lister.Keys() {
entry, _ := kv.Get(ctx, key)
process(entry.Value())
}
lister.Stop()
Nota: Esta API está centrada en Go a July 2026. Como alternativas portables, puedes usar Watch con MetaOnly o keys() con comodines en nats.js.
3. Patrón de índice secundario (mantenido por la aplicación)
Mantén claves adicionales que actúen como índices al escribir:
func upsertOrder(ctx context.Context, kv jetstream.KeyValue, o Order) error {
id := o.ID
payload, _ := json.Marshal(o)
// Primary record
if _, err := kv.Put(ctx, "orders-v1."+id, payload); err != nil {
return err
}
// Index keys — your "WHERE clause"
idxKey := fmt.Sprintf("idx.status.%s.%s", o.Status, id)
if _, err := kv.Put(ctx, idxKey, []byte(id)); err != nil {
return err
}
return nil
}
// Query active orders
lister, _ := kv.ListKeysFiltered(ctx, "idx.status.active.*")
Este es un patrón de aplicación, no una funcionalidad de NATS. Debes:
- Actualizar las claves de índice cuando cambie el estado (eliminar la clave de índice anterior y escribir la nueva)
- Limpiar las claves de índice al eliminar
- Hacer que las operaciones de escritura sean idempotentes: los puts duplicados no deben dejar claves de índice obsoletas
Implementación concreta: precarga de caché mediante watch
Un servicio que mantiene precargada una caché en memoria a partir de los cambios en KV:
import { connect, StringCodec } from "nats";
const nc = await connect({ servers: process.env.NATS_URL });
const js = nc.jetstream();
const kv = await js.views.kv("app-objects");
const cache = new Map<string, object>();
const sc = StringCodec();
const watcher = await kv.watch({ key: "orders-v1.>" });
(async () => {
for await (const entry of watcher) {
if (entry === null) {
console.log("snapshot complete, cache size:", cache.size);
continue;
}
const key = entry.key;
if (entry.operation === "DEL" || entry.operation === "PURGE") {
cache.delete(key);
continue;
}
cache.set(key, JSON.parse(sc.decode(entry.value)));
}
})();
// Fast read path — from memory, not KV
function getOrder(id: string): object | undefined {
return cache.get(`orders-v1.${id}`);
}
Combina esto con bloqueo optimista para la coordinación:
// Leader election / lock — only one worker runs the export
rev, err := kv.Create(ctx, "lock.export."+jobID, []byte(workerID))
if err != nil {
return ErrAlreadyRunning
}
defer kv.Delete(ctx, "lock.export."+jobID)
Criterios para elegir KV
Elige KV cuando necesites estado compartido, persistente y observable, con acceso por clave y una vía de lectura reactiva (watch). Elige una base de datos, Object Store o un stream sin procesar cuando la carga de trabajo requiera consultas relacionales, blobs grandes, historial ilimitado o almacenamiento de entidades como fuente de verdad.
| Usa KV para | Usa otra opción para |
|---|---|
| Configuración compartida y feature flags | Consultas relacionales sobre campos arbitrarios |
| Bloqueos distribuidos y coordinación con CAS | Búsqueda de texto completo (→ OpenSearch) |
| Lecturas por clave de blobs pequeños con baja latencia | Almacenamiento de archivos grandes (→ Object Store) |
| Proyecciones y precarga de caché mediante watch | Fuente de verdad para entidades de negocio |
| Estado compartido entre servicios con historial ≤ 64 revisiones | Registro de auditoría con historial ilimitado (→ stream sin procesar) |
KV almacena proyecciones y estado de coordinación, no superficies de consulta ad hoc ni la fuente de verdad del dominio. Tu base de datos primaria o servicio de dominio mantiene la fuente de verdad; KV es la copia observable a la que se suscriben otros servicios.
Errores comunes
- Un bucket por tipo de entidad. Genera sobrecarga operativa. Usa prefijos de clave en un solo bucket, a menos que el aislamiento requiera separarlos.
- Tratar
getcomo la integración principal. Watch es la vía reactiva;getsirve para lecturas puntuales. - Suponer que tiene semántica SQL. No hay joins,
WHEREni transacciones entre claves. - Ignorar el mantenimiento de índices. Las claves de índices secundarios quedan obsoletas si no se limpian al actualizar o eliminar.
- Confundir KV con la fuente de verdad. KV es una capa de proyección o caché. Tu base de datos primaria o servicio de dominio mantiene la fuente de verdad; consulta la Parte 4.
Siguiente en la serie: EDA con NATS: una fuente de verdad, múltiples modelos de lectura — proyecciones CQRS
Fuentes
- Almacén clave-valor — semántica de los buckets, watch, consistencia
- Uso de KV (guía para desarrolladores) — put, get, delete, keys, history
- Recorrido por KV — asignación de bucket a stream
- Streams — MaxMsgsPerSubject — retención por clave
- nats-io/nats.go — jetstream/kv.go — plantillas de subject
$KV,ListKeysFiltered, opciones de watch - Core NATS (Parte 1) — reglas de comodines para suscripciones
$KV.*.>