A lightweight, embeddable Mongo-style document store for Kotlin/JVM apps. Optimized for local use, fast CRUD, simple secondary indexes, and Mongo-like IDs (ObjectId).
From Sozocode - We built Minileaf to power VisuaLeaf, our MongoDB GUI for developers. Now we're sharing it with the community. If you're working with MongoDB, check out VisuaLeaf for a modern, intuitive database management experience.
- Embedded: Runs in-process (no server) for desktop/CLI/services
- Mongo-like API: Familiar document-oriented interface with schemaless JSON objects
- Flexible ID Types: ObjectId, UUID, String, or Long as document identifiers
- Flexible Queries: Dot-path access, array indexes, and comprehensive filter operators
- Indexes: Single-field and compound indexes with enum and range optimization
- Persistence: File-backed storage with WAL and snapshots for crash-safe recovery
- Encryption: Optional AES-256-GCM encryption at rest
- Type-safe: Repository pattern with Kotlin generics and data classes
Gradle (Kotlin DSL)
dependencies {
implementation("com.sozocode:mini-leaf-core:1.6.13")
implementation("com.sozocode:mini-leaf-jackson:1.6.13")
implementation("com.sozocode:mini-leaf-kotlin:1.6.13")
}Gradle (Groovy)
dependencies {
implementation 'com.sozocode:mini-leaf-core:1.6.13'
implementation 'com.sozocode:mini-leaf-jackson:1.6.13'
implementation 'com.sozocode:mini-leaf-kotlin:1.6.13'
}Maven
<dependencies>
<dependency>
<groupId>com.sozocode</groupId>
<artifactId>mini-leaf-core</artifactId>
<version>1.6.13</version>
</dependency>
<dependency>
<groupId>com.sozocode</groupId>
<artifactId>mini-leaf-jackson</artifactId>
<version>1.6.13</version>
</dependency>
<dependency>
<groupId>com.sozocode</groupId>
<artifactId>mini-leaf-kotlin</artifactId>
<version>1.6.13</version>
</dependency>
</dependencies>import com.minileaf.core.Minileaf
import com.minileaf.core.config.MinileafConfig
import com.minileaf.kotlin.repository
import org.bson.types.ObjectId
// Define your entity
data class User(
var _id: ObjectId? = null,
val email: String,
val status: Status,
val age: Int
)
enum class Status { ACTIVE, INACTIVE }
// Initialize Minileaf
val db = Minileaf.open(MinileafConfig())
// Get a typed repository
val users = db.repository<User, ObjectId>("users")
// Insert
val user = users.save(User(
email = "alice@example.com",
status = Status.ACTIVE,
age = 30
))
// Find by ID
val found = users.findById(user._id!!)
// Query with filters
val activeUsers = users.findAll(
filter = mapOf("status" to "ACTIVE"),
skip = 0,
limit = 10
)
// Close
db.close()import com.minileaf.kotlin.repositoryWithUUID
import com.minileaf.kotlin.repositoryWithString
import com.minileaf.kotlin.repositoryWithLong
import java.util.UUID
// UUID as ID
data class Session(var _id: UUID? = null, val token: String)
val sessions = db.repositoryWithUUID<Session>("sessions")
// String as ID
data class Config(var _id: String? = null, val value: String)
val configs = db.repositoryWithString<Config>("configs")
// Long as ID
data class Counter(var _id: Long? = null, val count: Int)
val counters = db.repositoryWithLong<Counter>("counters")val config = MinileafConfig(
dataDir = Paths.get("./minileaf-data"), // Data directory
encryptionKey = null, // 32 bytes for AES-256-GCM
autosaveIntervalMs = 5_000, // Flush buffers every 5s
snapshotIntervalMs = 60_000, // Create snapshot every 60s
walMaxBytesBeforeSnapshot = 64 * 1024 * 1024, // 64 MB
memoryOnly = false, // Set true for in-memory only
cacheSize = null, // LRU cache size (null = all in memory)
backgroundIndexBuild = true, // Build indexes in background
syncOnWrite = true, // fsync after each write
maxDocumentSize = 16 * 1024 * 1024 // 16 MB max document size
)
val db = Minileaf.open(config)import com.minileaf.core.index.IndexKey
import com.minileaf.core.index.IndexOptions
// Single-field unique index
db.collection("users").admin().createIndex(
IndexKey(linkedMapOf("email" to 1)),
IndexOptions(unique = true)
)
// Enum-optimized index
db.collection("users").admin().createIndex(
IndexKey(linkedMapOf("status" to 1)),
IndexOptions(enumOptimized = true)
)
// Compound index
db.collection("users").admin().createIndex(
IndexKey(linkedMapOf("status" to 1, "age" to -1))
)
// Drop index
db.collection("users").admin().dropIndex("email_1")
// List indexes
val indexes = db.collection("users").admin().listIndexes()// Greater than
users.findAll(filter = mapOf("age" to mapOf("\$gt" to 25)))
// Range
users.findAll(filter = mapOf("age" to mapOf("\$gte" to 25, "\$lte" to 35)))
// Not equal
users.findAll(filter = mapOf("status" to mapOf("\$ne" to "INACTIVE")))// AND
users.findAll(
filter = mapOf(
"\$and" to listOf(
mapOf("status" to "ACTIVE"),
mapOf("age" to mapOf("\$gte" to 25))
)
)
)
// OR
users.findAll(
filter = mapOf(
"\$or" to listOf(
mapOf("status" to "ACTIVE"),
mapOf("age" to mapOf("\$lt" to 18))
)
)
)// In array
users.findAll(filter = mapOf("country" to mapOf("\$in" to listOf("US", "CA"))))
// Not in array
users.findAll(filter = mapOf("country" to mapOf("\$nin" to listOf("XX", "YY"))))
// Field exists
users.findAll(filter = mapOf("middleName" to mapOf("\$exists" to true)))// Regex (case-insensitive)
users.findAll(
filter = mapOf(
"email" to mapOf(
"\$regex" to ".*@example\\.com$",
"\$options" to "i"
)
)
)Query documents by date/time using Instant or LocalDateTime:
import java.time.Instant
import java.time.temporal.ChronoUnit
data class Metrics(
var _id: ObjectId? = null,
val name: String,
val timestamp: Instant = Instant.now()
)
val metrics = db.repository<Metrics, ObjectId>("metrics")
// Find documents from the last hour
val oneHourAgo = Instant.now().minus(1, ChronoUnit.HOURS)
val recentDocs = metrics.findAll(
filter = mapOf("timestamp" to mapOf("\$gte" to oneHourAgo)),
skip = 0,
limit = 100
)
// Time range query
val startTime = Instant.parse("2024-01-01T00:00:00Z")
val endTime = Instant.parse("2024-01-02T00:00:00Z")
val results = metrics.findAll(
filter = mapOf(
"timestamp" to mapOf(
"\$gte" to startTime,
"\$lte" to endTime
)
),
skip = 0,
limit = 1000
)
// Combine with other filters
val filtered = metrics.findAll(
filter = mapOf(
"name" to "cpu",
"timestamp" to mapOf("\$gte" to oneHourAgo)
),
skip = 0,
limit = 100
)Supported temporal types: java.time.Instant, java.time.LocalDateTime
// Nested field
users.findAll(filter = mapOf("person.address.city" to "San Francisco"))
// Array element
users.findAll(filter = mapOf("phones.0.type" to "mobile"))
// Element match
users.findAll(
filter = mapOf(
"phones" to mapOf(
"\$elemMatch" to mapOf(
"type" to "mobile",
"verified" to true
)
)
)
)// Save (upsert)
val user = users.save(User(...))
val batch = users.saveAll(listOf(user1, user2, user3))
// Find
val found = users.findById(id)
val all = users.findAll()
val page = users.findAll(skip = 0, limit = 10)
val filtered = users.findAll(filter = mapOf(...), skip = 0, limit = 10)
// Index-optimized queries
val activeUsers = users.findByEnumField("status", Status.ACTIVE)
val ageRange = users.findByRange("age", 18, 35)
// Delete
val deleted = users.deleteById(id)
// Utilities
val exists = users.exists(id)
val count = users.count()Automatically expire documents after a specified duration:
db.collection("sessions").admin().createIndex(
IndexKey(linkedMapOf("createdAt" to 1)),
IndexOptions(expireAfterSeconds = 1800) // 30 minutes
)Index only documents matching a filter:
db.collection("users").admin().createIndex(
IndexKey(linkedMapOf("email" to 1)),
IndexOptions(
unique = true,
partialFilterExpression = mapOf("status" to "ACTIVE")
)
)Select or exclude specific fields from results:
// Inclusion
users.findAll(
filter = mapOf("status" to "ACTIVE"),
projection = mapOf("name" to 1, "email" to 1),
skip = 0,
limit = 10
)
// Exclusion
users.findAll(
filter = emptyMap(),
projection = mapOf("password" to 0, "ssn" to 0),
skip = 0,
limit = 10
)For large datasets that don't fit in memory:
val config = MinileafConfig(
cacheSize = 50000 // Keep 50k documents in memory
)See docs/ADVANCED_FEATURES.md for detailed documentation.
// Collection stats
val stats = db.collection("users").stats()
println("Documents: ${stats.documentCount}")
println("Storage: ${stats.storageBytes} bytes")
println("Indexes: ${stats.indexCount}")
// Force compaction (snapshot + WAL reset)
db.collection("users").compact()import com.minileaf.core.crypto.Encryption
// Generate a key
val key = Encryption.generateKey() // 32 bytes
// Save key securely (e.g., environment variable, key vault)
val keyHex = Encryption.bytesToHex(key)
// Load key and configure Minileaf
val loadedKey = Encryption.hexToBytes(keyHex)
val config = MinileafConfig(encryptionKey = loadedKey)
val db = Minileaf.open(config)- mini-leaf-core: Storage engine, indexes, query engine, ObjectId utilities
- mini-leaf-jackson: Jackson-based codec for entity serialization
- mini-leaf-kotlin: Kotlin extensions and helpers
- In-memory mode: Fastest, no durability
- File-backed mode:
- Write-Ahead Log (WAL) for durability
- Periodic snapshots for compaction
- Crash-safe recovery (snapshot + WAL replay)
- Optional AES-256-GCM encryption
- Cached mode: LRU cache for large datasets that don't fit in memory
- Single-writer / multi-reader per collection
- Document-level atomicity
- MVCC snapshots for readers
- Primary-key operations: O(log N) via B-tree
- Indexed queries: Index scan + selective fetch
- Create indexes before bulk imports
- Use
backgroundIndexBuild=truefor large datasets - Use
cacheSizefor datasets larger than available RAM - Compact periodically for optimal performance
See the examples module for complete working examples:
# Run basic example
./gradlew :examples:run
# Run advanced features example (TTL, Partial Indexes, Projections)
./gradlew :examples:runAdvanced
# Run UUID example
./gradlew :examples:runUUIDIn-memory mode is perfect for unit tests:
val db = Minileaf.open(MinileafConfig(memoryOnly = true))
// ... tests ...
db.close()- Max document size: 16 MB (configurable)
- Local DB only, for local application
Copyright © 2025 Sozocode. Licensed under the Apache License 2.0.