file-json-db

Architecture

This page explains how file-json-db works under the hood. Understanding the architecture helps you make better decisions about part sizes, indexes, concurrency, and recovery.

High-level overview

┌─────────────────────────────────────────────────────────┐
│                     Your Node.js App                    │
│                                                         │
│   const db = new JSONDB('./data');                      │
│   const users = db.collection({ name: 'users', ... });  │
└──────────────────────────┬──────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────┐
│                      JSONDB instance                    │
│  - owns the root directory                              │
│  - manages Collection instances                         │
│  - exposes backup / listCollections / dropCollection    │
└──────────────────────────┬──────────────────────────────┘
                           │
           ┌───────────────┼───────────────┐
           ▼               ▼               ▼
     Collection A    Collection B    Collection C
     (users/)        (orders/)       (products/)
           │
           ├── index.json          (metadata + indexes)
           ├── part-0001.json      (documents)
           ├── part-0002.json
           └── crash/              (isolated bad parts)

1. Database = Directory

When you write new JSONDB('./data'), the library ensures that the directory ./data exists. That directory is the entire database. Everything the library knows about lives inside it.

There is no central server process, no network protocol, and no separate binary. The database is simply files on disk that your process reads and writes.

2. Collection = Subdirectory

Each collection is a subdirectory of the database root. A collection named users lives at ./data/users/.

Inside that directory you will find:

3. Part files – why they exist

A simple JSON-file database usually puts every document of a collection into a single array inside one file. That works until the file becomes large:

file-json-db therefore splits a collection across multiple part files. New documents are appended to the current part. When the part reaches either the configured maxPartSize (default 256 KB) or maxRecords, a new part is created.

users/
├── index.json
├── part-0001.json   ← first ~256 KB or N documents
├── part-0002.json
└── part-0003.json

4. The index (index.json)

The in-memory (and on-disk) index is the brain of a collection. It contains:

FieldPurpose
nextIdNext auto-increment value when idType: 'auto'
partsMap of part key → { count, size }
idIndexPrimary index: document ID → part key
indexesSecondary indexes: field → value → [{ id, part }, …]
corruptedPartsParts that previously failed to parse (skipped on load)

Because the primary and secondary indexes live in memory after the collection is loaded, equality lookups on an indexed field are O(1) (or O(k) where k is the number of documents that share the same value).

5. Atomic writes

Every time the library needs to persist a part or the index, it follows the classic safe-write pattern:

  1. Write the new content to a temporary file in the same directory (.part-0001.json.<pid>.<timestamp>.tmp)
  2. Call fs.rename (or the async equivalent) to move the temp file over the real file
  3. If the rename fails (rare race on some filesystems), retry a few times

On POSIX systems a rename within the same filesystem is atomic. If the process crashes between writing the temp file and the rename, the original file remains intact. The next start simply ignores the orphaned temp file.

6. Concurrency model

Each collection maintains an internal lock and a queue. Asynchronous methods (insertAsync, updateOneAsync, …) acquire the lock, perform the work, then release it so the next queued operation can run.

Single-process design The lock is in-process only. If two separate Node.js processes open the same collection and write concurrently, you can still get data races. For multi-process scenarios you need external locking or a different database.

7. Snapshot transactions

When you call collection.startTransaction() the library:

Disk is not touched until you call commit(). rollback() simply discards the overlay; the original files stay exactly as they were.

Only one transaction may be active on a collection at a time.

8. Self-healing / corrupted parts

If a part file cannot be parsed (invalid JSON, truncated write, etc.):

You can later inspect the crash copies, fix them manually, delete the corruptedParts entry, and call rebuildIndexes().

9. Memory model

At any moment the following live in memory for an open collection:

Parts that are not needed for the current operation are not kept in memory. This keeps the memory footprint proportional to the size of the indexes plus the parts you actually touch, not to the entire collection.

Summary

ConceptImplementation
DatabaseDirectory on disk
CollectionSubdirectory + index.json + part-*.json
Document storageChunked JSON arrays in part files
Primary key lookupidIndex → part → linear scan of that part
Secondary indexesIn-memory maps persisted in index.json
Write safetyAtomic temp-file + rename with retries
ConcurrencyPer-collection async lock/queue
TransactionsIn-memory snapshot overlay, disk on commit
Corruption handlingIsolate bad parts, keep the rest usable