Your first bucket
Time to make the INVENTORY bucket real. The inventory service keeps a
stock count for each SKU, and a bucket is where those counts live. This
page creates the bucket with one command, puts a count, gets it back, and
reads the bucket's status, and does nothing more than that.
The previous chapter gave you JetStream. A bucket rides on top of it: a
bucket is a JetStream stream the key-value API creates and configures
for you, so you never write the stream by hand. You ask for a bucket, and the
server stores one stream named KV_INVENTORY behind the friendly name
INVENTORY. The stream is the topic of the under the
hood page; here you only need to know it
exists.
Create the bucket
A running nats-server with JetStream enabled is the one prerequisite.
The key-value API is part of JetStream, so without -js the next command
has nothing to talk to:
nats-server -js
In another terminal, create the bucket:
- CLI
- C
#!/bin/bash
# Create the INVENTORY bucket. A bucket is created as a JetStream stream,
# so this one command sets up the backing stream KV_INVENTORY on the
# subjects $KV.INVENTORY.>.
#
# --history 1 keeps only the current value of each key. We raise this
# later on the history-and-revisions page; 1 is the default and is all
# the inventory service needs to start.
nats kv add INVENTORY --history 1
# Expected output is the bucket's status, ending with its configuration
# (labels abbreviated here):
#
# Information for Key-Value Store Bucket INVENTORY created <time>
#
# Configuration:
#
# Bucket Name: INVENTORY
# History Kept: 1
# JetStream Stream: KV_INVENTORY
# ...
// Create the INVENTORY bucket. The server stores it as the
// JetStream stream KV_INVENTORY behind the friendly name.
kvConfig cfg;
kvConfig_Init(&cfg);
cfg.Bucket = "INVENTORY";
// Keep only the current value of each key. This is the default;
// the history-and-revisions page raises it.
cfg.History = 1;
s = js_CreateKeyValue(&kv, js, &cfg);
if (s == NATS_OK)
printf("Created bucket %s\n", kvStore_Bucket(kv));
Two parts of this command matter. The first is the bucket name: INVENTORY. Bucket names are
case-sensitive identifiers, and they show up in every command and every
error message in this chapter. The name maps straight onto the backing
stream: INVENTORY becomes KV_INVENTORY.
The second is --history 1. History is how many prior values the
bucket keeps for each key. One means the bucket holds only the current
value of a key and forgets the rest. That's the default and all the
inventory service needs to start. The depth can go as high as 64, but no
higher; History and revisions raises it so a key
remembers where it's been, and for now, one is enough.
You didn't set any other configuration. A bucket has the same long list
of stream knobs underneath, all filled with sensible defaults. The full
set of bucket configuration options lives in Reference → Create
Stream, since a bucket is created
as a stream. We use only History here.
Put a value, get an entry
The bucket is empty. Put the first stock count into it. The key is the SKU, and the value is the count stored as bytes:
- CLI
- C
#!/bin/bash
# Put the stock count for widget-blue into the bucket. The key is the SKU,
# the value is the count as bytes. A put is unconditional: it writes the
# value whether or not the key already exists.
#
# This is the inventory service recording that there are 42 widget-blue
# units in stock.
nats kv put INVENTORY widget-blue 42
# The put API returns the new revision to client code; the CLI just echoes
# the value it stored. Because INVENTORY is empty, this first write lands at
# revision 1, which you can confirm with `nats kv get`. The output here is
# only the value:
#
# 42
// Put the stock count for widget-blue. The key is the SKU, the
// value is the count as bytes. A put is unconditional: it writes
// whether or not the key already exists.
uint64_t rev = 0;
s = kvStore_PutString(&rev, kv, "widget-blue", "42");
if (s == NATS_OK)
{
// The bucket assigns each write a revision from a single
// counter. INVENTORY is empty, so this first write lands at 1.
printf("Stored widget-blue at revision %llu\n",
(unsigned long long) rev);
}
That's a put: an unconditional write. It stores the value whether or
not the key already exists. Each write also gets a revision: a number
the bucket assigns from a single counter it keeps across every key, not a
per-key count. Because INVENTORY is empty, this first write lands at
revision 1; in a bucket that already holds other keys, the same put would
take the next number in the bucket's sequence instead. The put API returns
that revision to client code; the CLI prints only the value you stored.
Revisions are how the bucket tracks change over time, and
History and revisions builds on them.
Now read it back:
- CLI
- C
#!/bin/bash
# Get widget-blue back. By default a get returns the whole entry: the
# value plus its revision and timestamp. --raw asks for only the value
# bytes, which is what a program usually wants.
nats kv get INVENTORY widget-blue --raw
# Prints just the value:
#
# 42
# Without --raw you see the full entry the server returns:
nats kv get INVENTORY widget-blue
# INVENTORY > widget-blue created @ ...
#
# 42
#
# A get for a key that was never put fails with a key-not-found error,
# which is distinct from a key whose value is empty. Check for that error
# before using the value:
nats kv get INVENTORY widget-green --raw || echo "widget-green not in stock yet"
// A get returns an entry, not a bare value: the value together
// with its revision and the time it was written.
kvEntry *e = NULL;
s = kvStore_Get(&e, kv, "widget-blue");
if (s == NATS_OK)
{
printf("value: %.*s\n",
kvEntry_ValueLen(e), (const char*) kvEntry_Value(e));
printf("revision: %llu\n",
(unsigned long long) kvEntry_Revision(e));
printf("created: %lld (unix seconds)\n",
(long long) (kvEntry_Created(e) / 1000000000LL));
kvEntry_Destroy(e);
e = NULL;
}
// A key that was never put fails with NATS_NOT_FOUND, which is
// distinct from a key whose value is empty. Check for absence
// instead of treating a missing SKU as "the count is zero".
s = kvStore_Get(&e, kv, "widget-green");
if (s == NATS_NOT_FOUND)
{
printf("widget-green not in stock yet\n");
s = NATS_OK;
}
A get returns an entry rather than a bare
value: the value together with its revision and
the time it was written. The CLI's --raw flag strips the entry down to
just the value bytes (42), which is usually what a program wants, but the
full object is what the server actually sends.
That shape is intentional, because the inventory service rarely wants only the count; it wants the count and the revision, because history and revisions uses that revision to decrement the value safely. The entry carries both in one read, so you never have to make a second call to learn which revision you just saw.
The timestamp on the entry matters too. It records when the value was written, not when you read it, so the inventory service can tell a count taken seconds ago from one that has sat untouched for a week. You get all three facts — value, revision, and write time — from the single get you already made.
Read the bucket's status
One command summarizes the bucket as a whole:
- CLI
- C
#!/bin/bash
# Ask the server about the bucket as a whole. Status reports the bucket's
# configuration and how many values it currently holds.
nats kv status INVENTORY
# Expected output reports the bucket name, history depth, value count, and
# the backing stream (labels abbreviated here):
#
# Information for Key-Value Store Bucket INVENTORY created <time>
#
# Configuration:
#
# Bucket Name: INVENTORY
# History Kept: 1
# Values Stored: 1
# Backing Store Kind: JetStream
# JetStream Stream: KV_INVENTORY
#
# The "JetStream Stream" line names the stream under the bucket:
# KV_INVENTORY. That is the proof the bucket is a stream; the
# under-the-hood page opens it up.
// Ask the server about the bucket as a whole: the history depth
// you set and how many values it currently holds.
kvStatus *sts = NULL;
s = kvStore_Status(&sts, kv);
if (s == NATS_OK)
{
printf("Bucket: %s\n", kvStatus_Bucket(sts));
printf("History kept: %lld\n",
(long long) kvStatus_History(sts));
printf("Values stored: %llu\n",
(unsigned long long) kvStatus_Values(sts));
// A bucket is a stream with a friendlier name: the backing
// stream is KV_<bucket>, here KV_INVENTORY.
printf("Backing stream: KV_%s\n", kvStatus_Bucket(sts));
kvStatus_Destroy(sts);
}
The status reports the bucket name, the history depth you set, and how many
values it holds. It also reports the backing stream: the stream the
bucket is built on, named KV_INVENTORY. That line is your first concrete
proof that a bucket is a stream with a friendlier name. The under the
hood page opens that stream and reads
it directly.
Pitfalls
Two mistakes are common on your very first bucket, and each is easy to avoid once you've seen it.
A get returns an entry rather than a value, and a missing key is signaled distinctly from an empty one. Reaching straight for the value bytes works only when the key exists. A key that was never put doesn't return an empty entry: the CLI and some clients (Go, Python, C#) fail with a key-not-found error, while others (JavaScript, Rust, Java) return null or None. Either way, absence is reported as its own thing, separate from a value that happens to be empty: an empty value is a value, and a missing key is the absence of one. Don't treat a missing key as "the count is zero." The get example above ends with exactly this case: its last line gets a SKU that was never stocked, the get signals absence, and the program decides what a missing SKU means instead of reading a stale or zero count by accident. Check for absence first, then read the value.
Bucket and key names are validated. A bucket name may contain only
letters, digits, dash, and underscore, and it can't be empty. A key is
more permissive (letters, digits, and the characters -, /, _, =,
and .), but nothing beyond that set, no leading or trailing dot, and no
two dots in a row (a..b is rejected even though a.b is fine). An order
id like ord:8w2k has a colon, so it can't be a key; the client library
validates the name and rejects the write before it's sent, rather than
storing a broken key. Pick names from the allowed set, and reach for an
underscore or dash where you'd have used a colon:
- CLI
- C
#!/bin/bash
# Key and bucket names are validated. A bucket name may use only letters,
# digits, dash, and underscore. A key may use letters, digits, and the
# characters - / _ = . — and nothing else.
#
# An order id like "ord:8w2k" has a colon, which is not allowed as a key.
# The client library validates the name and rejects the put before it is
# sent, so nothing is written to the bucket:
nats kv put INVENTORY "ord:8w2k" 42 || echo "rejected: ':' is not a legal key character"
# A legal key with the same intent uses an allowed separator, e.g.
#
# nats kv put INVENTORY ord_8w2k 42
#
# We don't run that against INVENTORY here: it's the running example for the
# whole chapter and should still hold only widget-blue at this point.
// "ord:8w2k" has a colon, which is not a legal key character.
// The client validates the key and rejects the put before
// anything is sent, so nothing is written to the bucket.
uint64_t rev = 0;
s = kvStore_PutString(&rev, kv, "ord:8w2k", "42");
if (s == NATS_INVALID_ARG)
{
printf("rejected: ':' is not a legal key character\n");
// A legal key with the same intent uses an allowed
// separator, e.g. "ord_8w2k".
s = NATS_OK;
}
Where you are
You now have:
- An
INVENTORYbucket, backed by the streamKV_INVENTORY. - The key
widget-blueholding the value42at revision 1. - The ability to put a value, get back its entry, and read the bucket's status.
What's next
The next page puts the warehouse dashboard on the bucket: a watch that streams every stock change live, starting with a snapshot of what's already there.
Continue to Watching.
See also
- Reference → Create Stream — every configuration option a bucket inherits from its backing stream.
- Core Concepts → JetStream — the storage layer a bucket is built on.
- Watching — stream live changes off the bucket.