Skip to content

Merge Contracts & Resource Types

This page explains the foundational concepts that govern how your data syncs across devices: resource types and merge contracts.

Locorda uses three annotations to classify your data classes, each serving a distinct purpose in the sync architecture.

A RootResource is an independent entity that syncs across storage backends using state-based CRDTs. Each instance has:

  • Unique IRI (Internationalized Resource Identifier) - Generated from fields marked with @RdfIriPart()
  • Independent lifecycle - Can be created, modified, deleted independently
  • Merge contract - Defines CRDT merge strategies for conflict resolution

Real example from the personal notes app:

@RootResource(
appVocab,
mergeContract: MergeContract(
label: 'Personal Note CRDT Document Mapping v1',
comment: 'Defines how personal notes should merge when conflicts occur during sync.',
),
iriStrategy: RootIriStrategy(RootIriConfig('note')),
fullIndex: FullIndex.disabled(),
subClassOf: SchemaNoteDigitalDocument.classIri,
)
class Note {
@RdfIriPart()
final String id;
@RdfProperty(SchemaNoteDigitalDocument.name)
@CrdtLwwRegister()
final String title;
@RdfProperty(SchemaNoteDigitalDocument.text)
@CrdtLwwRegister()
final String content;
@RdfProperty(SchemaNoteDigitalDocument.keywords)
@CrdtOrSet()
final Set<String> tags;
@RdfProperty(SchemaNoteDigitalDocument.dateCreated)
@CrdtImmutable()
final DateTime createdAt;
// ...
}

Examples of RootResources:

  • A note in a notes app
  • A category for organizing notes
  • TODO: Add more examples

@SubResource - Nested Resources with Fragment IRIs

Section titled “@SubResource - Nested Resources with Fragment IRIs”

A SubResource is a nested resource within a RootResource that has its own fragment IRI but is not registered globally in the type index. It:

  • Fragment IRI - Identified using fragment IRIs derived from parent resource
  • Inherits merge contract - Uses merge strategies from parent’s CRDT mapping
  • Not globally indexed - Not registered in type index

Real example from the personal notes app:

@SubResource(
appVocab,
SubIriStrategy('#comment-{id}'),
subClassOf: SchemaComment.classIri,
)
class Comment {
@RdfIriPart()
final String id; // Used in fragment template
@RdfProperty(SchemaComment.text)
@CrdtLwwRegister()
final String text;
@RdfProperty(SchemaComment.dateCreated)
@CrdtImmutable()
final DateTime createdAt;
// ...
}

Examples of SubResources:

  • Comments on a note (IRI-identified sub-resources)
  • TODO: Add more examples

When to use SubResource:

  • TODO: Document decision criteria

@LocalResource - Blank Nodes (Local RDF Resources)

Section titled “@LocalResource - Blank Nodes (Local RDF Resources)”

A LocalResource represents a blank node in RDF - a resource without a globally unique IRI that exists only within the context of a parent resource.

CRDT Merge Identification:

Blank nodes can be identified for CRDT merging in two ways:

  1. Single-path blank nodes - Reachable via exactly one property path (e.g., Category → displaySettings). No identification annotation needed.

  2. Property-identified blank nodes - Multiple instances identified by a unique property marked with @MergeIdentifying().

Real examples from the personal notes app:

Single-path blank node:

@LocalResource(appVocab)
class CategoryDisplaySettings {
@RdfProperty.define(fragment: 'color')
@CrdtLwwRegister()
final String? color;
@RdfProperty.define(fragment: 'icon')
@CrdtLwwRegister()
final String? icon;
}

Property-identified blank node:

@LocalResource(appVocab, subClassOf: SchemaThing.classIri)
class Weblink {
@RdfProperty(SchemaThing.url)
@MergeIdentifying() // Identifies this blank node for CRDT merging
@CrdtImmutable()
final String url;
@RdfProperty(SchemaThing.name)
@CrdtLwwRegister()
final String? title;
}

Examples of LocalResources:

  • Display settings for a category (single-path)
  • Weblinks referenced by a note (property-identified)
  • TODO: Add more examples

A merge contract is a published RDF document that defines CRDT merge strategies for properties of a RootResource.

Locorda provides three CRDT merge strategies via property annotations:

@CrdtLwwRegister() - Last-Write-Wins Register

Section titled “@CrdtLwwRegister() - Last-Write-Wins Register”

TODO: Document:

  • How LWW resolution works
  • Hybrid Logical Clock usage
  • Causality tracking
  • Tie-breaking rules

TODO: Document:

  • How OR-Set merges additions and removals
  • Use cases (tags, collections)
  • Behavior with re-additions

Used for properties that should never change once set:

  • Creation timestamps
  • Identifying properties
  • Write-once data
@RootResource(appVocab, mergeContract: MergeContract())
class Note {
@RdfIriPart()
final String id;
@RdfProperty(Schema.name)
@CrdtLwwRegister() // Last writer wins
final String title;
@RdfProperty(Schema.keywords)
@CrdtOrSet() // Additions/removals merge independently
final Set<String> tags;
@RdfProperty(Schema.dateCreated)
@CrdtImmutable() // Never changes
final DateTime createdAt;
}

TODO: Add concrete merge scenario examples

Automatic generation (recommended):

MergeContract(
label: 'Note CRDT Mapping v1',
comment: 'Defines merge strategies for notes',
)

The builder scans CRDT annotations on properties and generates merge contract RDF documents.

External contracts (for shared/standard vocabularies):

MergeContract.external('https://vocab.example.org/mappings/note-v1#')

References a manually authored CRDT mapping document.

TODO: Document:

  • How merge contracts are published
  • How clients discover and validate compatibility
  • Versioning strategies

How Resource Types and Merge Contracts Interact

Section titled “How Resource Types and Merge Contracts Interact”

Real example from personal notes app:

@RootResource(
appVocab,
mergeContract: MergeContract(
label: 'Personal Note CRDT Document Mapping v1',
),
)
class Note {
@RdfIriPart()
final String id;
@RdfProperty(Schema.name)
@CrdtLwwRegister()
final String title;
@RdfProperty(Schema.relatedLink)
@CrdtOrSet()
final Set<Weblink> weblinks; // LocalResources (blank nodes)
@RdfProperty(Schema.comment)
@CrdtOrSet()
final Set<Comment> comments; // SubResources with fragment IRIs
}
@SubResource(appVocab, SubIriStrategy('#comment-{id}'))
class Comment {
@RdfIriPart()
final String id;
@RdfProperty(Schema.text)
@CrdtLwwRegister() // Inherits from Note's merge contract
final String text;
}
@LocalResource(appVocab) // Blank node
class Weblink {
@RdfProperty(Schema.url)
@MergeIdentifying()
@CrdtImmutable()
final String url; // Identifies this blank node
@RdfProperty(Schema.name)
@CrdtLwwRegister() // Inherits from Note's merge contract
final String? title;
}

Inheritance rules:

  1. RootResource - Defines the merge contract
  2. SubResource - Inherits parent’s merge contract
  3. LocalResource - Inherits parent’s merge contract

TODO: Add composition examples once the actual patterns are documented

TODO: Document decision criteria based on actual patterns from the implementation:

TODO: Define criteria

TODO: Document when to use SubResource vs LocalResource

  • SubResource: Has fragment IRI, not in type index
  • LocalResource: Blank node, no IRI

TODO: Document blank node patterns:

  • Single-path identified (CategoryDisplaySettings)
  • Property-identified (Weblink)

TODO: Add real-world patterns from personal_notes_app and other examples

TODO: Add proper links once corresponding pages are created

  • 📘 Conflict Resolution - TODO
  • 📘 Indexing Strategies - TODO
  • 📘 Getting Started Guide - TODO
  1. RootResource = Independent entity with IRI, defines merge contract
  2. SubResource = Nested resource with fragment IRI, inherits merge contract
  3. LocalResource = Blank node (no globally unique IRI), inherits merge contract
  4. CRDT annotations define property-level merge strategies:
    • @CrdtLwwRegister() - Last-Write-Wins
    • @CrdtOrSet() - Observed-Remove Set
    • @CrdtImmutable() - Immutable
  5. Merge contracts are generated from CRDT annotations or referenced externally
  6. @MergeIdentifying() marks identifying properties for property-identified blank nodes