---
title: Variants
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

Some entities do not have one shape for their whole lifetime — a work item is a backlog entry until a
pull request exists for it, then it is a pull request until it merges. Modeling that as a single read
model with a `Status` property and an ever-growing set of nullable columns makes every query filter on
state before it can trust a field, and a shape meant for one stage leaks properties that only make sense
in another.

Variants let you declare several mutually exclusive read models for the same logical entity instead. Each
variant is an ordinary fluent projection with its own shape — only the properties that stage of the
entity's life actually has. Entering one variant automatically removes the entity from every other variant
in the group, so at any point in time an entity exists in exactly one of them.

## Declaring a group of variants

Use `.VariantOf<TIdentity>(keyAccessor)` on every projection that represents one stage, and `.EntersOn<TEvent>()`
to say which event activates that stage. `TIdentity` anchors the group — every projection declaring the
same identity type is mutually exclusive with every other:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
/// <summary>
/// Anchors the logical identity shared by DecVariantBacklogItem and DecVariantPullRequestItem.
/// Deliberately not a read model itself, and does not need a common CLR base type with either variant.
/// </summary>
public class DecVariantWorkItem;

public record DecVariantBacklogItem(Guid Id, string Title);

public record DecVariantPullRequestItem(Guid Id, string PullRequestUrl);
```

</TabItem>
<TabItem label="Kotlin">

```kotlin
/**
 * Anchors the logical identity shared by DecVariantBacklogItem and DecVariantPullRequestItem.
 * Deliberately not a read model itself, and does not need a common supertype with either variant.
 */
class DecVariantWorkItem

data class DecVariantBacklogItem(val id: String = "", val title: String = "")

data class DecVariantPullRequestItem(val id: String = "", val pullRequestUrl: String = "")
```

</TabItem>
<TabItem label="Java">

```java
/**
 * Anchors the logical identity shared by DecVariantBacklogItem and DecVariantPullRequestItem.
 * Deliberately not a read model itself, and does not need a common supertype with either variant.
 */
class DecVariantWorkItem {}

class DecVariantBacklogItem {
    public String id = "";
    public String title = "";
}

class DecVariantPullRequestItem {
    public String id = "";
    public String pullRequestUrl = "";
}
```

</TabItem>
<TabItem label="Elixir">

```elixir
# Anchors the logical identity shared by DecVariantBacklogItem and DecVariantPullRequestItem.
# Deliberately not a read model itself, and does not need a common shape with either variant.
defmodule MyApp.ReadModels.DecVariantWorkItem do
end

defmodule MyApp.ReadModels.DecVariantBacklogItem do
  use Chronicle.ReadModels.ReadModel

  defstruct [:id, :title]
end

defmodule MyApp.ReadModels.DecVariantPullRequestItem do
  use Chronicle.ReadModels.ReadModel

  defstruct [:id, :pull_request_url]
end
```

</TabItem>
<TabItem label="TypeScript">

```typescript
/**
 * Anchors the logical identity shared by DecVariantBacklogItem and DecVariantPullRequestItem.
 * Deliberately not a read model itself, and does not need a common shape with either variant.
 */
class DecVariantWorkItem {}

class DecVariantBacklogItem {
    id = '';
    title = '';
}

class DecVariantPullRequestItem {
    id = '';
    pullRequestUrl = '';
}
```

</TabItem>
</Tabs>

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;

[EventType]
public record DecVariantIssueCreated(string Title);

[EventType]
public record DecVariantPullRequestCreated(string PullRequestUrl);
```

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.events.EventType

@EventType
data class DecVariantIssueCreated(val title: String)

@EventType
data class DecVariantPullRequestCreated(val pullRequestUrl: String)
```

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.events.EventType;

@EventType
record DecVariantIssueCreated(String title) {}

@EventType
record DecVariantPullRequestCreated(String pullRequestUrl) {}
```

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.Events.DecVariantIssueCreated do
  use Chronicle.Events.EventType, id: "dec-variant-issue-created-v1"

  defstruct [:title]
end

defmodule MyApp.Events.DecVariantPullRequestCreated do
  use Chronicle.Events.EventType, id: "dec-variant-pull-request-created-v1"

  defstruct [:pull_request_url]
end
```

</TabItem>
<TabItem label="TypeScript">

```typescript
import { eventType } from '@cratis/chronicle';

@eventType()
class DecVariantIssueCreated {
    title = '';
}

@eventType()
class DecVariantPullRequestCreated {
    pullRequestUrl = '';
}
```

</TabItem>
</Tabs>

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Projections;

public class DecVariantBacklogItemProjection : IProjectionFor<DecVariantBacklogItem>
{
    public void Define(IProjectionBuilderFor<DecVariantBacklogItem> builder) => builder
        .VariantOf<DecVariantWorkItem>(_ => _.Id)
        .EntersOn<DecVariantIssueCreated>();
}

public class DecVariantPullRequestItemProjection : IProjectionFor<DecVariantPullRequestItem>
{
    public void Define(IProjectionBuilderFor<DecVariantPullRequestItem> builder) => builder
        .VariantOf<DecVariantWorkItem>(_ => _.Id)
        .EntersOn<DecVariantPullRequestCreated>();
}
```

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.projections.IProjectionBuilderFor
import io.cratis.chronicle.projections.IProjectionFor

class DecVariantBacklogItemProjection : IProjectionFor<DecVariantBacklogItem> {
    override fun define(builder: IProjectionBuilderFor<DecVariantBacklogItem>) {
        builder
            .variantOf(DecVariantWorkItem::class, DecVariantBacklogItem::id)
            .entersOn(DecVariantIssueCreated::class)
    }
}

class DecVariantPullRequestItemProjection : IProjectionFor<DecVariantPullRequestItem> {
    override fun define(builder: IProjectionBuilderFor<DecVariantPullRequestItem>) {
        builder
            .variantOf(DecVariantWorkItem::class, DecVariantPullRequestItem::id)
            .entersOn(DecVariantPullRequestCreated::class)
    }
}
```

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.projections.IProjectionBuilderFor;
import io.cratis.chronicle.projections.IProjectionFor;

class DecVariantBacklogItemProjection implements IProjectionFor<DecVariantBacklogItem> {
    @Override
    public void define(IProjectionBuilderFor<DecVariantBacklogItem> builder) {
        builder
            .variantOf(DecVariantWorkItem.class, "id")
            .entersOn(DecVariantIssueCreated.class);
    }
}

class DecVariantPullRequestItemProjection implements IProjectionFor<DecVariantPullRequestItem> {
    @Override
    public void define(IProjectionBuilderFor<DecVariantPullRequestItem> builder) {
        builder
            .variantOf(DecVariantWorkItem.class, "id")
            .entersOn(DecVariantPullRequestCreated.class);
    }
}
```

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.Projections.DecVariantBacklogItemProjection do
  use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecVariantBacklogItem

  alias MyApp.Events.DecVariantIssueCreated
  alias MyApp.ReadModels.DecVariantWorkItem

  variant_of DecVariantWorkItem, key: :id
  enters_on DecVariantIssueCreated

  from DecVariantIssueCreated,
    set: [id: :event_source_id, title: :title]
end

defmodule MyApp.Projections.DecVariantPullRequestItemProjection do
  use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecVariantPullRequestItem

  alias MyApp.Events.DecVariantPullRequestCreated
  alias MyApp.ReadModels.DecVariantWorkItem

  variant_of DecVariantWorkItem, key: :id
  enters_on DecVariantPullRequestCreated

  from DecVariantPullRequestCreated,
    set: [id: :event_source_id, pull_request_url: :pull_request_url]
end
```

</TabItem>
<TabItem label="TypeScript">

```typescript
import { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';

@projection()
class DecVariantBacklogItemProjection implements IProjectionFor<DecVariantBacklogItem> {
    define(builder: IProjectionBuilderFor<DecVariantBacklogItem>): void {
        builder
            .variantOf(DecVariantWorkItem, m => m.id)
            .entersOn(DecVariantIssueCreated);
    }
}

@projection()
class DecVariantPullRequestItemProjection implements IProjectionFor<DecVariantPullRequestItem> {
    define(builder: IProjectionBuilderFor<DecVariantPullRequestItem>): void {
        builder
            .variantOf(DecVariantWorkItem, m => m.id)
            .entersOn(DecVariantPullRequestCreated);
    }
}
```

</TabItem>
</Tabs>

`DecVariantWorkItem` does not have to be a read model itself, and it does not need a common CLR base type
with any of the variants — its only job is to be a shared type every projection in the group points at.
AutoMap is enabled by default, exactly as for any other fluent projection, so `Title` and `PullRequestUrl`
map by convention without an explicit `.Set()` call.

When an `IssueCreated` event is processed, a `DecVariantBacklogItem` is created — its own event, handled
by its own projection. When a `PullRequestCreated` event is processed for the same entity,
`DecVariantPullRequestItem` is created **and the `DecVariantBacklogItem` for that entity is removed** — no
explicit removal call needed, the group takes care of it, cross-wired automatically the first time every
variant in the group has been discovered. Variants are not required to be entered in any particular order,
only to be mutually exclusive once entered.

## Only the entering event can create a variant

Everything a variant projects from besides its entering event is declared exactly like an ordinary
multi-event projection — the same `.From<TEvent>()` call you would use on any projection. The only
difference is what that mapping is allowed to do: it can update an already-active instance of the
variant, but it can never create one.

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;

[EventType]
public record DecVariantUpdatingPullRequestCreated(string PullRequestUrl);

[EventType]
public record DecVariantUpdatingBuildCompleted(string BuildStatus);

public class DecVariantUpdatingWorkItem;

public record DecVariantUpdatingPullRequestItem(Guid Id, string PullRequestUrl, string BuildStatus);

/// <summary>
/// From&lt;DecVariantUpdatingBuildCompleted&gt; is declared exactly like an ordinary multi-event
/// projection. Because that event is NOT the one named with EntersOn, the builder automatically
/// reclassifies it into an update-only join on the variant's own key when the definition is built - it
/// can bring an already-active instance up to date, but it can never create one on its own.
/// </summary>
public class DecVariantUpdatingPullRequestItemProjection : IProjectionFor<DecVariantUpdatingPullRequestItem>
{
    public void Define(IProjectionBuilderFor<DecVariantUpdatingPullRequestItem> builder) => builder
        .VariantOf<DecVariantUpdatingWorkItem>(_ => _.Id)
        .EntersOn<DecVariantUpdatingPullRequestCreated>()
        .From<DecVariantUpdatingBuildCompleted>();
}
```

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.events.EventType
import io.cratis.chronicle.projections.IProjectionBuilderFor
import io.cratis.chronicle.projections.IProjectionFor

@EventType
data class DecVariantUpdatingPullRequestCreated(val pullRequestUrl: String)

@EventType
data class DecVariantUpdatingBuildCompleted(val buildStatus: String)

class DecVariantUpdatingWorkItem

data class DecVariantUpdatingPullRequestItem(val id: String = "", val pullRequestUrl: String = "", val buildStatus: String = "")

/**
 * from(DecVariantUpdatingBuildCompleted::class) is declared exactly like an ordinary multi-event
 * projection. Because that event is NOT the one named with entersOn, the builder automatically
 * reclassifies it into an update-only join on the variant's own key when the definition is built -
 * it can bring an already-active instance up to date, but it can never create one on its own.
 */
class DecVariantUpdatingPullRequestItemProjection : IProjectionFor<DecVariantUpdatingPullRequestItem> {
    override fun define(builder: IProjectionBuilderFor<DecVariantUpdatingPullRequestItem>) {
        builder
            .variantOf(DecVariantUpdatingWorkItem::class, DecVariantUpdatingPullRequestItem::id)
            .entersOn(DecVariantUpdatingPullRequestCreated::class)
            .from(DecVariantUpdatingBuildCompleted::class)
    }
}
```

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.projections.IProjectionBuilderFor;
import io.cratis.chronicle.projections.IProjectionFor;

@EventType
record DecVariantUpdatingPullRequestCreated(String pullRequestUrl) {}

@EventType
record DecVariantUpdatingBuildCompleted(String buildStatus) {}

class DecVariantUpdatingWorkItem {}

class DecVariantUpdatingPullRequestItem {
    public String id = "";
    public String pullRequestUrl = "";
    public String buildStatus = "";
}

/**
 * from(DecVariantUpdatingBuildCompleted.class) is declared exactly like an ordinary multi-event
 * projection. Because that event is NOT the one named with entersOn, the builder automatically
 * reclassifies it into an update-only join on the variant's own key when the definition is built -
 * it can bring an already-active instance up to date, but it can never create one on its own.
 */
class DecVariantUpdatingPullRequestItemProjection implements IProjectionFor<DecVariantUpdatingPullRequestItem> {
    @Override
    public void define(IProjectionBuilderFor<DecVariantUpdatingPullRequestItem> builder) {
        builder
            .variantOf(DecVariantUpdatingWorkItem.class, "id")
            .entersOn(DecVariantUpdatingPullRequestCreated.class)
            .from(DecVariantUpdatingBuildCompleted.class);
    }
}
```

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.Events.DecVariantUpdatingPullRequestCreated do
  use Chronicle.Events.EventType, id: "dec-variant-updating-pull-request-created-v1"

  defstruct [:pull_request_url]
end

defmodule MyApp.Events.DecVariantUpdatingBuildCompleted do
  use Chronicle.Events.EventType, id: "dec-variant-updating-build-completed-v1"

  defstruct [:build_status]
end

defmodule MyApp.ReadModels.DecVariantUpdatingWorkItem do
end

defmodule MyApp.ReadModels.DecVariantUpdatingPullRequestItem do
  use Chronicle.ReadModels.ReadModel

  defstruct [:id, :pull_request_url, :build_status]
end

# `from DecVariantUpdatingBuildCompleted` is declared exactly like an ordinary multi-event
# projection. Because that event is NOT the one named with enters_on, it is automatically
# reclassified into an update-only join on the variant's own key when the definition is built -
# it can bring an already-active instance up to date, but it can never create one on its own.
defmodule MyApp.Projections.DecVariantUpdatingPullRequestItemProjection do
  use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecVariantUpdatingPullRequestItem

  alias MyApp.Events.{DecVariantUpdatingPullRequestCreated, DecVariantUpdatingBuildCompleted}
  alias MyApp.ReadModels.DecVariantUpdatingWorkItem

  variant_of DecVariantUpdatingWorkItem, key: :id
  enters_on DecVariantUpdatingPullRequestCreated

  from DecVariantUpdatingPullRequestCreated,
    set: [id: :event_source_id, pull_request_url: :pull_request_url]

  from DecVariantUpdatingBuildCompleted,
    set: [build_status: :build_status]
end
```

</TabItem>
<TabItem label="TypeScript">

```typescript
import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';

@eventType()
class DecVariantUpdatingPullRequestCreated {
    pullRequestUrl = '';
}

@eventType()
class DecVariantUpdatingBuildCompleted {
    buildStatus = '';
}

class DecVariantUpdatingWorkItem {}

class DecVariantUpdatingPullRequestItem {
    id = '';
    pullRequestUrl = '';
    buildStatus = '';
}

/**
 * .from(DecVariantUpdatingBuildCompleted) is declared exactly like an ordinary multi-event
 * projection. Because that event is NOT the one named with entersOn, the builder automatically
 * reclassifies it into an update-only join on the variant's own key when the definition is built -
 * it can bring an already-active instance up to date, but it can never create one on its own.
 */
@projection()
class DecVariantUpdatingPullRequestItemProjection implements IProjectionFor<DecVariantUpdatingPullRequestItem> {
    define(builder: IProjectionBuilderFor<DecVariantUpdatingPullRequestItem>): void {
        builder
            .variantOf(DecVariantUpdatingWorkItem, m => m.id)
            .entersOn(DecVariantUpdatingPullRequestCreated)
            .from(DecVariantUpdatingBuildCompleted);
    }
}
```

</TabItem>
</Tabs>

`.From<DecVariantUpdatingBuildCompleted>()` looks like an ordinary event subscription, but because
`DecVariantUpdatingBuildCompleted` is not the event named with `.EntersOn<T>()`, the builder reclassifies
it into an update-only join — keyed on the same property `.VariantOf<T>(_ => _.Id)` declared — before the
definition ever reaches the projection engine. The entering event stays the only door into the variant.

:::caution[Why this matters]
Without that reclassification, an out-of-order or replayed `BuildCompleted` event could **resurrect** a
pull-request variant for an entity that has since moved on to another variant, or even one that was never
a pull request at all — the exact bug this feature exists to make structurally impossible. You do not have
to reason about event ordering to get this guarantee; declaring `.EntersOn<T>()` is what gives it to you.
:::

## Sharing handlers across variants

Unlike model-bound projections, the fluent API has no equivalent of `[GlobalFor<T>]`. Each fluent
projection is its own class producing its own definition, so a mapping every variant needs — a title
every stage keeps up to date, for example — is declared with `.From<TEvent>()` on each variant's builder
individually. If the same shared handler is repeated across several variants often enough to be a
maintenance concern, that repetition is itself a reason to reach for
[model-bound projections](/chronicle/projections/model-bound/variants/) for that group instead.

## Complete example

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;

[EventType]
public record DecVariantFullIssueCreated(string Title);

[EventType]
public record DecVariantFullPullRequestCreated(string PullRequestUrl);

[EventType]
public record DecVariantFullBuildCompleted(string BuildStatus);

/// <summary>
/// Anchors the logical identity shared by DecVariantFullBacklogItem and
/// DecVariantFullPullRequestItem. Deliberately not a read model itself.
/// </summary>
public class DecVariantFullWorkItem;

public record DecVariantFullBacklogItem(Guid Id, string Title);

public record DecVariantFullPullRequestItem(Guid Id, string PullRequestUrl, string BuildStatus);

/// <summary>
/// The variant an entity is in before a pull request exists for it.
/// </summary>
public class DecVariantFullBacklogItemProjection : IProjectionFor<DecVariantFullBacklogItem>
{
    public void Define(IProjectionBuilderFor<DecVariantFullBacklogItem> builder) => builder
        .VariantOf<DecVariantFullWorkItem>(_ => _.Id)
        .EntersOn<DecVariantFullIssueCreated>();
}

/// <summary>
/// The variant an entity enters once a pull request is created for it. BuildStatus comes from
/// DecVariantFullBuildCompleted - an event that is NOT this variant's entering event, so the builder
/// reclassifies it into an update-only join and it can never create the row on its own.
/// </summary>
public class DecVariantFullPullRequestItemProjection : IProjectionFor<DecVariantFullPullRequestItem>
{
    public void Define(IProjectionBuilderFor<DecVariantFullPullRequestItem> builder) => builder
        .VariantOf<DecVariantFullWorkItem>(_ => _.Id)
        .EntersOn<DecVariantFullPullRequestCreated>()
        .From<DecVariantFullBuildCompleted>();
}
```

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.events.EventType
import io.cratis.chronicle.projections.IProjectionBuilderFor
import io.cratis.chronicle.projections.IProjectionFor

@EventType
data class DecVariantFullIssueCreated(val title: String)

@EventType
data class DecVariantFullPullRequestCreated(val pullRequestUrl: String)

@EventType
data class DecVariantFullBuildCompleted(val buildStatus: String)

/**
 * Anchors the logical identity shared by DecVariantFullBacklogItem and
 * DecVariantFullPullRequestItem. Deliberately not a read model itself.
 */
class DecVariantFullWorkItem

data class DecVariantFullBacklogItem(val id: String = "", val title: String = "")

data class DecVariantFullPullRequestItem(val id: String = "", val pullRequestUrl: String = "", val buildStatus: String = "")

/** The variant an entity is in before a pull request exists for it. */
class DecVariantFullBacklogItemProjection : IProjectionFor<DecVariantFullBacklogItem> {
    override fun define(builder: IProjectionBuilderFor<DecVariantFullBacklogItem>) {
        builder
            .variantOf(DecVariantFullWorkItem::class, DecVariantFullBacklogItem::id)
            .entersOn(DecVariantFullIssueCreated::class)
    }
}

/**
 * The variant an entity enters once a pull request is created for it. buildStatus comes from
 * DecVariantFullBuildCompleted - an event that is NOT this variant's entering event, so the
 * builder reclassifies it into an update-only join and it can never create the row on its own.
 */
class DecVariantFullPullRequestItemProjection : IProjectionFor<DecVariantFullPullRequestItem> {
    override fun define(builder: IProjectionBuilderFor<DecVariantFullPullRequestItem>) {
        builder
            .variantOf(DecVariantFullWorkItem::class, DecVariantFullPullRequestItem::id)
            .entersOn(DecVariantFullPullRequestCreated::class)
            .from(DecVariantFullBuildCompleted::class)
    }
}
```

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.projections.IProjectionBuilderFor;
import io.cratis.chronicle.projections.IProjectionFor;

@EventType
record DecVariantFullIssueCreated(String title) {}

@EventType
record DecVariantFullPullRequestCreated(String pullRequestUrl) {}

@EventType
record DecVariantFullBuildCompleted(String buildStatus) {}

/**
 * Anchors the logical identity shared by DecVariantFullBacklogItem and
 * DecVariantFullPullRequestItem. Deliberately not a read model itself.
 */
class DecVariantFullWorkItem {}

class DecVariantFullBacklogItem {
    public String id = "";
    public String title = "";
}

class DecVariantFullPullRequestItem {
    public String id = "";
    public String pullRequestUrl = "";
    public String buildStatus = "";
}

/** The variant an entity is in before a pull request exists for it. */
class DecVariantFullBacklogItemProjection implements IProjectionFor<DecVariantFullBacklogItem> {
    @Override
    public void define(IProjectionBuilderFor<DecVariantFullBacklogItem> builder) {
        builder
            .variantOf(DecVariantFullWorkItem.class, "id")
            .entersOn(DecVariantFullIssueCreated.class);
    }
}

/**
 * The variant an entity enters once a pull request is created for it. buildStatus comes from
 * DecVariantFullBuildCompleted - an event that is NOT this variant's entering event, so the
 * builder reclassifies it into an update-only join and it can never create the row on its own.
 */
class DecVariantFullPullRequestItemProjection implements IProjectionFor<DecVariantFullPullRequestItem> {
    @Override
    public void define(IProjectionBuilderFor<DecVariantFullPullRequestItem> builder) {
        builder
            .variantOf(DecVariantFullWorkItem.class, "id")
            .entersOn(DecVariantFullPullRequestCreated.class)
            .from(DecVariantFullBuildCompleted.class);
    }
}
```

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.Events.DecVariantFullIssueCreated do
  use Chronicle.Events.EventType, id: "dec-variant-full-issue-created-v1"

  defstruct [:title]
end

defmodule MyApp.Events.DecVariantFullPullRequestCreated do
  use Chronicle.Events.EventType, id: "dec-variant-full-pull-request-created-v1"

  defstruct [:pull_request_url]
end

defmodule MyApp.Events.DecVariantFullBuildCompleted do
  use Chronicle.Events.EventType, id: "dec-variant-full-build-completed-v1"

  defstruct [:build_status]
end

# Anchors the logical identity shared by DecVariantFullBacklogItem and
# DecVariantFullPullRequestItem. Deliberately not a read model itself.
defmodule MyApp.ReadModels.DecVariantFullWorkItem do
end

defmodule MyApp.ReadModels.DecVariantFullBacklogItem do
  use Chronicle.ReadModels.ReadModel

  defstruct [:id, :title]
end

defmodule MyApp.ReadModels.DecVariantFullPullRequestItem do
  use Chronicle.ReadModels.ReadModel

  defstruct [:id, :pull_request_url, :build_status]
end

# The variant an entity is in before a pull request exists for it.
defmodule MyApp.Projections.DecVariantFullBacklogItemProjection do
  use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecVariantFullBacklogItem

  alias MyApp.Events.DecVariantFullIssueCreated
  alias MyApp.ReadModels.DecVariantFullWorkItem

  variant_of DecVariantFullWorkItem, key: :id
  enters_on DecVariantFullIssueCreated

  from DecVariantFullIssueCreated,
    set: [id: :event_source_id, title: :title]
end

# The variant an entity enters once a pull request is created for it. build_status comes from
# DecVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it is
# reclassified into an update-only join and can never create the row on its own.
defmodule MyApp.Projections.DecVariantFullPullRequestItemProjection do
  use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecVariantFullPullRequestItem

  alias MyApp.Events.{DecVariantFullPullRequestCreated, DecVariantFullBuildCompleted}
  alias MyApp.ReadModels.DecVariantFullWorkItem

  variant_of DecVariantFullWorkItem, key: :id
  enters_on DecVariantFullPullRequestCreated

  from DecVariantFullPullRequestCreated,
    set: [id: :event_source_id, pull_request_url: :pull_request_url]

  from DecVariantFullBuildCompleted,
    set: [build_status: :build_status]
end
```

</TabItem>
<TabItem label="TypeScript">

```typescript
import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';

@eventType()
class DecVariantFullIssueCreated {
    title = '';
}

@eventType()
class DecVariantFullPullRequestCreated {
    pullRequestUrl = '';
}

@eventType()
class DecVariantFullBuildCompleted {
    buildStatus = '';
}

/**
 * Anchors the logical identity shared by DecVariantFullBacklogItem and
 * DecVariantFullPullRequestItem. Deliberately not a read model itself.
 */
class DecVariantFullWorkItem {}

class DecVariantFullBacklogItem {
    id = '';
    title = '';
}

class DecVariantFullPullRequestItem {
    id = '';
    pullRequestUrl = '';
    buildStatus = '';
}

/** The variant an entity is in before a pull request exists for it. */
@projection()
class DecVariantFullBacklogItemProjection implements IProjectionFor<DecVariantFullBacklogItem> {
    define(builder: IProjectionBuilderFor<DecVariantFullBacklogItem>): void {
        builder
            .variantOf(DecVariantFullWorkItem, m => m.id)
            .entersOn(DecVariantFullIssueCreated);
    }
}

/**
 * The variant an entity enters once a pull request is created for it. buildStatus comes from
 * DecVariantFullBuildCompleted - an event that is NOT this variant's entering event, so the
 * builder reclassifies it into an update-only join and it can never create the row on its own.
 */
@projection()
class DecVariantFullPullRequestItemProjection implements IProjectionFor<DecVariantFullPullRequestItem> {
    define(builder: IProjectionBuilderFor<DecVariantFullPullRequestItem>): void {
        builder
            .variantOf(DecVariantFullWorkItem, m => m.id)
            .entersOn(DecVariantFullPullRequestCreated)
            .from(DecVariantFullBuildCompleted);
    }
}
```

</TabItem>
</Tabs>

## Best practices

1. **Pick an identity type that means something on its own** — `DecVariantWorkItem` in the examples above,
   not a marker interface with no purpose beyond grouping. It is the type every variant in the group
   points back to.
2. **Give every variant only the properties that stage of the entity actually has.** A property every
   stage needs is still declared on each variant's builder individually — see "Sharing handlers" above.
3. **Reach for variants when the shapes genuinely diverge.** A single read model with a `Status` property
   is still the right choice when every stage shares almost all of its properties and differs only in a
   flag or two — see [Choosing a read-model style](/chronicle/projections/choosing-a-read-model-style/).
4. **Remember each variant is its own collection.** There is no built-in query that spans a whole group —
   a caller that needs "this entity, whichever stage it's currently in" queries each variant explicitly
   rather than assuming a single combined collection.
