Projection with composite keys
When a single property isn’t sufficient to uniquely identify a projection instance, you can use composite keys made up of multiple values. This is useful for multi-tenant scenarios, hierarchical data, or when you need complex keys.
Defining a composite key
Section titled “Defining a composite key”Define a key made up of multiple properties when one event value is not enough to identify the read model instance:
using Cratis.Chronicle.Projections;
public class CompositeOrderProjection : IProjectionFor<CompositeOrder>{ public void Define(IProjectionBuilderFor<CompositeOrder> builder) => builder .From<CompositeOrderCreated>(created => created .UsingCompositeKey<CompositeOrderKey>(key => key .Set(k => k.CustomerId).To(e => e.CustomerId) .Set(k => k.OrderNumber).To(e => e.OrderNumber))) .From<CompositeOrderShipped>(shipped => shipped .UsingCompositeKey<CompositeOrderKey>(key => key .Set(k => k.CustomerId).To(e => e.CustomerId) .Set(k => k.OrderNumber).To(e => e.OrderNumber)));}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Composite key type
Section titled “Composite key type”Define a record or class to represent your composite key:
public record CompositeOrderKey(string CustomerId, string OrderNumber);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Read model with composite key
Section titled “Read model with composite key”The read model’s Id property should match your composite key type:
public record CompositeOrder( CompositeOrderKey Id, string CustomerName, DateTimeOffset OrderDate, DateTimeOffset? ShippedDate);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Composite keys with event context
Section titled “Composite keys with event context”You can combine event properties with event context properties in composite keys:
using Cratis.Chronicle.Projections;
public class AuditEntryProjectionWithCompositeKey : IProjectionFor<AuditEntryWithCompositeKey>{ public void Define(IProjectionBuilderFor<AuditEntryWithCompositeKey> builder) => builder .From<CompositeUserAction>(action => action .UsingCompositeKey<AuditEntryKey>(key => key .Set(k => k.UserId).To(e => e.UserId) .Set(k => k.Timestamp).ToEventContextProperty(c => c.Occurred)));}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.The corresponding key and read model:
public record AuditEntryKey(string UserId, DateTimeOffset Timestamp);
public record AuditEntryWithCompositeKey( AuditEntryKey Id, string Action, string Details);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Composite keys in child collections
Section titled “Composite keys in child collections”Composite keys work with child collections too:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record CompositeOrderCreatedForItems(string CustomerId, string OrderNumber);
[EventType]public record CompositeItemAddedToOrder( string CustomerId, string OrderNumber, string ProductId, string Variant, int Quantity);
public record CompositeOrderWithItems( CompositeOrderKey Id, IEnumerable<CompositeOrderItem> OrderItems);
public record CompositeOrderItem( CompositeItemKey Id, string ProductId, string Variant, int Quantity);
public record CompositeItemKey(string ProductId, string Variant);
public class CompositeOrderItemsProjection : IProjectionFor<CompositeOrderWithItems>{ public void Define(IProjectionBuilderFor<CompositeOrderWithItems> builder) => builder .From<CompositeOrderCreatedForItems>(created => created .UsingCompositeKey<CompositeOrderKey>(key => key .Set(k => k.CustomerId).To(e => e.CustomerId) .Set(k => k.OrderNumber).To(e => e.OrderNumber))) .Children(m => m.OrderItems, items => items .IdentifiedBy(m => m.Id) .From<CompositeItemAddedToOrder>(added => added .UsingParentCompositeKey<CompositeOrderKey>(key => key .Set(k => k.CustomerId).To(e => e.CustomerId) .Set(k => k.OrderNumber).To(e => e.OrderNumber)) .UsingCompositeKey<CompositeItemKey>(key => key .Set(k => k.ProductId).To(e => e.ProductId) .Set(k => k.Variant).To(e => e.Variant))));}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Joins with composite keys
Section titled “Joins with composite keys”Composite keys can be used in join scenarios:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record CompositeProductUpdated( string ProductId, string Variant, string ProductName);
public record CompositeProductKey(string ProductId, string Variant);
public record CompositeOrderLine( CompositeProductKey ProductKey, string ProductName);
public class CompositeOrderLineProjection : IProjectionFor<CompositeOrderLine>{ public void Define(IProjectionBuilderFor<CompositeOrderLine> builder) => builder .Join<CompositeProductUpdated>(product => product .On(m => m.ProductKey) .UsingCompositeKey<CompositeProductKey>(key => key .Set(k => k.ProductId).To(e => e.ProductId) .Set(k => k.Variant).To(e => e.Variant)));}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Event definitions
Section titled “Event definitions”using Cratis.Chronicle.Events;
[EventType]public record CompositeOrderCreated( string CustomerId, string OrderNumber, string CustomerName, DateTimeOffset OrderDate);
[EventType]public record CompositeOrderShipped( string CustomerId, string OrderNumber, DateTimeOffset ShippedDate);
[EventType]public record CompositeUserAction( string UserId, string Action, string Details);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Key composition rules
Section titled “Key composition rules”- Consistent structure: All events that target the same projection must use the same composite key structure
- Immutable parts: Key components should not change during the lifetime of a projection instance
- Uniqueness: The combination of all key parts must uniquely identify each projection instance
- Type safety: Key components are strongly typed and validated at compile time
Performance considerations
Section titled “Performance considerations”- Index efficiency: Composite keys create complex indexes in the underlying storage
- Query patterns: Consider how you’ll query the data when designing key structure
- Key size: Larger composite keys use more storage and may impact performance
- Sort order: The order of properties in the composite key affects index efficiency
Composite keys provide powerful flexibility for complex identification scenarios while maintaining type safety and performance.