Multi-Source Convergence
ConvergeDB is designed from the ground up for multiple independent writers. This page explains the mechanics that make multi-source writing safe and predictable.
Source bitsets
Section titled “Source bitsets”Every entity has a source set: a u64 bitmask where bit N is set if source N has asserted the entity. The source set answers the question: “which sources currently claim this entity exists?”
Source set: 0b00000101 (binary) ││││││││ ││││││││ ││││││└┘─ Source 0: asserted ✓ │││││└─── Source 1: not asserted ││││└──── Source 2: asserted ✓ │││└───── Source 3: not asserted ...- ASSERT sets the calling source’s bit.
- PATCH sets the calling source’s bit (PATCH implies assertion).
- RETRACT clears the calling source’s bit.
- An entity is alive if any bit is set (source set != 0).
- An entity is tombstoned if all bits are cleared (source set == 0).
SourceId assignment
Section titled “SourceId assignment”SourceId is a number from 0 to 63. The mapping between services and SourceIds is your responsibility.
Two instances of the same service should use the same SourceId. If service A has two replicas, both should connect with the same SourceId. This way, writes from either replica are treated as coming from the same logical source.
Two different services should use different SourceIds. If service A and service B both write to the same entity kind, they should use different SourceIds so their assertions are tracked independently.
Because the maximum is 64, SourceIds should be assigned to logical services, not to individual instances.
Multi-source field ownership
Section titled “Multi-source field ownership”The most common multi-source pattern is field-level ownership, where each service owns a distinct set of fields.
// Order service (SourceId = 1) asserts the full entityawait orders.AssertAsync(new OrderView{ EntityId = orderId, OrderId = 42, CustomerId = 100, ItemCount = 3, PaymentStatus = "", ShippingStatus = "", TrackingNumber = "",});
// Payment service (SourceId = 2) patches payment fields onlyawait orders.PatchAsync(new OrderViewPatch{ EntityId = orderId, PaymentStatus = "charged",});
// Shipping service (SourceId = 3) patches shipping fields onlyawait orders.PatchAsync(new OrderViewPatch{ EntityId = orderId, ShippingStatus = "shipped", TrackingNumber = "1Z999...",});After all three services have written, the entity’s source set is 0b00000111 (sources 0, 1, and 2). Subscribers see the fully merged state with all fields populated.
If the payment service retracts, its bit is cleared but the entity remains alive because the order and shipping services still assert it. The payment fields retain their last values.
Deduplication
Section titled “Deduplication”When a source asserts an entity with data identical to what is already stored, the server detects this through byte-level comparison and suppresses the update. No version bump occurs and no subscriber notification is sent.
This makes re-assertion cheap and safe. Common scenarios where deduplication matters:
- Periodic heartbeat assertions. A service re-asserts its entities on a timer to prove liveness. If nothing changed, the re-assertions are silent.
- Source epoch reconciliation. During an epoch, the source re-asserts its entire snapshot. Unchanged entities produce no notifications.
- Idempotent retries. If a write might have been lost in transit, re-sending it is safe because identical writes are deduplicated.
Entity handoff between sources
Section titled “Entity handoff between sources”When an entity needs to migrate from one source to another (for example, a player transferring between game servers), the handoff is seamless:
- Source B asserts the entity with the new state. The entity’s source set now includes both sources.
- Source A retracts the entity. Source A’s bit is cleared, but the entity stays alive because source B’s bit is still set.
The entity is alive throughout the transition. Subscribers see an Updated notification (if the data changed) but never see a Deleted followed by a Created.
Last-write-wins semantics
Section titled “Last-write-wins semantics”Within a single source, writes are ordered by arrival at the server. When two sources write to the same fields, the last write to arrive within the coalescing window wins. Across coalescing windows, the most recently flushed value is the stored state.
There is no conflict detection or merge strategy beyond this. If two sources write conflicting values to the same field, the last one flushed wins and subscribers see the result. If this is not acceptable for your use case, use PATCH with disjoint field sets so each source writes only to its own fields.