Projection Architecture
Attributes, fluent definitions, and PDL all become projection definitions that the kernel executes. For application code, start with model-bound attributes, then fluent mappings when you need structure, then reducer code when state transitions read better; start new reducers as self-reducing models. Choose how to build a read model explains that decision. This page follows the three projection inputs through their common engine.
Three Projection Approaches
Section titled “Three Projection Approaches”1. Projection Declaration Language
Section titled “1. Projection Declaration Language”The Projection Declaration Language provides a concise, indentation-based syntax optimized for readability and quick authoring. Typically available from the tooling like Workbench.
Example:
projection MyProjection => MyApp.ReadModels.MyModel from MyEvent name = eventProperty count eventCountBenefits:
- Minimal syntax and ceremony
- Easy to read and understand
- Fast iteration with live preview
- Context-sensitive auto-completion in editors
Use when:
- Authoring a definition outside application code
- Prototyping and ad-hoc querying
- Working with Workbench or Screenplay
2. Model-Bound Projections
Section titled “2. Model-Bound Projections”Model-bound projections use attributes or annotations on your read model classes, keeping the projection logic close to the data structure.
Example:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record ArchitectureModelBoundItemAdded(string Category);
[FromEvent<ArchitectureModelBoundItemAdded>(key: nameof(ArchitectureModelBoundItemAdded.Category))]public record ArchitectureModelBoundSummary( [Key] string Category, [Count<ArchitectureModelBoundItemAdded>] int Count);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.Countimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class ArchitectureModelBoundItemAdded(val category: String)
@ReadModel@FromEvent(ArchitectureModelBoundItemAdded::class, key = "category")data class ArchitectureModelBoundSummary( @Count(ArchitectureModelBoundItemAdded::class) val count: Int = 0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.Count;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord ArchitectureModelBoundItemAdded(String category) {}
@ReadModel@FromEvent(eventType = ArchitectureModelBoundItemAdded.class, key = "category")class ArchitectureModelBoundSummary { @Count(eventType = ArchitectureModelBoundItemAdded.class) public int count = 0;}defmodule MyApp.Events.ArchitectureModelBoundItemAdded do use Chronicle.Events.EventType, id: "architecture-model-bound-item-added"
defstruct [:category]end
defmodule MyApp.ReadModels.ArchitectureModelBoundSummary do use Chronicle.ReadModels.ReadModel
defstruct category: nil, count: 0
from MyApp.Events.ArchitectureModelBoundItemAdded, key: :category, count: :countendimport { count, eventType, fromEvent } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()export class ArchitectureModelBoundItemAdded { category = '';}
@fromEvent(ArchitectureModelBoundItemAdded, { key: 'category' })export class ArchitectureModelBoundSummary { @field(String) category = '';
@count(ArchitectureModelBoundItemAdded) count = 0;}Benefits:
- Type-safe at compile time
- Co-located with read model definition
- Leverages your language’s tooling and IntelliSense
- Natural for developers already using attributes/annotations elsewhere
Use when:
- Strong type safety is required
- Using an attribute/annotation-first development workflow
- Read model and projection logic should be together
- Refactoring tools are important
3. Declarative Projections
Section titled “3. Declarative Projections”Fluent projections use code to construct a mapping definition. The kernel executes that definition;
it does not call Define or its builder lambdas for each event.
Example:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record ArchitectureDeclarativeItemAdded(string Category);
public record ArchitectureDeclarativeSummary( string Category, int Count);
public class ArchitectureDeclarativeSummaryProjection : IProjectionFor<ArchitectureDeclarativeSummary>{ public void Define(IProjectionBuilderFor<ArchitectureDeclarativeSummary> builder) => builder .From<ArchitectureDeclarativeItemAdded>(_ => _ .UsingKey(e => e.Category) .Count(m => m.Count));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
@EventTypedata class ArchitectureDeclarativeItemAdded(val category: String)
data class ArchitectureDeclarativeSummary( val category: String = "", val count: Int = 0)
class ArchitectureDeclarativeSummaryProjection : IProjectionFor<ArchitectureDeclarativeSummary> { override fun define(builder: IProjectionBuilderFor<ArchitectureDeclarativeSummary>) { builder .from(ArchitectureDeclarativeItemAdded::class) { it.usingKey("category") it.count(ArchitectureDeclarativeSummary::count) } }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
@EventTyperecord ArchitectureDeclarativeItemAdded(String category) {}
class ArchitectureDeclarativeSummary { public String category = ""; public int count = 0;}
class ArchitectureDeclarativeSummaryProjection implements IProjectionFor<ArchitectureDeclarativeSummary> { @Override public void define(IProjectionBuilderFor<ArchitectureDeclarativeSummary> builder) { builder .from(ArchitectureDeclarativeItemAdded.class, fb -> { fb.usingKey("category"); fb.count("count"); }); }}defmodule MyApp.Events.ArchitectureDeclarativeItemAdded do use Chronicle.Events.EventType, id: "architecture-declarative-item-added"
defstruct [:category]end
defmodule MyApp.ReadModels.ArchitectureDeclarativeSummary do use Chronicle.ReadModels.ReadModel
defstruct category: nil, count: 0end
defmodule MyApp.Projections.ArchitectureDeclarativeSummaryProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.ArchitectureDeclarativeSummary
from MyApp.Events.ArchitectureDeclarativeItemAdded, key: :category, count: :countendimport { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ArchitectureDeclarativeItemAdded { @field(String) readonly category: string;
constructor(category: string) { this.category = category; }}
class ArchitectureDeclarativeSummary { category = ''; count = 0;}
@projection()class ArchitectureDeclarativeSummaryProjection implements IProjectionFor<ArchitectureDeclarativeSummary> { define(builder: IProjectionBuilderFor<ArchitectureDeclarativeSummary>): void { builder.from(ArchitectureDeclarativeItemAdded, _ => _ .usingKey(e => e.category) .count(m => m.count)); }}Benefits:
- Explicit keys, context mappings, joins, children, and nested objects
- Separation of model and mapping
- Programmatic construction of supported mapping expressions
Use when:
- Attributes make the mapping hard to scan
- The model must remain free of mapping attributes
- You need specialized builder operations
Constructor dependencies can help construct the definition, but they are not injected into a per-event fold. Use a reducer for arbitrary branching over previous state.
Architecture Components
Section titled “Architecture Components”Frontend Layer
Section titled “Frontend Layer”Workbench UI: Web-based interface for creating, editing, and testing projections.
Monaco Editor with Language Service: Provides rich editing experience with:
- Syntax highlighting for all declaration keywords
- Context-sensitive auto-completion
- Real-time validation with schema awareness
- Hover documentation
- Error markers
Compilation Layer
Section titled “Compilation Layer”Screenplay Compiler: The projection language is part of the Cratis Screenplay language, and parsing is owned by the Cratis.Screenplay package:
- Indentation-based structure with 1-based line and column tracking
- Keywords, identifiers, string literals and expressions
- Nested blocks (children, nested objects, joins, composite keys)
- Syntax errors reported as diagnostics with precise locations
Semantic Validator: Performs semantic validation over the parsed syntax:
- Checks event types exist
- Validates property names against schemas
- Ensures composite key types are objects
- Verifies expression syntax
ProjectionDefinition Visitor: Chronicle’s implementation of Screenplay’s IProjectionSyntaxVisitor<T> that turns the syntax tree into the final ProjectionDefinition object all three approaches produce.
Core Layer
Section titled “Core Layer”ProjectionDefinition: Unified representation of projection logic, regardless of source (Projection Declaration, Model-Bound, or Declarative).
Projection Registry: Manages all registered projections in the system.
Projection Engine: Coordinates projection execution, handles event replay, and manages state.
Runtime Layer
Section titled “Runtime Layer”Event Stream: Source of events to be projected.
Projector: Executes projection logic for each event:
- Applies transformations
- Manages read model instances
- Handles keys and lookups
- Performs joins and child operations
Storage Sink: Persists read models (typically MongoDB or another database).
Data Flow
Section titled “Data Flow”Choosing an approach
Section titled “Choosing an approach”Use the read-model decision guide for scenarios and a capability comparison. PDL suits definitions authored in tooling; it does not replace the model-bound-first path for application code.
Common Patterns Across All Approaches
Section titled “Common Patterns Across All Approaches”Regardless of which approach you choose, all projections share these concepts:
- Event Types: Define which events trigger the projection
- Keys: Identify read model instances
- Property Mappings: Transform event data to read model properties
- Joins: Correlate events across event sources
- Children: Model parent-child relationships
- Subscriptions: Select event types; projections do not have append-metadata subscription filters
- Operations: Set, increment, decrement, count, add, subtract
All three inputs produce the same ProjectionDefinition format. Their authoring and configuration
surfaces differ: sharing an engine does not mean that PDL has every option exposed by the code builders.