Introducing Delva: an embedded JSON database for Kotlin that indexes everything
Every application ends up holding JSON it didn't design: API responses worth keeping, exports from another system, event logs, configuration that grew a life of its own. Storing it is easy. The trouble starts when you want to ask it something. Then you're choosing between running a database server you didn't want, or forcing the data into a schema it never had.
Delva is a third option. It's an embedded JSON document database for the JVM, written in Kotlin: a library you add as a dependency, not a server you deploy. Point it at a directory, put JSON documents in, and search them by any key, at any depth, in documents of any size. No schema, no mapping, no process to babysit. Apache-2.0, on Maven Central today.
Delva is Bulgarian. A делва is the earthenware jar a household sinks into the cellar floor and seals for the winter. You put things in to keep them, and they're there, intact, when you come back for them. That's the whole brief.
Who it's for
Delva is for the program that owns its data and runs on one machine:
- Desktop and CLI tools that collect JSON from APIs and need to look things up later without making the user install PostgreSQL.
- Ingestion and migration jobs handed a multi-gigabyte JSON export and asked to find the records that matter in it.
- Services that keep a local document cache or audit trail next to the code, where a network hop to a separate database is cost without benefit.
- Kotlin developers who want an API that feels like Kotlin: suspending functions by default, a blocking view when you don't want coroutines,
use {}for everything that holds a resource.
If you need many machines writing to one dataset, you want a database server. If you need one process to keep and query JSON well, read on.
Five minutes with Delva
Add the dependency (JDK 22 or newer; it depends only on the Kotlin standard library and kotlinx-coroutines):
dependencies {
implementation("app.oreshkov.delva:delva:0.2.0")
}
Open a store, insert a document, search it:
JsonStore.openOrCreate(Path.of("data/store")).use { store ->
val db = store.blocking()
db.insert("""{"name":"ada","tags":["math","engines"],"score":42}""")
db.search(KeyValueQuery("name", JsonQueryValue.Str("ada"))).use { result ->
for (hit in result) println(hit.readJsonAsString())
}
}
That's the whole setup. There's no mapping step and no index to declare first: every key–value pair in every object was indexed as the document went in.
When a key-and-value lookup isn't enough, there's JSONPath: all of RFC 9535, filters and functions included.
val lowStock = JsonPathQuery(JsonPath.compile("$..[?@.qty < 1].sku"))
println(db.explain(lowStock)) // QueryPlan('qty' < 1)
db.search(lowStock).use { result ->
for (hit in result) println("${hit.path} = ${hit.readJsonAsString()}")
}
Note the explain. Delva uses the query's filters to narrow the search through the index, and it tells you how. A query that can't be narrowed, like a bare $..sku, is rejected rather than quietly scanning everything, unless you opt into a scan explicitly. Slow queries don't slip into production unannounced.
What makes it different
Every key is searchable, from day one. Most embedded stores make you choose your indexes up front, which means guessing your questions before you've seen your data. Delva indexes every key–value pair in every object, so the question you think of next month already has an index. When that's more index than you want, controls fixed at creation time narrow what gets indexed.
Document size doesn't matter. Documents are streamed in and out, so memory is bounded by configuration and never by document size. That's not a slogan: Delva's own test suite inserts, searches and reads back a 2 GiB document in a 256 MB heap. Hand insert a Path or an InputStream instead of a string and it indexes as it reads.
Results come back as the part you asked for. A search can return whole documents or just the objects that matched, oldest or newest first, paged with cursors. Every hit knows its own location as a normalized path such as $['items'][1], so you always know where a result came from.
History you can trust. Documents are immutable. replace swaps in a new version atomically, an optional document key adds get, upsert and listing in key order, and compare streams a diff between any two versions with bounded memory.
Readers don't wait for the writer. One process writes; other processes can open the same store as a secondary that takes no lock and never writes, and catches up with refresh() when it's ready. A dashboard, a report job or a debugging session can read a live store without disturbing it.
It doesn't lose your data. An insert is either completely visible after a crash or completely absent, never half there. You choose among three durability modes, and verify scrubs the entire store for damage. The test suite kills writers mid-flush on purpose to keep that promise honest.
Storage stays compact. Stored fragments are compressed with deflate, using dictionaries trained on your own data, so stored bytes sit well below the raw JSON. Optional Unicode NFC normalization makes the two spellings of "é" one string, if your data mixes them.
A command line for the rest of us
Not every question comes from code. Each release ships a delva command line, so you can work with a store from a shell:
delva init ./data/store
export DELVA_STORE=./data/store
delva insert orders/*.json # one document per file
delva search kv status open --string # documents with "status": "open"
delva search path '$..[?@.qty < 1].sku' # a JSONPath search
delva compare 41 42 --diff # what changed between two versions
delva verify # read everything, report any damage
Read-only commands open a secondary, so they're safe to run beside a live application. --json turns output into JSON Lines for scripts, exit codes follow sysexits.h, and every download comes with SHA-256 sums and verifiable build provenance.
If your data already lives in Kotlin classes, delva-kotlinx-serialization stores and reads @Serializable values directly, with no hand-written JSON.
Where it stands today
I'd rather you trust it than be surprised by it, so here's the plain status. Delva is at 0.2.0, in early development. Until 1.0, a minor version can change the API and the on-disk format. A store records its format version, so a mismatch is reported as an error rather than misread. The API is Kotlin-first: its write path isn't usable from Java yet, and a Java facade is on the roadmap. And because the index covers everything, it can be large: anywhere from about a third of your JSON's size to more than its size, depending on how repetitive the data is.
None of that is hidden in the fine print. The guide and the roadmap list the limits next to the features, so you know what you're adopting before you adopt it.
Try it
Add app.oreshkov.delva:delva:0.2.0 to a Kotlin project on JDK 22+, or grab the delva CLI from the latest release. The repo's delva-examples module has small runnable programs (typed search, a keyed record store, versioning, a follower that reads beside the writer), each built only on the public API.
Source (Apache-2.0): https://github.com/aoreshkov/delva. The KDoc documents every public declaration. Bug reports and questions about the semantics are very welcome, and if you have a JSON file that Delva handles badly, I want to see it.
Enjoyed this? I also wrote about letting your AI assistant read Oracle Forms.