Skip to content

Multi-Service Field Ownership

A common pattern in microservice architectures is having multiple services contribute data to the same logical entity. In ConvergeDB, this is achieved through PATCH with distinct source IDs.

Each service:

  1. Connects with a unique SourceId.
  2. Registers the same entity kind.
  3. Uses PatchAsync to write only the fields it owns.

The server maintains the fully merged state. Subscribers see all fields from all services in a single notification.

Three services collaborate on an OrderView entity:

[ConvergenceEntity("OrderView")]
public partial struct OrderView
{
// Order service (SourceId = 1) owns these fields
[Field(0)] public ulong OrderId { get; set; }
[Field(1)] public ulong CustomerId { get; set; }
[Field(2)] public uint ItemCount { get; set; }
[Field(3)] public ulong CreatedAtUnixMs { get; set; }
// Payment service (SourceId = 2) owns these fields
[Field(4, MaxLength = 16)] public string PaymentStatus { get; set; }
[Field(5)] public ulong AmountCents { get; set; }
[Field(6)] public ulong PaidAtUnixMs { get; set; }
// Shipping service (SourceId = 3) owns these fields
[Field(7, MaxLength = 16)] public string ShippingStatus { get; set; }
[Field(8, MaxLength = 64)] public string TrackingNumber { get; set; }
[Field(9)] public ulong ShippedAtUnixMs { get; set; }
public ReadOnlyMemory<byte> EntityId { get; init; }
}
// Order service (SourceId = 1)
await orders.AssertAsync(new OrderView
{
EntityId = EntityId.FromLong(orderId),
OrderId = orderId,
CustomerId = customerId,
ItemCount = 3,
CreatedAtUnixMs = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
});

After this step, the entity’s source set is 0b001 (source 0 only). All payment and shipping fields are zero-default.

// Payment service (SourceId = 2)
await orders.PatchAsync(new OrderViewPatch
{
EntityId = EntityId.FromLong(orderId),
PaymentStatus = "charged",
AmountCents = 4999,
PaidAtUnixMs = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
});

After this step, the entity’s source set is 0b011 (sources 0 and 1). The subscriber notification includes a ChangedFields bitmask with bits 4, 5, and 6 set.

// Shipping service (SourceId = 3)
await orders.PatchAsync(new OrderViewPatch
{
EntityId = EntityId.FromLong(orderId),
ShippingStatus = "shipped",
TrackingNumber = "1Z999AA10123456784",
ShippedAtUnixMs = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
});

After this step, the entity’s source set is 0b111 (all three sources). Subscribers see the fully merged order with all fields populated.

A dashboard service subscribes and sees the complete merged state:

await foreach (var change in orders.SubscribeAsync(
bootstrap: true,
includePrevious: true,
ct: ct))
{
var order = change.Entity!.Value;
Console.WriteLine($"Order {order.OrderId}: " +
$"items={order.ItemCount}, " +
$"payment={order.PaymentStatus}, " +
$"shipping={order.ShippingStatus}");
// Check which service triggered this update
if (change.HasChanged(OrderView.Fields.PaymentStatus))
Console.WriteLine(" Payment status changed");
if (change.HasChanged(OrderView.Fields.ShippingStatus))
Console.WriteLine(" Shipping status changed");
}

If the payment service disconnects and the liveness deadline expires (default: 30 seconds):

  • Source 2’s bit is cleared from the entity’s source set.
  • The entity remains alive because sources 0 and 2 still assert it.
  • The payment fields retain their last values. They are not zeroed out.
  • No subscriber notification is sent (the entity’s field data did not change).

If all three services retract (or all disconnect beyond the liveness deadline), the entity is tombstoned and subscribers receive a Deleted notification.

  • Assign one SourceId per service, not per instance. Two replicas of the payment service should use the same SourceId.
  • Each service patches only its fields. Never have two services write to the same field, as last-write-wins would produce unpredictable results.
  • Use ASSERT for the “primary owner” that creates the entity, and PATCH for services that enrich it. This is a convention, not a rule: both ASSERT and PATCH set the source bit.
  • Use Source Epochs when a service reconnects to its upstream data source and needs to reconcile its entity set.