By default, when you pass standard Elixir data structures to Xitdb.transact/3 or put inside Xitdb.DSL.tx:
- Plain Elixir maps (
%{}) are stored as:hash_map(16-way HAMT). - Plain Elixir lists (
[]) are stored as:array_list(16-way Radix Trie). %MapSet{}values are stored as:counted_hash_set(16-way HAMT set with $O(1)$ count).
When your workload requires $O(1)$ map cardinality, lexicographical B+Tree key ordering and range scans, or $O(\log N)$ middle-of-list insertions and removals, wrap your data in Xitdb's explicit collection constructors (Xitdb.counted_map/1, Xitdb.sorted_map/1, Xitdb.linked_list/1, etc.) or initialize them with init_col in tx.
1. Persistent Collection Types & Complexity Matrix
xitdb provides 8 persistent, structurally shared collection types:
| Collection Kind | Constructor | Selector Sigil | Backing Structure | get / member? | put / append | insert / rm / slice | Enum.count/1 |
|---|---|---|---|---|---|---|---|
:hash_map | Xitdb.hash_map(map) (or %{}) | .key | 16-way HAMT | $O(\log_{16} N)$ | $O(\log_{16} N)$ | rm: $O(\log_{16} N)$ | $O(N)$ scan |
:counted_hash_map | Xitdb.counted_map(map) | #key | 16-way HAMT + count slot | $O(\log_{16} N)$ | $O(\log_{16} N)$ | rm: $O(\log_{16} N)$ | $O(1)$ |
:sorted_map | Xitdb.sorted_map(map) | $key | Count-augmented B+Tree | $O(\log N)$ | $O(\log N)$ | rm & slice: $O(\log N)$ | $O(1)$ |
:array_list | Xitdb.array_list(list) (or []) | [idx] | 16-way Radix Trie | $O(\log_{16} N)$ | $O(\log_{16} N)$ | Tail slice only; no middle insert/rm | $O(1)$ |
:linked_array_list | Xitdb.linked_list(list) | [[idx]] | Count-augmented B-Tree | $O(\log N)$ | $O(\log N)$ | Arbitrary insert, rm, slice: $O(\log N)$ | $O(1)$ |
:hash_set | Xitdb.hash_set(enum) | %member | 16-way HAMT set | $O(\log_{16} N)$ | $O(\log_{16} N)$ | rm: $O(\log_{16} N)$ | $O(N)$ scan |
:counted_hash_set | Xitdb.counted_set(enum) (or MapSet) | #%member | 16-way HAMT set + count | $O(\log_{16} N)$ | $O(\log_{16} N)$ | rm: $O(\log_{16} N)$ | $O(1)$ |
:sorted_set | Xitdb.sorted_set(enum) | ^member | Count-augmented B+Tree set | $O(\log N)$ | $O(\log N)$ | rm & slice: $O(\log N)$ | $O(1)$ |
2. Choosing the Right Collection Type
Maps: hash_map vs. counted_map vs. sorted_map
- Use
%{}/Xitdb.hash_map/1for entity attributes (%{"username" => "alice", "age" => 30}) and general key-value lookups where you don't needEnum.count/1or sorted key iteration. - Use
Xitdb.counted_map/1for entity tables ("departments","tenants") when you frequently callEnum.count(db["tenants"])and want $O(1)$ cardinality without B+Tree key sorting overhead. - Use
Xitdb.sorted_map/1for lexicographically ordered directories, time-series keys ("2026-11-03T22:00:00Z"), leaderboards, and secondary indexes where you need ordered iteration,Enum.slice/2pagination by rank, or Datalogstarts_with/>=B+Tree range pushdowns.
Lists: array_list vs. linked_list
- Use
[]/Xitdb.array_list/1for append-only logs, fixed-order arrays, or random index reads (roles[0]).Array_Listsupports fast tailappendand tail truncation (slice list, 0, new_size), but does not allow middle-of-list insertions or element deletion. - Use
Xitdb.linked_list/1for task queues, ordered feeds, or editable lists where items are inserted at index0(insert todos[[0]], "urgent"), removed by index (rm todos[[2]]), or sliced at arbitrary offsets (slice todos, 10..19) in $O(\log N)$ time without re-indexing subsequent elements.
Sets: hash_set vs. counted_set vs. sorted_set
- Use
Xitdb.counted_set/1(orMapSet.new/1) for user roles, permissions, or feature flags when you need $O(1)$Enum.count/1and $O(1)$Enum.member?/2. - Use
Xitdb.sorted_set/1when set members should always iterate and slice in deterministic alphabetical order (e.g., tags or badges).
use Xitdb.DSL
{:ok, db} = Xitdb.open(backend: :memory)
{:ok, db} =
Xitdb.transact(db, %{
"departments" => Xitdb.counted_map(%{"eng" => %{"budget" => 500_000}}),
"catalog" => Xitdb.sorted_map(%{"sku-001" => "Keyboard", "sku-002" => "Monitor"}),
"job_queue" => Xitdb.linked_list(["job-a", "job-b", "job-c"]),
"tags" => Xitdb.sorted_set(["rust", "elixir", "odin"])
})
Enum.to_list(db["tags"])
# => ["elixir", "odin", "rust"] (lexicographically ordered!)3. Secondary B+Tree Indexes (index_on)
xitdb can automatically maintain secondary Sorted_Map ($) indexes whenever records in a source map are inserted, updated, or deleted:
- Automatic population: Writing
users[id].username = "alice"automatically writes"alice" -> idinto the targetSorted_Mapindex ("username->id"). - Automatic stale-key eviction: Updating
users[id].usernamefrom"alice"to"alicia"—or deletingusers[id]viarm users[id]—automatically evicts the old"alice"key from"username->id"in the same atomic transaction.
You can declare index_on rules either inside Xitdb.DSL.tx or via index_rules: in Xitdb.transact/3:
use Xitdb.DSL
{:ok, db} =
tx db, doc: "seed indexed users" do
index_on :users, :username, "username->id"
index_on :users, :email, "email->id"
put users["u101"], %{"username" => "dan", "email" => "dan@acme.io"}
put users["u102"], %{"username" => "daria", "email" => "daria@acme.io"}
end
# Point lookup via the Sorted_Map secondary index:
user_id = db["username->id"]["dan"] # => "u101"
# Or in a single NIF hop with path/1:
Xitdb.select!(db, path(sorted("email->id")["daria@acme.io"])) # => "u102"4. Scalar Types & Custom 2-Byte Format Tags ({:tagged, tag, payload})
xitdb natively encodes scalars into 9-byte Slots (Short_Bytes inline up to 8 bytes, Bytes, Uint, Int, Float):
| Elixir Value | xitdb Storage Representation | Round-Trip Value |
|---|---|---|
"hello" | Tag.Short_Bytes ($\le 8$ bytes inline in Slot) or Tag.Bytes | "hello" |
42 (non-negative integer()) | Tag.Uint (u64) | 42 |
-42 (negative integer()) | Tag.Int (i64) | -42 |
3.14 (float()) | Tag.Float (f64) | 3.14 |
true / false | Tag.Short_Bytes with 2-byte format tag "bl" ("1" / "0") | true / false |
{:tagged, <<a, b>>, binary} | Tag.Bytes / Tag.Short_Bytes with custom 2-byte format_tag | {:tagged, <<a, b>>, binary} |
Custom 2-byte format tags let you store domain-typed scalars (such as UUIDs, decimals, timestamps, or CBOR/Protobuf payloads) without wrapper maps:
use Xitdb.DSL
{:ok, db} =
tx db do
put orders["ord-1"].total, {:tagged, "dc", "199.95"}
put orders["ord-1"].created_at, {:tagged, "dt", "2026-11-03T22:00:00Z"}
end
db["orders"]["ord-1"]["total"]
# => {:tagged, "dc", "199.95"}