---
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, then it is closed. 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 model-bound 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>]` on every read model that represents one stage, and `[EntersOn<TEvent>]` to say
which event activates that stage. `TIdentity` anchors the group — every variant marked with the same
identity type is mutually exclusive with every other:

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

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

[EventType]
public record MbVariantIssueCreated(string Title);

[EventType]
public record MbVariantPullRequestCreated(string PullRequestUrl);

/// <summary>
/// Anchors the logical identity shared by every variant. It does not need to be a
/// read model itself, and it does not need a common CLR base type with any of the variants.
/// </summary>
public class MbVariantWorkItem;

[VariantOf<MbVariantWorkItem>]
[EntersOn<MbVariantIssueCreated>]
public record MbVariantBacklogItem([property: Key] Guid Id, string Title);

[VariantOf<MbVariantWorkItem>]
[EntersOn<MbVariantPullRequestCreated>]
public record MbVariantPullRequestItem([property: Key] Guid Id, [property: SetFrom<MbVariantPullRequestCreated>] string PullRequestUrl);
```

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

```kotlin
import io.cratis.chronicle.events.EventType
import io.cratis.chronicle.projections.EntersOn
import io.cratis.chronicle.projections.FromEvent
import io.cratis.chronicle.projections.FromEventSourceId
import io.cratis.chronicle.projections.SetFrom
import io.cratis.chronicle.projections.VariantOf
import io.cratis.chronicle.readModels.ReadModel

@EventType
data class MbVariantIssueCreated(val title: String)

@EventType
data class MbVariantPullRequestCreated(val pullRequestUrl: String)

/**
 * Anchors the logical identity shared by every variant. It does not need to be a read model
 * itself, and it does not need a common supertype with any of the variants.
 */
class MbVariantWorkItem

@ReadModel
@VariantOf(MbVariantWorkItem::class, key = "id")
@EntersOn(MbVariantIssueCreated::class)
@FromEvent(MbVariantIssueCreated::class)
data class MbVariantBacklogItem(
    @FromEventSourceId
    val id: String = "",

    @SetFrom("title", MbVariantIssueCreated::class)
    val title: String = ""
)

@ReadModel
@VariantOf(MbVariantWorkItem::class, key = "id")
@EntersOn(MbVariantPullRequestCreated::class)
@FromEvent(MbVariantPullRequestCreated::class)
data class MbVariantPullRequestItem(
    @FromEventSourceId
    val id: String = "",

    @SetFrom("pullRequestUrl", MbVariantPullRequestCreated::class)
    val pullRequestUrl: String = ""
)
```

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

```java
import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.projections.EntersOn;
import io.cratis.chronicle.projections.FromEvent;
import io.cratis.chronicle.projections.FromEventSourceId;
import io.cratis.chronicle.projections.SetFrom;
import io.cratis.chronicle.projections.VariantOf;
import io.cratis.chronicle.readModels.ReadModel;

@EventType
record MbVariantIssueCreated(String title) {}

@EventType
record MbVariantPullRequestCreated(String pullRequestUrl) {}

/**
 * Anchors the logical identity shared by every variant. It does not need to be a read model
 * itself, and it does not need a common supertype with any of the variants.
 */
class MbVariantWorkItem {}

@ReadModel
@VariantOf(identity = MbVariantWorkItem.class, key = "id")
@EntersOn(eventType = MbVariantIssueCreated.class)
@FromEvent(eventType = MbVariantIssueCreated.class)
class MbVariantBacklogItem {
    @FromEventSourceId
    public String id = "";

    @SetFrom(propertyPath = "title", eventType = MbVariantIssueCreated.class)
    public String title = "";
}

@ReadModel
@VariantOf(identity = MbVariantWorkItem.class, key = "id")
@EntersOn(eventType = MbVariantPullRequestCreated.class)
@FromEvent(eventType = MbVariantPullRequestCreated.class)
class MbVariantPullRequestItem {
    @FromEventSourceId
    public String id = "";

    @SetFrom(propertyPath = "pullRequestUrl", eventType = MbVariantPullRequestCreated.class)
    public String pullRequestUrl = "";
}
```

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

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

  defstruct [:title]
end

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

  defstruct [:pull_request_url]
end

# Anchors the logical identity shared by every variant. It does not need to be a read model
# itself, and it does not need a common shape with any of the variants.
defmodule MyApp.ReadModels.MbVariantWorkItem do
end

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

  alias MyApp.Events.MbVariantIssueCreated
  alias MyApp.ReadModels.MbVariantWorkItem

  defstruct [:id, :title]

  variant_of MbVariantWorkItem, key: :id
  enters_on MbVariantIssueCreated

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

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

  alias MyApp.Events.MbVariantPullRequestCreated
  alias MyApp.ReadModels.MbVariantWorkItem

  defstruct [:id, :pull_request_url]

  variant_of MbVariantWorkItem, key: :id
  enters_on MbVariantPullRequestCreated

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

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

```typescript
import { entersOn, eventType, fromEvent, readModel, setFrom, variantOf } from '@cratis/chronicle';

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

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

/**
 * Anchors the logical identity shared by every variant. It does not need to be a read model
 * itself, and it does not need a common shape with any of the variants.
 */
class MbVariantWorkItem {}

@variantOf(MbVariantWorkItem, 'id')
@entersOn(MbVariantIssueCreated)
@fromEvent(MbVariantIssueCreated)
@readModel()
class MbVariantBacklogItem {
    id = '';

    @setFrom(MbVariantIssueCreated, 'title')
    title = '';
}

@variantOf(MbVariantWorkItem, 'id')
@entersOn(MbVariantPullRequestCreated)
@fromEvent(MbVariantPullRequestCreated)
@readModel()
class MbVariantPullRequestItem {
    id = '';

    @setFrom(MbVariantPullRequestCreated, 'pullRequestUrl')
    pullRequestUrl = '';
}
```

</TabItem>
</Tabs>

`WorkItem` 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 variant in the group points at.

When an `IssueCreated` event is processed, a `BacklogItem` is created — `Title` maps by convention, exactly
as it would on an ordinary `[FromEvent<T>]` projection. When a `PullRequestCreated` event is processed for
the same entity, a `PullRequestItem` is created **and the `BacklogItem` for that entity is removed** — no
`RemovedWith` attribute needed, the group takes care of it. If `PullRequestCreated` arrives for an entity
that was never a `BacklogItem` at all, `PullRequestItem` is still created; 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 maps from besides its entering event is still declared exactly like an ordinary
projection — `[SetFrom<TEvent>]`, `[Key]`, and the rest all work the same way. 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
[EventType]
public record MbVariantUpdatingPullRequestCreated(string PullRequestUrl);

[EventType]
public record MbVariantUpdatingBuildCompleted(string BuildStatus);

public class MbVariantUpdatingWorkItem;

/// <summary>
/// BuildStatus is mapped from MbVariantUpdatingBuildCompleted - an event that is NOT this
/// variant's entering event, so it is automatically reclassified into an update-only join. It can bring
/// an already-active instance up to date, but it can never create one on its own.
/// </summary>
[VariantOf<MbVariantUpdatingWorkItem>]
[EntersOn<MbVariantUpdatingPullRequestCreated>]
public record MbVariantUpdatingPullRequestItem(
    [property: Key] Guid Id,
    [property: SetFrom<MbVariantUpdatingPullRequestCreated>] string PullRequestUrl,
    [property: SetFrom<MbVariantUpdatingBuildCompleted>] string BuildStatus);
```

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

```kotlin
import io.cratis.chronicle.events.EventType
import io.cratis.chronicle.projections.EntersOn
import io.cratis.chronicle.projections.FromEvent
import io.cratis.chronicle.projections.FromEventSourceId
import io.cratis.chronicle.projections.SetFrom
import io.cratis.chronicle.projections.VariantOf
import io.cratis.chronicle.readModels.ReadModel

@EventType
data class MbVariantUpdatingPullRequestCreated(val pullRequestUrl: String)

@EventType
data class MbVariantUpdatingBuildCompleted(val buildStatus: String)

class MbVariantUpdatingWorkItem

/**
 * buildStatus is mapped from MbVariantUpdatingBuildCompleted - an event that is NOT this variant's
 * entering event, so it is automatically reclassified into an update-only join. It can bring an
 * already-active instance up to date, but it can never create one on its own.
 */
@ReadModel
@VariantOf(MbVariantUpdatingWorkItem::class, key = "id")
@EntersOn(MbVariantUpdatingPullRequestCreated::class)
@FromEvent(MbVariantUpdatingPullRequestCreated::class)
@FromEvent(MbVariantUpdatingBuildCompleted::class)
data class MbVariantUpdatingPullRequestItem(
    @FromEventSourceId
    val id: String = "",

    @SetFrom("pullRequestUrl", MbVariantUpdatingPullRequestCreated::class)
    val pullRequestUrl: String = "",

    @SetFrom("buildStatus", MbVariantUpdatingBuildCompleted::class)
    val buildStatus: String = ""
)
```

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

```java
import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.projections.EntersOn;
import io.cratis.chronicle.projections.FromEvent;
import io.cratis.chronicle.projections.FromEventSourceId;
import io.cratis.chronicle.projections.SetFrom;
import io.cratis.chronicle.projections.VariantOf;
import io.cratis.chronicle.readModels.ReadModel;

@EventType
record MbVariantUpdatingPullRequestCreated(String pullRequestUrl) {}

@EventType
record MbVariantUpdatingBuildCompleted(String buildStatus) {}

class MbVariantUpdatingWorkItem {}

/**
 * buildStatus is mapped from MbVariantUpdatingBuildCompleted - an event that is NOT this variant's
 * entering event, so it is automatically reclassified into an update-only join. It can bring an
 * already-active instance up to date, but it can never create one on its own.
 */
@ReadModel
@VariantOf(identity = MbVariantUpdatingWorkItem.class, key = "id")
@EntersOn(eventType = MbVariantUpdatingPullRequestCreated.class)
@FromEvent(eventType = MbVariantUpdatingPullRequestCreated.class)
@FromEvent(eventType = MbVariantUpdatingBuildCompleted.class)
class MbVariantUpdatingPullRequestItem {
    @FromEventSourceId
    public String id = "";

    @SetFrom(propertyPath = "pullRequestUrl", eventType = MbVariantUpdatingPullRequestCreated.class)
    public String pullRequestUrl = "";

    @SetFrom(propertyPath = "buildStatus", eventType = MbVariantUpdatingBuildCompleted.class)
    public String buildStatus = "";
}
```

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

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

  defstruct [:pull_request_url]
end

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

  defstruct [:build_status]
end

defmodule MyApp.ReadModels.MbVariantUpdatingWorkItem do
end

# build_status is mapped from MbVariantUpdatingBuildCompleted - an event that is NOT this
# variant's entering event, so it is automatically reclassified into an update-only join. It can
# bring an already-active instance up to date, but it can never create one on its own.
defmodule MyApp.ReadModels.MbVariantUpdatingPullRequestItem do
  use Chronicle.ReadModels.ReadModel

  alias MyApp.Events.{MbVariantUpdatingPullRequestCreated, MbVariantUpdatingBuildCompleted}
  alias MyApp.ReadModels.MbVariantUpdatingWorkItem

  defstruct [:id, :pull_request_url, :build_status]

  variant_of MbVariantUpdatingWorkItem, key: :id
  enters_on MbVariantUpdatingPullRequestCreated

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

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

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

```typescript
import { entersOn, eventType, fromEvent, readModel, setFrom, variantOf } from '@cratis/chronicle';

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

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

class MbVariantUpdatingWorkItem {}

/**
 * buildStatus is mapped from MbVariantUpdatingBuildCompleted - an event that is NOT this variant's
 * entering event, so it is automatically reclassified into an update-only join. It can bring an
 * already-active instance up to date, but it can never create one on its own.
 */
@variantOf(MbVariantUpdatingWorkItem, 'id')
@entersOn(MbVariantUpdatingPullRequestCreated)
@fromEvent(MbVariantUpdatingPullRequestCreated)
@fromEvent(MbVariantUpdatingBuildCompleted)
@readModel()
class MbVariantUpdatingPullRequestItem {
    id = '';

    @setFrom(MbVariantUpdatingPullRequestCreated, 'pullRequestUrl')
    pullRequestUrl = '';

    @setFrom(MbVariantUpdatingBuildCompleted, 'buildStatus')
    buildStatus = '';
}
```

</TabItem>
</Tabs>

`BuildStatus` is mapped from `BuildCompleted`. Because `BuildCompleted` is not the event named with
`[EntersOn<T>]`, Chronicle reclassifies that mapping into an update-only join on the variant's own key
before it 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
`PullRequestItem` 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

A mapping that every variant needs — a title every stage keeps up to date, for example — does not have to
be repeated on each variant type. Declare it once on a type marked `[GlobalFor<TIdentity>]` instead:

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

```csharp
[EventType]
public record MbVariantSharedIssueCreated(string Title);

[EventType]
public record MbVariantSharedPullRequestCreated(string PullRequestUrl);

[EventType]
public record MbVariantSharedTitleChanged(string Title);

public class MbVariantSharedWorkItem;

[VariantOf<MbVariantSharedWorkItem>]
[EntersOn<MbVariantSharedIssueCreated>]
public record MbVariantSharedBacklogItem([property: Key] Guid Id, string Title);

[VariantOf<MbVariantSharedWorkItem>]
[EntersOn<MbVariantSharedPullRequestCreated>]
public record MbVariantSharedPullRequestItem(
    [property: Key] Guid Id,
    string Title,
    [property: SetFrom<MbVariantSharedPullRequestCreated>] string PullRequestUrl);

/// <summary>
/// Declares a mapping every variant of MbVariantSharedWorkItem shares. Every variant must have a
/// Title member - one that does not is a declaration error, not a silently skipped mapping.
/// </summary>
/// <param name="Title">The title every variant carrying one keeps up to date.</param>
[GlobalFor<MbVariantSharedWorkItem>]
public record MbVariantSharedHandlers([property: SetFrom<MbVariantSharedTitleChanged>] string Title);
```

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

```kotlin
import io.cratis.chronicle.events.EventType
import io.cratis.chronicle.projections.EntersOn
import io.cratis.chronicle.projections.FromEvent
import io.cratis.chronicle.projections.FromEventSourceId
import io.cratis.chronicle.projections.GlobalFor
import io.cratis.chronicle.projections.SetFrom
import io.cratis.chronicle.projections.VariantOf
import io.cratis.chronicle.readModels.ReadModel

@EventType
data class MbVariantSharedIssueCreated(val title: String)

@EventType
data class MbVariantSharedPullRequestCreated(val pullRequestUrl: String)

@EventType
data class MbVariantSharedTitleChanged(val title: String)

class MbVariantSharedWorkItem

@ReadModel
@VariantOf(MbVariantSharedWorkItem::class, key = "id")
@EntersOn(MbVariantSharedIssueCreated::class)
@FromEvent(MbVariantSharedIssueCreated::class)
data class MbVariantSharedBacklogItem(
    @FromEventSourceId
    val id: String = "",
    val title: String = ""
)

@ReadModel
@VariantOf(MbVariantSharedWorkItem::class, key = "id")
@EntersOn(MbVariantSharedPullRequestCreated::class)
@FromEvent(MbVariantSharedPullRequestCreated::class)
data class MbVariantSharedPullRequestItem(
    @FromEventSourceId
    val id: String = "",
    val title: String = "",

    @SetFrom("pullRequestUrl", MbVariantSharedPullRequestCreated::class)
    val pullRequestUrl: String = ""
)

/**
 * Declares a mapping every variant of MbVariantSharedWorkItem shares. Every variant must have a
 * title member - one that does not is a declaration error, not a silently skipped mapping.
 */
@GlobalFor(MbVariantSharedWorkItem::class)
@FromEvent(MbVariantSharedTitleChanged::class)
data class MbVariantSharedHandlers(
    @SetFrom("title", MbVariantSharedTitleChanged::class)
    val title: String = ""
)
```

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

```java
import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.projections.EntersOn;
import io.cratis.chronicle.projections.FromEvent;
import io.cratis.chronicle.projections.FromEventSourceId;
import io.cratis.chronicle.projections.GlobalFor;
import io.cratis.chronicle.projections.SetFrom;
import io.cratis.chronicle.projections.VariantOf;
import io.cratis.chronicle.readModels.ReadModel;

@EventType
record MbVariantSharedIssueCreated(String title) {}

@EventType
record MbVariantSharedPullRequestCreated(String pullRequestUrl) {}

@EventType
record MbVariantSharedTitleChanged(String title) {}

class MbVariantSharedWorkItem {}

@ReadModel
@VariantOf(identity = MbVariantSharedWorkItem.class, key = "id")
@EntersOn(eventType = MbVariantSharedIssueCreated.class)
@FromEvent(eventType = MbVariantSharedIssueCreated.class)
class MbVariantSharedBacklogItem {
    @FromEventSourceId
    public String id = "";
    public String title = "";
}

@ReadModel
@VariantOf(identity = MbVariantSharedWorkItem.class, key = "id")
@EntersOn(eventType = MbVariantSharedPullRequestCreated.class)
@FromEvent(eventType = MbVariantSharedPullRequestCreated.class)
class MbVariantSharedPullRequestItem {
    @FromEventSourceId
    public String id = "";
    public String title = "";

    @SetFrom(propertyPath = "pullRequestUrl", eventType = MbVariantSharedPullRequestCreated.class)
    public String pullRequestUrl = "";
}

/**
 * Declares a mapping every variant of MbVariantSharedWorkItem shares. Every variant must have a
 * title member - one that does not is a declaration error, not a silently skipped mapping.
 */
@GlobalFor(identity = MbVariantSharedWorkItem.class)
@FromEvent(eventType = MbVariantSharedTitleChanged.class)
class MbVariantSharedHandlers {
    @SetFrom(propertyPath = "title", eventType = MbVariantSharedTitleChanged.class)
    public String title = "";
}
```

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

```elixir
defmodule MyApp.Events.MbVariantSharedIssueCreated do
  use Chronicle.Events.EventType, id: "mb-variant-shared-issue-created-v1"

  defstruct [:title]
end

defmodule MyApp.Events.MbVariantSharedPullRequestCreated do
  use Chronicle.Events.EventType, id: "mb-variant-shared-pull-request-created-v1"

  defstruct [:pull_request_url]
end

defmodule MyApp.Events.MbVariantSharedTitleChanged do
  use Chronicle.Events.EventType, id: "mb-variant-shared-title-changed-v1"

  defstruct [:title]
end

defmodule MyApp.ReadModels.MbVariantSharedWorkItem do
end

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

  alias MyApp.Events.MbVariantSharedIssueCreated
  alias MyApp.ReadModels.MbVariantSharedWorkItem

  defstruct [:id, :title]

  variant_of MbVariantSharedWorkItem, key: :id
  enters_on MbVariantSharedIssueCreated

  from MbVariantSharedIssueCreated,
    set: [id: :event_source_id]
end

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

  alias MyApp.Events.MbVariantSharedPullRequestCreated
  alias MyApp.ReadModels.MbVariantSharedWorkItem

  defstruct [:id, :title, :pull_request_url]

  variant_of MbVariantSharedWorkItem, key: :id
  enters_on MbVariantSharedPullRequestCreated

  from MbVariantSharedPullRequestCreated,
    set: [id: :event_source_id, pull_request_url: :pull_request_url]
end

# Declares a mapping every variant of MbVariantSharedWorkItem shares. Every variant must have a
# title member - one that does not is a declaration error, not a silently skipped mapping. Never
# registered as a projection on its own.
defmodule MyApp.Projections.MbVariantSharedHandlers do
  use Chronicle.Projections.GlobalHandler, identity: MyApp.ReadModels.MbVariantSharedWorkItem

  alias MyApp.Events.MbVariantSharedTitleChanged

  from MbVariantSharedTitleChanged,
    set: [title: :title]
end
```

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

```typescript
import { entersOn, eventType, fromEvent, globalFor, readModel, setFrom, variantOf } from '@cratis/chronicle';

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

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

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

class MbVariantSharedWorkItem {}

@variantOf(MbVariantSharedWorkItem, 'id')
@entersOn(MbVariantSharedIssueCreated)
@fromEvent(MbVariantSharedIssueCreated)
@readModel()
class MbVariantSharedBacklogItem {
    id = '';
    title = '';
}

@variantOf(MbVariantSharedWorkItem, 'id')
@entersOn(MbVariantSharedPullRequestCreated)
@fromEvent(MbVariantSharedPullRequestCreated)
@readModel()
class MbVariantSharedPullRequestItem {
    id = '';
    title = '';

    @setFrom(MbVariantSharedPullRequestCreated, 'pullRequestUrl')
    pullRequestUrl = '';
}

/**
 * Declares a mapping every variant of MbVariantSharedWorkItem shares. Every variant must have a
 * title member - one that does not is a declaration error, not a silently skipped mapping. Never
 * registered as a projection on its own.
 */
@globalFor(MbVariantSharedWorkItem)
class MbVariantSharedHandlers {
    @setFrom(MbVariantSharedTitleChanged, 'title')
    title = '';
}
```

</TabItem>
</Tabs>

Every mapping declared on the shared handler is merged into every variant of `WorkItem`. Like any other
non-entering event, a shared mapping can only update an already-active variant — it can never create or
resurrect one, so it is safe to share across a group whose members enter at different times.

Every variant must have the member a shared mapping targets. A variant that does not is a declaration
error, not a silently skipped mapping — this is caught when the projection is discovered, not at some
unpredictable point at runtime once a shared event happens to arrive for that variant.

## Complete example

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

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

[EventType]
public record MbVariantFullIssueCreated(string Title);

[EventType]
public record MbVariantFullPullRequestCreated(string PullRequestUrl);

[EventType]
public record MbVariantFullBuildCompleted(string BuildStatus);

[EventType]
public record MbVariantFullTitleChanged(string Title);

/// <summary>
/// Anchors the logical identity shared by MbVariantFullBacklogItem and MbVariantFullPullRequestItem.
/// Deliberately not a read model itself.
/// </summary>
public class MbVariantFullWorkItem;

/// <summary>
/// The variant an entity is in before a pull request exists for it.
/// </summary>
[VariantOf<MbVariantFullWorkItem>]
[EntersOn<MbVariantFullIssueCreated>]
public record MbVariantFullBacklogItem([property: Key] Guid Id, string Title);

/// <summary>
/// The variant an entity enters once a pull request is created for it. BuildStatus is
/// mapped from MbVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it
/// becomes an update-only join and can never create the row on its own.
/// </summary>
[VariantOf<MbVariantFullWorkItem>]
[EntersOn<MbVariantFullPullRequestCreated>]
public record MbVariantFullPullRequestItem(
    [property: Key] Guid Id,
    string Title,
    [property: SetFrom<MbVariantFullPullRequestCreated>] string PullRequestUrl,
    [property: SetFrom<MbVariantFullBuildCompleted>] string BuildStatus);

/// <summary>
/// Declares a mapping every variant of MbVariantFullWorkItem shares.
/// </summary>
/// <param name="Title">The title every variant carrying one keeps up to date.</param>
[GlobalFor<MbVariantFullWorkItem>]
public record MbVariantFullSharedHandlers([property: SetFrom<MbVariantFullTitleChanged>] string Title);
```

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

```kotlin
import io.cratis.chronicle.events.EventType
import io.cratis.chronicle.projections.EntersOn
import io.cratis.chronicle.projections.FromEvent
import io.cratis.chronicle.projections.FromEventSourceId
import io.cratis.chronicle.projections.GlobalFor
import io.cratis.chronicle.projections.SetFrom
import io.cratis.chronicle.projections.VariantOf
import io.cratis.chronicle.readModels.ReadModel

@EventType
data class MbVariantFullIssueCreated(val title: String)

@EventType
data class MbVariantFullPullRequestCreated(val pullRequestUrl: String)

@EventType
data class MbVariantFullBuildCompleted(val buildStatus: String)

@EventType
data class MbVariantFullTitleChanged(val title: String)

/**
 * Anchors the logical identity shared by MbVariantFullBacklogItem and MbVariantFullPullRequestItem.
 * Deliberately not a read model itself.
 */
class MbVariantFullWorkItem

/** The variant an entity is in before a pull request exists for it. */
@ReadModel
@VariantOf(MbVariantFullWorkItem::class, key = "id")
@EntersOn(MbVariantFullIssueCreated::class)
@FromEvent(MbVariantFullIssueCreated::class)
data class MbVariantFullBacklogItem(
    @FromEventSourceId
    val id: String = "",
    val title: String = ""
)

/**
 * The variant an entity enters once a pull request is created for it. buildStatus is mapped from
 * MbVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it becomes
 * an update-only join and can never create the row on its own.
 */
@ReadModel
@VariantOf(MbVariantFullWorkItem::class, key = "id")
@EntersOn(MbVariantFullPullRequestCreated::class)
@FromEvent(MbVariantFullPullRequestCreated::class)
@FromEvent(MbVariantFullBuildCompleted::class)
data class MbVariantFullPullRequestItem(
    @FromEventSourceId
    val id: String = "",
    val title: String = "",

    @SetFrom("pullRequestUrl", MbVariantFullPullRequestCreated::class)
    val pullRequestUrl: String = "",

    @SetFrom("buildStatus", MbVariantFullBuildCompleted::class)
    val buildStatus: String = ""
)

/** Declares a mapping every variant of MbVariantFullWorkItem shares. */
@GlobalFor(MbVariantFullWorkItem::class)
@FromEvent(MbVariantFullTitleChanged::class)
data class MbVariantFullSharedHandlers(
    @SetFrom("title", MbVariantFullTitleChanged::class)
    val title: String = ""
)
```

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

```java
import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.projections.EntersOn;
import io.cratis.chronicle.projections.FromEvent;
import io.cratis.chronicle.projections.FromEventSourceId;
import io.cratis.chronicle.projections.GlobalFor;
import io.cratis.chronicle.projections.SetFrom;
import io.cratis.chronicle.projections.VariantOf;
import io.cratis.chronicle.readModels.ReadModel;

@EventType
record MbVariantFullIssueCreated(String title) {}

@EventType
record MbVariantFullPullRequestCreated(String pullRequestUrl) {}

@EventType
record MbVariantFullBuildCompleted(String buildStatus) {}

@EventType
record MbVariantFullTitleChanged(String title) {}

/**
 * Anchors the logical identity shared by MbVariantFullBacklogItem and MbVariantFullPullRequestItem.
 * Deliberately not a read model itself.
 */
class MbVariantFullWorkItem {}

/** The variant an entity is in before a pull request exists for it. */
@ReadModel
@VariantOf(identity = MbVariantFullWorkItem.class, key = "id")
@EntersOn(eventType = MbVariantFullIssueCreated.class)
@FromEvent(eventType = MbVariantFullIssueCreated.class)
class MbVariantFullBacklogItem {
    @FromEventSourceId
    public String id = "";
    public String title = "";
}

/**
 * The variant an entity enters once a pull request is created for it. buildStatus is mapped from
 * MbVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it becomes
 * an update-only join and can never create the row on its own.
 */
@ReadModel
@VariantOf(identity = MbVariantFullWorkItem.class, key = "id")
@EntersOn(eventType = MbVariantFullPullRequestCreated.class)
@FromEvent(eventType = MbVariantFullPullRequestCreated.class)
@FromEvent(eventType = MbVariantFullBuildCompleted.class)
class MbVariantFullPullRequestItem {
    @FromEventSourceId
    public String id = "";
    public String title = "";

    @SetFrom(propertyPath = "pullRequestUrl", eventType = MbVariantFullPullRequestCreated.class)
    public String pullRequestUrl = "";

    @SetFrom(propertyPath = "buildStatus", eventType = MbVariantFullBuildCompleted.class)
    public String buildStatus = "";
}

/** Declares a mapping every variant of MbVariantFullWorkItem shares. */
@GlobalFor(identity = MbVariantFullWorkItem.class)
@FromEvent(eventType = MbVariantFullTitleChanged.class)
class MbVariantFullSharedHandlers {
    @SetFrom(propertyPath = "title", eventType = MbVariantFullTitleChanged.class)
    public String title = "";
}
```

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

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

  defstruct [:title]
end

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

  defstruct [:pull_request_url]
end

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

  defstruct [:build_status]
end

defmodule MyApp.Events.MbVariantFullTitleChanged do
  use Chronicle.Events.EventType, id: "mb-variant-full-title-changed-v1"

  defstruct [:title]
end

# Anchors the logical identity shared by MbVariantFullBacklogItem and
# MbVariantFullPullRequestItem. Deliberately not a read model itself.
defmodule MyApp.ReadModels.MbVariantFullWorkItem do
end

# The variant an entity is in before a pull request exists for it.
defmodule MyApp.ReadModels.MbVariantFullBacklogItem do
  use Chronicle.ReadModels.ReadModel

  alias MyApp.Events.MbVariantFullIssueCreated
  alias MyApp.ReadModels.MbVariantFullWorkItem

  defstruct [:id, :title]

  variant_of MbVariantFullWorkItem, key: :id
  enters_on MbVariantFullIssueCreated

  from MbVariantFullIssueCreated,
    set: [id: :event_source_id]
end

# The variant an entity enters once a pull request is created for it. build_status is mapped
# from MbVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it
# becomes an update-only join and can never create the row on its own.
defmodule MyApp.ReadModels.MbVariantFullPullRequestItem do
  use Chronicle.ReadModels.ReadModel

  alias MyApp.Events.{MbVariantFullPullRequestCreated, MbVariantFullBuildCompleted}
  alias MyApp.ReadModels.MbVariantFullWorkItem

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

  variant_of MbVariantFullWorkItem, key: :id
  enters_on MbVariantFullPullRequestCreated

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

  from MbVariantFullBuildCompleted,
    set: [build_status: :build_status]
end

# Declares a mapping every variant of MbVariantFullWorkItem shares.
defmodule MyApp.Projections.MbVariantFullSharedHandlers do
  use Chronicle.Projections.GlobalHandler, identity: MyApp.ReadModels.MbVariantFullWorkItem

  alias MyApp.Events.MbVariantFullTitleChanged

  from MbVariantFullTitleChanged,
    set: [title: :title]
end
```

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

```typescript
import { entersOn, eventType, fromEvent, globalFor, readModel, setFrom, variantOf } from '@cratis/chronicle';

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

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

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

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

/**
 * Anchors the logical identity shared by MbVariantFullBacklogItem and MbVariantFullPullRequestItem.
 * Deliberately not a read model itself.
 */
class MbVariantFullWorkItem {}

/** The variant an entity is in before a pull request exists for it. */
@variantOf(MbVariantFullWorkItem, 'id')
@entersOn(MbVariantFullIssueCreated)
@fromEvent(MbVariantFullIssueCreated)
@readModel()
class MbVariantFullBacklogItem {
    id = '';
    title = '';
}

/**
 * The variant an entity enters once a pull request is created for it. buildStatus is mapped from
 * MbVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it becomes
 * an update-only join and can never create the row on its own.
 */
@variantOf(MbVariantFullWorkItem, 'id')
@entersOn(MbVariantFullPullRequestCreated)
@fromEvent(MbVariantFullPullRequestCreated)
@fromEvent(MbVariantFullBuildCompleted)
@readModel()
class MbVariantFullPullRequestItem {
    id = '';
    title = '';

    @setFrom(MbVariantFullPullRequestCreated, 'pullRequestUrl')
    pullRequestUrl = '';

    @setFrom(MbVariantFullBuildCompleted, 'buildStatus')
    buildStatus = '';
}

/** Declares a mapping every variant of MbVariantFullWorkItem shares. */
@globalFor(MbVariantFullWorkItem)
class MbVariantFullSharedHandlers {
    @setFrom(MbVariantFullTitleChanged, 'title')
    title = '';
}
```

</TabItem>
</Tabs>

## Best practices

1. **Pick an identity type that means something on its own** — `WorkItem` in the examples above, not a
   marker interface with no purpose beyond grouping. It is the type every variant, and every shared
   handler, points back to.
2. **Give every variant only the properties that stage of the entity actually has.** A shared property
   that every stage needs belongs on a `[GlobalFor<T>]` handler instead of being copy-pasted onto each
   variant.
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
   (or by its shared key) rather than assuming a single combined collection.
