CloudBitmaps architecture

Build walkthrough · Phases 1–4 complete

How it's built, one chunk at a time.

CloudBitmaps splits every bitmap into 16-bit chunks and spreads them across three storage tiers by temperature — hot RAM, a warm key-value store, and cold immutable object storage. This is the whole system, phase by phase: the diagram, a real code example, and the invariant each phase secured.

3storage tiers
11phases / sub-phases
385tests green
1runtime dependency

The whole picture

One bitmap, three temperatures

A Roaring Bitmap is already a set of 16-bit chunks: an ID in [0, 2³²) splits into a 16-bit chunk key (which chunk) and a 16-bit remainder (which bit inside it). CloudBitmaps takes that natural seam and maps each chunk onto whichever storage tier its access pattern deserves.

HOT · RAM (LRU) WARM · NoSQL deltas COLD · immutable .crbm

Two design rules hold the whole thing together, and every phase below respects them:

  • Tombstones are first-class. A delete isn't "rebuild the bitmap without this ID" — it's a removes entry in the warm delta. The effective set subtracts it at read time. That's what makes remove() O(one chunk write) over an immutable cold base.
  • The core is pure. core/ touches no clock, no randomness, and no cloud SDK — time, entropy, and all I/O are injected. A lint rule fails the build if that's ever violated. This is what lets a deterministic simulator replay any concurrency or crash bug from a single seed.

Everything else is drivers behind interfaces. IWarmDriver, IColdDriver, and IRegistryDriver each have in-memory, local-filesystem, and cloud implementations that all pass the same conformance suite — so you swap storage backends without the engine noticing.

the engine is wired once, from drivers
const store = new CloudRoaring({
  warm: new DynamoDbWarmDriver({ client: dynamo, tableName: 'cloudbitmaps' }),
  cold: s3Cold, // raw driver — the store builds the .crbm cold source
  registry,
  keystore,
});
// retries, backoff and the HOT cache are on by default — nothing else to wire.
const seg = store.segment('high-value-shoppers');

P1

Milestone M1 · local

The in-memory engine routing · tombstones · LRU · OCC

Phase 1 is the brain with no storage yet — just the in-memory drivers. It establishes the data model every later tier has to honor: route an ID to its chunk, keep deletes as tombstones, cache decoded chunks under a hard ceiling, and guard concurrent writers with optimistic concurrency.

SafeBitmap wraps the native roaring addon but only ever uses its safe deserializer behind a size cap — every byte from any tier is treated as untrusted input. The warm row for a chunk is a delta { adds, removes }; writes carry an opaque, never-reused OCC token, so two writers to the same chunk can't silently lose an update (one gets a WriteConflictError and re-reads).

zero-setup in-memory store — ideal for a first look or tests
import { CloudRoaring, MemoryWarmDriver, MemoryColdChunkSource } from '@cloudbitmaps/roaring';

const store = new CloudRoaring({
  warm: new MemoryWarmDriver(),
  cold: new MemoryColdChunkSource(),
});

const vips = store.segment('high-value-shoppers');

await vips.add(1_234_567_890);
await vips.addMany([5, 99_999, 2_000_000_000]); // grouped by chunk → one write per chunk
await vips.has(1_234_567_890);             // → true

await vips.remove(1_234_567_890);          // single-chunk tombstone
await vips.has(1_234_567_890);             // → false, immediately
await vips.count();                       // exact cardinality

Invariant secured

OR-merge can't delete — so deletes are tombstones, not rebuilds. addMany of 10,000 IDs spanning 12 chunks becomes 12 writes, not 10,000. Concurrent writers never lose an acknowledged update (OCC). Named property + concurrency tests pin both.

P2

Milestone M1 · local

Disk, the .crbm format & the simulator durability · CRC · determinism

Phase 2 makes state survive a restart and defines the on-disk cold format. A .crbm archive is a sequence of per-chunk roaring blobs followed by a footer index — every chunk, the index, and the footer each carry a CRC32C, so corruption is caught before a byte reaches the native decoder. Cold objects are generation-keyed and immutable: segment.<gen>.crbm, never overwritten in place.

This phase also builds the correctness moat: a FoundationDB-style deterministic simulator that runs the real core/ against fake in-memory drivers under a seeded scheduler, injecting latency, errors, reordering and crashes — and checks every result against an oracle (a trivial Set). A mismatch prints the seed; the seed replays the failure byte-for-byte.

same API, now durable on the local filesystem
import { CloudRoaring, LocalFsWarmDriver, LocalFsColdDriver } from '@cloudbitmaps/roaring';

const store = new CloudRoaring({
  warm: new LocalFsWarmDriver('./.cloudbitmaps/warm'),
  cold: new LocalFsColdDriver('./.cloudbitmaps/cold'),  // raw driver → reads .crbm generations
});

await store.segment('active-this-week').add(42);
// a fresh process pointed at the same dirs sees 42 again — writes are durable.

Invariant secured

Byte-exact round-trip, and no corrupt byte reaches the decoder. Property tests fuzz the codec against the oracle; golden files pin the v1 bytes; the simulator turns "zero data loss" from a hope into a seed-reproducible test.

P3

Milestone M2 · Topology-A

S3 cold & the crown jewel: chunk-skipping intersection serverless ∩

Phase 3 puts the cold tier on S3 (or any S3-compatible store like MinIO) and delivers the feature the whole project exists for. The S3ColdDriver lives at the @cloudbitmaps/roaring/s3 subpath, so the AWS SDK is an optional peer dependency — the core package never pulls it in. Generations are write-once via a conditional If-None-Match:* PUT.

The crown jewel is intersect(). To AND two segments, you only need the chunks whose 16-bit key appears in both — a key missing from either side contributes nothing and is never downloaded. So two billion-ID segments that barely overlap transfer almost nothing, and the whole thing streams under a bounded in-flight window: it runs in a small / serverless process regardless of segment size.

cold on S3, then stream the intersection of two segments
import { S3Client } from '@aws-sdk/client-s3';
import { S3ColdDriver } from '@cloudbitmaps/roaring/s3';

const cold = new S3ColdDriver({ client: new S3Client({ region: 'us-east-1' }),
                              bucket: 'my-bitmaps' });
const store = new CloudRoaring({ warm, cold });

const shoppers = store.segment('high-value-shoppers');
const active   = store.segment('active-this-week');

// stream the ids in BOTH segments — only overlapping chunks leave S3
for await (const id of shoppers.intersect([active])) { /* … */ }

// or materialize the result straight into a new segment
await shoppers.intersectInto(store.segment('campaign-targets'), [active]);

Invariant secured

Bounded memory and bounded transfer, always. Only chunks present in every operand are fetched; only a small window is resident. intersect is commutative — a.intersect([b]) ≡ b.intersect([a]) — and verified so by test.

Milestone M3 · Topology-B · the production cluster

Phase 4 — making it a real distributed system

Topology-A proved the idea. Phase 4 is six sub-phases that turn it into something you'd run in production: a real multi-writer warm tier, resilience to flaky networks, a registry so reads don't scan, a crash-safe background compactor, encryption with crypto-shred, and finally constant-memory streaming so segment size never bounds RAM.

4a

DynamoDB warm tier — real cross-process OCC

The warm tier moves to DynamoDB (subpath @cloudbitmaps/roaring/dynamodb, optional peer dep). A single table — PK = ns#…|seg#…, SK = chunk#… — holds the delta rows, and every write is a conditional write. Two processes mutating the same chunk now get genuine optimistic concurrency: one wins, the other gets a WriteConflictError and re-reads. The OCC token of Phase 1 is now backed by a real distributed compare-and-set.

a conditional, conflict-safe write to DynamoDB
import { DynamoDbWarmDriver } from '@cloudbitmaps/roaring/dynamodb';

const warm = new DynamoDbWarmDriver({ client: dynamo, tableName: 'cloudbitmaps' });
const store = new CloudRoaring({ warm, cold });
await store.segment('active-this-week').add(42);  // safe under N concurrent writers

Invariant secured

No lost updates under N writers (driver invariant D4), proven in the concurrency simulator. The same conformance suite that LocalFs passes runs against DynamoDB-Local in CI.

4b

Resilience — retries, backoff & jitter, on by default

Cloud storage throttles, returns 5xx, and drops connections. Every warm/cold call now automatically retries transient faults with bounded exponential backoff and full jitter — you don't wire anything. Crucially, deterministic errors are never retried: a ValidationError, IntegrityError, or WriteConflictError can't be helped by trying again, so they surface immediately.

Safety across a retry is the subtle part: a write that times out after it committed is caught on retry (the OCC token has advanced → a clean conflict, never a double-apply), and cold generations are write-once so there's no half-written object a reader could pick up. A transient outage costs you latency, not correctness.

on by default; tunable or disable-able per store
const store = new CloudRoaring({ warm, cold }); // retries already enabled

const tuned = new CloudRoaring({
  warm, cold,
  retry: { maxAttempts: 4, baseDelayMs: 50, maxDelayMs: 2_000, jitter: 'full' },
  onRetry: ({ attempt, delayMs }) => log.warn({ attempt, delayMs }),
  // …or `retry: false` to defer entirely to your client's own retries.
});

Invariant secured

Transient ≠ deterministic. Only infrastructure faults retry; correctness errors fail fast. No double-apply, no torn read across an outage.

4c

The segment registry — resolve the current generation in one read

Cold is a series of immutable, numbered generations; a reader needs to know which one is current. Without a registry, the cold source lists every generation and takes the max — a storage scan per segment. The registry replaces that with a single authoritative record (currentGen) you read once. It's optional (omit it, fall back to the scan), with memory / LocalFs / DynamoDB implementations. The record also reserves the slots later phases fill: status, dirtyChunkCount, and the wrapped-DEK list.

seed + publish in one call; reads resolve with no list-scan
const registry = new LocalFsRegistryDriver('./.cloudbitmaps/registry');

// seed a generation AND publish it to the registry
await bulkLoadCrbmGeneration(cold, { segment: 'active', generation: 0 },
                             ids, { registry });

const store = new CloudRoaring({
  warm, cold, registry, // no scan
});

Invariant secured

One authoritative pointer, forward-only. publishGeneration never regresses currentGen; the registry round-trip is conformance-tested across all three backends.

4d

Crash-safe compaction — a 2-phase commit you can kill at any step

Every add/remove grows the warm tier; left alone, reads slow down as more deltas merge. Compaction folds the warm deltas into a fresh immutable cold generation and clears the warm rows — in a separate background process that never touches your app path. The hard part is doing it without ever losing a write that lands mid-compaction. The answer is a 2-phase commit, fenced on the per-chunk version it archived, that recovers cleanly from a crash at any step.

a Lambda handler that runs one compaction cycle
import { runCompactionCycle } from '@cloudbitmaps/roaring';

const deps = { cold, warm, registry, clock: { now: () => Date.now() } };

export const handler = async (_e, ctx) => {
  const { results } = await runCompactionCycle(deps,
      { owner: `lambda:${ctx.awsRequestId}`, keep: 1 });
  return results;
};
// or the bundled CLI: CR_COMPACT_MODE=loop npx compact-segments

Invariant secured

Merge-preservation + fenced purge + no torn reads. A write landing mid-compaction is never lost; the swap is the single atomic commit point; a reader pinned to a GC'd generation re-resolves rather than failing. Verified by crash-at-every-step injection.

4e

Encryption at rest & crypto-shred

Encrypt the cold objects so a leaked bucket reveals neither IDs nor cardinality (payloads and the chunk index are encrypted; the footer's count fields are zeroed and re-derived after decryption). It's envelope encryption: you hold one root KEK (32 bytes, bring-your-own — no required cloud dependency); each segment gets a random DEK, wrapped under your KEK and stored in the registry. Each ciphertext's AAD binds it to (namespace, segment, generation, chunkKey) so a blob can't be lifted into another context.

That same design gives crypto-shred — GDPR "right to erasure" that works even on immutable or backed-up storage. Delete the wrapped DEK and the encrypted bytes are permanently unreadable everywhere, no overwrite needed. Encryption is fully opt-in: pass a keystore and it's on; set requireEncryption: true to forbid any cleartext write or read.

bring your own key; read/write encrypted transparently
import { InProcessKeystore } from '@cloudbitmaps/roaring';

const keystore = new InProcessKeystore({
  keys: { '2026-06': loadKekFromSecrets() },  // 32-byte Uint8Array
  activeKeyId: '2026-06',                    // keyId-aware → rotate w/o re-encrypting
  // recoveryKeyId: 'offline-escrow',         // optional offline recovery KEK
});

await bulkLoadCrbmGeneration(cold, { segment: 'pii', generation: 0 },
                             ids, { registry, keystore });

const store = new CloudRoaring({
  warm, cold, registry, keystore,
});

// erase — irreversible; you must name the segment as confirmation:
await destroySegment({ segment: 'pii' }, { registry, warm },
                    { confirmSegment: 'pii' });

⚠ The one rule when you turn this on

Back up the KEK like a database password. Lose every KEK for a segment and its at-rest bytes are gone — by design (that's also what makes crypto-shred real). But CloudBitmaps segments are almost always derived data, so the recovery is usually "re-seed from source," not permanent loss. Rotate, keep old keys, and hold a recovery KEK offline.

Invariant secured

A leaked bucket leaks nothing; erasure is provable. No IDs, no cardinality, no cross-context blob reuse. Crypto-shred works on immutable and backed-up storage — the property no overwrite-based deletion can offer.

4f

Streaming compaction — constant memory at any segment size

The final piece closes the project's founding promise: compaction must not buffer a whole segment in RAM. The merge became an async generator feeding a streaming cold sink; on S3 that's multipart upload with at most one part buffered (default 8 MiB). Peak write memory is ~one part whether the segment is 2 GiB or 50 GiB.

Write-once survived the rewrite: a small object is a single conditional PutObject; a large one finishes with a conditional CompleteMultipartUpload (If-None-Match:*). And the object ceiling is honest — S3 caps multipart at 10,000 parts, so the cap is partBytes × 10,000 (≈80 GiB at the default, growing via partBytes up to S3's 5 TiB), enforced with a typed error rather than a silent overflow.

There's no new API to call — it's the same runCompactionCycle and the same S3ColdDriver from §4d/§3. Large objects simply upload via multipart automatically.

Invariant secured

Bounded memory regardless of segment size — and write-once on both paths. The advertised object ceiling is actually achievable (part-count enforced); an overflow is a typed ValidationError, never opaque truncation.


Status

Where this leaves us

M1 · local

Phases 1–2 ✓

The engine, tombstone data model, durable disk tier, .crbm format, and the deterministic simulator.

M2 · Topology-A

Phase 3 ✓

S3 cold tier and serverless chunk-skipping intersection — the crown jewel.

M3 · Topology-B

Phase 4a–4f ✓

DynamoDB warm, resilience, registry, crash-safe + constant-memory compaction, encryption & crypto-shred.

next

Phase 5 — to scope

Observability, the cost estimator, benchmark-as-test thresholds, and an audit sink.

The through-line: every phase shipped with named tests for its hard invariant, a full local gate (lint · arch-lint · format · typecheck · test · build), and a parallel adversarial review before merge. The result is a distributed bitmap engine whose correctness is reproducible from a seed — not hoped for. ← Back to the overview