Projection Architecture
Chronicle supports three distinct approaches for defining projections, each designed for different use cases and developer preferences. This page explains the architecture and how these approaches work together.
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 MyModel from MyEvent set name = $.eventProperty set count = $count + 1Benefits:
- Minimal syntax and ceremony
- Easy to read and understand
- Fast iteration with live preview
- Context-sensitive auto-completion in editors
Use when:
- Building new projections
- Prototyping and experimentation
- Working with the Workbench UI
- Team prefers concise, declarative syntax
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);Kotlin does not support this workflow yet.The `@FromEvent`/`@SetFrom` model-bound annotations have no counting equivalent toC#'s `[Count<TEvent>]` — track the client SDK issue before relying on model-boundevent counting from Kotlin.Java does not support this workflow yet.The `@FromEvent`/`@SetFrom` model-bound annotations have no counting equivalent toC#'s `[Count<TEvent>]` — track the client SDK issue before relying on model-boundevent counting from Java.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, readModel } from '@cratis/chronicle';
@eventType()class ArchitectureModelBoundItemAdded { category = '';}
@readModel()@fromEvent(ArchitectureModelBoundItemAdded, { key: 'category' })class ArchitectureModelBoundSummary { 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”Declarative projections use a fluent API to define projections programmatically with maximum flexibility.
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));}Kotlin does not support this workflow yet.The fluent `IProjectionBuilderFor` API has no counting equivalent to C#'s`.Count()` — track the client SDK issue before relying on declarativeevent counting from Kotlin.Java does not support this workflow yet.The fluent `IProjectionBuilderFor` API has no counting equivalent to C#'s`.Count()` — track the client SDK issue before relying on declarativeevent counting from Java.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: :countendTypeScript does not support this workflow yet.The declarative projection builder declares a `.count()` method, but itcurrently throws "count is not implemented yet." at runtime.Track the client SDK issue before relying on declarative counting from TypeScript.Benefits:
- Full programmatic control
- Complex conditional logic
- Dynamic projection generation
- Maximum flexibility
Use when:
- Complex business rules require code
- Generating projections dynamically
- Advanced scenarios beyond declaration language capabilities
- Need for custom extensions
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”| Criterion | Declaration | Model-Bound | Declarative |
|---|---|---|---|
| Ease of Learning | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Type Safety | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Conciseness | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| Flexibility | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Refactoring Support | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Prototyping Speed | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Complex Logic | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Recommendation: Start with the Projection Declaration Language for rapid development and prototyping. Move to Model-Bound when type safety becomes critical. Use Declarative for advanced scenarios requiring programmatic control.
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: Link to other read models
- Children: Model parent-child relationships
- Filters: Conditionally process events
- Operations: Set, increment, decrement, count, add, subtract
All three approaches compile down to the same ProjectionDefinition format, ensuring consistent behavior and performance.