A lightweight, structured CRDT implementation with near-zero memory overhead
Traditional CRDT implementation wrap your data field-by-field. You define a User and they give you back LWWRegister<LWWMap<String, LWWRegister<String>>>. Every field access becomes a traversal and every field read requires unwrapping.
lithium-crdt keeps your data clean. You define User in protobuf, you work with User objects directly. The version metadata lives in a parallel tree that's only consulted during sync. For us, this approach lead to ~90% reduction in db latency and 4x reduction in memory overhead compared to traditional CRDT libraries in production.
// Your proto stays clean
message Order {
string customer = 1;
OrderStatus status = 2;
repeated Item items = 3;
}
// Use it directly, no wrappers, no unwrapping
val order = Order(customer = "Alice", status = PENDING)
// Sync just works
val merged = resolver.resolveConflict(
localOrder,
localNode, // Your VersionNode for this order
incomingOrder,
incomingNode // Their VersionNode for this order
)| Feature | Description | Implementation |
|---|---|---|
| O(1) field access | Read protobuf fields directly, no traversal or unwrapping | Direct field access on generated protobuf classes |
| O(m) space overhead | Only modified fields are version tracked | VersionNode parallel tree |
| Field-level merging | Different fields edited on different devices merge correctly | Conflict resolution algorithms |
| Type safety | Compile-time checking through protobuf schemas | Generated protobuf types |
| Multi-platform | Android (Wire) and JVM (Protoc) implementations | wire/ and protoc/ |
| Delta sync | Efficient incremental synchronization | VersionChange tracking |
Gradle (Kotlin/Android)
dependencies {
implementation("co.atoms.lithium.crdt:crdt-wire:1.0.0")
}Gradle (Java/JVM)
dependencies {
implementation("co.atoms.lithium.crdt:crdt-protoc:1.0.0")
}Maven
<dependency>
<groupId>co.atoms.lithium.crdt</groupId>
<artifactId>crdt-protoc</artifactId>
<version>1.0.0</version>
</dependency>Define your protobuf message (you probably already have this):
syntax = "proto3";
message Order {
string customer = 1;
OrderStatus status = 2;
int64 total = 3;
}Then use it in your code:
// Create a resolver for your message type
val resolverProvider = WireCrdtResolverProvider() // or CrdtMessageResolverProvider() for Protoc
val orderResolver = resolverProvider.getResolver(Order::class)
// Load your order and its version metadata from the database
val (order, versionNode) = db.loadOrder(orderId)
// Apply local changes
val (updatedOrder, updatedVersionNode, _) = orderResolver.applyLocalWrite(
currentValue = order,
currentNode = versionNode, // The parallel version tree for this order
actors = actors,
newValue = order.copy(status = COMPLETED),
timestamp = clock.now()
)
db.save(updatedOrder, updatedVersionNode)
// When receiving changes from another device, resolve conflicts
val (mergedOrder, mergedNode, strategy) = orderResolver.resolveConflict(
localValue = localOrder,
localNode = localVersionNode, // Your version metadata
incomingValue = incomingOrder,
incomingNode = incomingVersionNode // Their version metadata
)
when (strategy) {
NO_CHANGE -> { /* both sides identical */ }
LOCAL -> { /* local was newer */ }
INCOMING -> { /* incoming was newer */ }
MERGED_VALUES -> { /* different fields merged from both sides */ }
}Run our interactive demo app. It simulates three devices editing the same document, syncing independently, and merging only the fields that changed.
git clone https://github.com/atoms-co/lithium-crdt.git
cd lithium-crdt
./gradlew :examples:interactive-demo:runWhat you'll see right away:
- Different fields edited on different nodes both survive sync
- The same field resolves deterministically by version
- The parallel
VersionNodetree that makes field-level merging possible - Counter fields that add across actors instead of last-write-wins
See the full demo guide in examples/interactive-demo/README.md.
Two parallel structures:
Your Protobuf Message Version Metadata
┌─────────────────────┐ ┌─────────────────────┐
│ Order │ │ VersionNode │
│ customer: "Alice" │ ◄────────► │ field[1]: v1.2 │
│ status: PENDING │ │ field[2]: v1.0 │
│ total: 100 │ │ field[3]: v1.1 │
└─────────────────────┘ └─────────────────────┘
When two devices edit different fields:
Device A: order.customer = "Bob" @ timestamp 2
Device B: order.status = COMPLETED @ timestamp 3
After sync on both devices:
customer = "Bob" (Device A won on field 1, timestamp 2)
status = COMPLETED (Device B won on field 2, timestamp 3)
total = 100 (unchanged)
Both changes survive. No last-write-wins on the entire message. Just the fields that actually changed.
- Conflict Resolution Algorithms - How field-level LWW, maps, and lists are resolved
- Version Architecture - The version tree structure explained
- Wire Implementation - Android/Kotlin details
- Protoc Implementation - Java/JVM backend details
Contributions welcome! See CONTRIBUTING.md for details.
Found a bug? Open an issue →
Apache License 2.0 - See LICENSE
Built with Protocol Buffers and Wire by Square
Questions? Open an issue or discussion. We'd love to hear how you're using lithium-crdt!