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)));}import io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
class CompositeOrderProjection : IProjectionFor<CompositeOrder> { override fun define(builder: IProjectionBuilderFor<CompositeOrder>) { builder .from(CompositeOrderCreated::class) { it.usingCompositeKey { key -> key.property("customerId", "customerId") .property("orderNumber", "orderNumber") } } .from(CompositeOrderShipped::class) { it.usingCompositeKey { key -> key.property("customerId", "customerId") .property("orderNumber", "orderNumber") } } }}import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
class CompositeOrderProjection implements IProjectionFor<CompositeOrder> { @Override public void define(IProjectionBuilderFor<CompositeOrder> builder) { builder.from(CompositeOrderCreated.class, it -> { it.usingCompositeKey(key -> { key.property("customerId", "customerId").property("orderNumber", "orderNumber"); }); }); builder.from(CompositeOrderShipped.class, it -> { it.usingCompositeKey(key -> { key.property("customerId", "customerId").property("orderNumber", "orderNumber"); }); }); }}Elixir does not support this workflow yet.import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@projection('')class CompositeOrderProjection implements IProjectionFor<CompositeOrder> { define(builder: IProjectionBuilderFor<CompositeOrder>): void { builder .from(CompositeOrderCreated, from => from .usingCompositeKey<CompositeOrderKey>(key => key .set(target => target.customerId, event => event.customerId) .set(target => target.orderNumber, event => event.orderNumber))) .from(CompositeOrderShipped, from => from .usingCompositeKey<CompositeOrderKey>(key => key .set(target => target.customerId, event => event.customerId) .set(target => target.orderNumber, event => event.orderNumber))); }}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.defmodule MyApp.ReadModels.CompositeOrderKey do defstruct [:customer_id, :order_number]endclass CompositeOrderKey { customerId = ''; orderNumber = '';}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);data class CompositeOrder( val customerName: String = "", val orderDate: String = "", val shippedDate: String? = null)class CompositeOrder { public String customerName = ""; public String orderDate = ""; public String shippedDate = null;}defmodule MyApp.ReadModels.CompositeOrder do defstruct [:id, :customer_name, :order_date, :shipped_date]endimport { readModel } from '@cratis/chronicle';
@readModel()class CompositeOrder { id = ''; customerName = ''; orderDate = new Date(); shippedDate?: Date;}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.defmodule MyApp.ReadModels.AuditEntryKey do defstruct [:user_id, :timestamp]end
defmodule MyApp.ReadModels.AuditEntryWithCompositeKey do defstruct [:id, :action, :details]endTypeScript 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.import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle';
@eventType()class CompositeItemAddedToOrder { constructor( readonly customerId: string = '', readonly orderNumber: string = '', readonly productId: string = '', readonly variant: string = '', readonly quantity: number = 0 ) {}}
class CompositeItemKey { productId = ''; variant = '';}
class CompositeOrderItem { id = ''; productId = ''; variant = ''; quantity = 0;}
@readModel()class CompositeOrderWithItems { id = ''; orderItems: CompositeOrderItem[] = [];}
@projection('', CompositeOrderWithItems)class CompositeOrderItemsProjection implements IProjectionFor<CompositeOrderWithItems> { define(builder: IProjectionBuilderFor<CompositeOrderWithItems>): void { builder .from(CompositeOrderCreated, from => from .usingCompositeKey<CompositeOrderKey>(key => key .set(target => target.customerId, event => event.customerId) .set(target => target.orderNumber, event => event.orderNumber))) .children<CompositeOrderItem>(model => model.orderItems, items => items .identifiedBy(model => model.id) .from(CompositeItemAddedToOrder, from => from .usingParentCompositeKey<CompositeOrderKey>(key => key .set(target => target.customerId, event => event.customerId) .set(target => target.orderNumber, event => event.orderNumber)) .usingCompositeKey<CompositeItemKey>(key => key .set(target => target.productId, event => event.productId) .set(target => target.variant, event => event.variant)))); }}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.import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle';
@eventType()class CompositeProductUpdated { constructor( readonly productId: string = '', readonly variant: string = '', readonly productName: string = '' ) {}}
class CompositeProductKey { productId = ''; variant = '';}
@readModel()class CompositeOrderLine { id = ''; productName = '';}
@projection('', CompositeOrderLine)class CompositeOrderLineProjection implements IProjectionFor<CompositeOrderLine> { define(builder: IProjectionBuilderFor<CompositeOrderLine>): void { builder .join(CompositeProductUpdated, join => join .on(model => model.id) .usingCompositeKey<CompositeProductKey>(key => key .set(target => target.productId, event => event.productId) .set(target => target.variant, event => event.variant))); }}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);import io.cratis.chronicle.events.EventType
@EventTypedata class CompositeOrderCreated( val customerId: String, val orderNumber: String, val customerName: String, val orderDate: String)
@EventTypedata class CompositeOrderShipped( val customerId: String, val orderNumber: String, val shippedDate: String)
@EventTypedata class CompositeUserAction( val userId: String, val action: String, val details: String)import io.cratis.chronicle.events.EventType;
@EventTyperecord CompositeOrderCreated( String customerId, String orderNumber, String customerName, String orderDate) {}
@EventTyperecord CompositeOrderShipped( String customerId, String orderNumber, String shippedDate) {}
@EventTyperecord CompositeUserAction( String userId, String action, String details) {}defmodule MyApp.Events.CompositeOrderCreated do use Chronicle.Events.EventType, id: "composite-order-created"
defstruct [:customer_id, :order_number, :customer_name, :order_date]end
defmodule MyApp.Events.CompositeOrderShipped do use Chronicle.Events.EventType, id: "composite-order-shipped"
defstruct [:customer_id, :order_number, :shipped_date]end
defmodule MyApp.Events.CompositeUserAction do use Chronicle.Events.EventType, id: "composite-user-action"
defstruct [:user_id, :action, :details]endimport { eventType } from '@cratis/chronicle';
@eventType()class CompositeOrderCreated { constructor( readonly customerId: string = '', readonly orderNumber: string = '', readonly customerName: string = '', readonly orderDate: Date = new Date() ) {}}
@eventType()class CompositeOrderShipped { constructor( readonly customerId: string = '', readonly orderNumber: string = '', readonly shippedDate: Date = new Date() ) {}}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.