---
title: Understanding constraints
editUrl: https://github.com/Cratis/Chronicle/edit/main/Documentation/understanding-constraints.mdx
description: How Chronicle enforces uniqueness without a unique index — rules you declare on the event with [Unique] or in a dedicated IConstraint class, checked in the kernel before anything is committed.
---

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


One question stops a lot of teams at the door of event sourcing, and it's a good one: **"If state is derived from events, how do I stop two users registering the same email?"** There's no table to put a `UNIQUE` index on.

Chronicle's answer is **constraints** — rules you declare next to your events, enforced in the **kernel** before an event is committed. This page is the mental model: why the check has to live server-side, the two ways to declare a rule, and how a violation surfaces. The exhaustive options live in the [Constraints reference](/chronicle/constraints/).

## Why the check belongs in the kernel

The worry is real: in a classic CRUD app you lean on a database `UNIQUE` constraint, but here the "current state" is a projection, built *after* the fact. By the time a read model notices a duplicate email, the duplicate event is already in the log. Checking the read model *before* appending doesn't save you either — two requests can both pass the check and both append.

Chronicle closes that gap by checking the rule **before the event is committed**, inside the kernel:

```mermaid
flowchart LR
    A["Append UserRegistered"] --> C{"Kernel checks UniqueEmail"}
    C -->|new value| K[(Committed)]
    C -->|already used| R["Append rejected"]
```

An append that would introduce a duplicate is *rejected at the source* — it never reaches the log. Because the check lives in the kernel, it's consistent across every client and every entry point: a .NET client, a REST call, or a future integration you haven't written yet. No constraint logic runs in your application code.

## Declare it on the event

The simplest way to declare a rule is right where the value lives — adorn the event property with `[Unique]`:

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

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

[EventType]
public record UcUserRegistered([property: Unique(name: "UniqueEmail")] string Email, string DisplayName);
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/understanding-constraints/property-attribute.md)

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

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/understanding-constraints/property-attribute.md)

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

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/understanding-constraints/property-attribute.md)

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

```elixir
defmodule MyApp.Events.UcUserRegistered do
  use Chronicle.Events.EventType, id: "uc-user-registered"

  defstruct [:email, :display_name]

  unique(:email, name: "UniqueEmail")
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/understanding-constraints/property-attribute.md)

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

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

@eventType('understanding-constraints-user-registered')
class UcUserRegistered {
    @unique('UniqueEmail') email = '';
    displayName = '';
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/understanding-constraints/property-attribute.md)

</TabItem>
</Tabs>

That one attribute is the whole client-side story. When your application starts, Chronicle scans for `[Unique]`-adorned types, registers the constraint with the kernel, and the kernel builds the indexes it needs to enforce it on every subsequent append.

A few things make this practical:

- **One rule can span several events.** Give the same `name` to `[Unique]` on `UserRegistered.Email` and `UserEmailChanged.NewEmail`, and a changed email is checked against registered ones too — the rule follows the *value*, not a single event type.
- **Some events are one-of-a-kind.** Put `[Unique]` on the event *type* instead of a property, and only one event of that type can ever be appended per event source — registering the same user twice becomes impossible by declaration:

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

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

[EventType]
[Unique]
public record UcUserRegisteredOnce(string Email, string DisplayName);
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/understanding-constraints/event-type-attribute.md)

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

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/understanding-constraints/event-type-attribute.md)

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

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/understanding-constraints/event-type-attribute.md)

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

```elixir
defmodule MyApp.Events.UcUserRegisteredOnce do
  use Chronicle.Events.EventType, id: "uc-user-registered-once"

  defstruct [:email, :display_name]

  unique_event_type()
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/understanding-constraints/event-type-attribute.md)

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

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

@eventType('understanding-constraints-user-registered-once')
@unique()
class UcUserRegisteredOnce {
    email = '';
    displayName = '';
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/understanding-constraints/event-type-attribute.md)

</TabItem>
</Tabs>

- **Values can be released.** When the thing that held the value goes away, mark that event with `[RemoveConstraint]` so the value can be claimed again. A lifecycle that ends in more than one way marks each of those events — every one of them releases the value:

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

```csharp
using Cratis.Concepts;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Events.Constraints;

public record UcUserId(Guid Value) : ConceptAs<Guid>(Value);

[EventType]
[RemoveConstraint("UniqueEmail")]
public record UcUserRemoved(UcUserId UserId);
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/understanding-constraints/remove-constraint.md)

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

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/understanding-constraints/remove-constraint.md)

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

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/understanding-constraints/remove-constraint.md)

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

```elixir
defmodule MyApp.Events.UcUserRemoved do
  use Chronicle.Events.EventType, id: "uc-user-removed"

  defstruct [:user_id]

  remove_constraint("UniqueEmail")
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/understanding-constraints/remove-constraint.md)

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

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

@eventType('understanding-constraints-user-removed')
@removeConstraint('UniqueEmail')
class UcUserRemoved {
    userId = '';
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/understanding-constraints/remove-constraint.md)

</TabItem>
</Tabs>

The reference calls this attribute style [model-bound constraints](/chronicle/constraints/model-bound/) — every parameter and combination is documented there.

## When the rule outgrows an attribute

Attributes shine when the rule belongs to one event type. Some rules don't: the same logical value appears under *different property names* in different events, the comparison should ignore casing, or the violation message needs to be composed from context. For those, declare the rule in a dedicated class — implement `IConstraint` and describe it with the builder:

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

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

[EventType]
public record UcDedicatedUserRegistered(string Email, string DisplayName);

[EventType]
public record UcDedicatedUserEmailChanged(string NewEmail);

[EventType]
public record UcDedicatedUserRemoved;

public class UcDedicatedUniqueEmail : IConstraint
{
    public void Define(IConstraintBuilder builder) =>
        builder.Unique(unique =>
            unique
                .WithName("UniqueEmail")
                .On<UcDedicatedUserRegistered>(e => e.Email)
                .On<UcDedicatedUserEmailChanged>(e => e.NewEmail)
                .IgnoreCasing()
                .RemovedWith<UcDedicatedUserRemoved>()
                .WithMessage("That email address is already in use."));
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/understanding-constraints/dedicated-class.md)

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

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/understanding-constraints/dedicated-class.md)

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

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/understanding-constraints/dedicated-class.md)

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

```elixir
defmodule MyApp.Events.UcDedicatedUserRegistered do
  use Chronicle.Events.EventType, id: "uc-dedicated-user-registered"

  defstruct [:email, :display_name]

  unique(:email,
    name: "UcDedicatedUniqueEmail",
    ignore_casing: true,
    message: "That email address is already in use."
  )
end

defmodule MyApp.Events.UcDedicatedUserEmailChanged do
  use Chronicle.Events.EventType, id: "uc-dedicated-user-email-changed"

  defstruct [:new_email]

  unique(:new_email,
    name: "UcDedicatedUniqueEmail",
    ignore_casing: true,
    message: "That email address is already in use."
  )
end

defmodule MyApp.Events.UcDedicatedUserRemoved do
  use Chronicle.Events.EventType, id: "uc-dedicated-user-removed"

  defstruct []

  remove_constraint("UcDedicatedUniqueEmail")
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/understanding-constraints/dedicated-class.md)

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

```typescript
import { constraint, eventType, IConstraint, IConstraintBuilder } from '@cratis/chronicle';
import { field } from '@cratis/fundamentals';

@eventType()
class UcDedicatedUserRegistered {
    @field(String) readonly email: string;
    @field(String) readonly displayName: string;

    constructor(email: string, displayName: string) {
        this.email = email;
        this.displayName = displayName;
    }
}

@eventType()
class UcDedicatedUserEmailChanged {
    @field(String) readonly newEmail: string;

    constructor(newEmail: string) {
        this.newEmail = newEmail;
    }
}

@eventType()
class UcDedicatedUserRemoved {
}

@constraint('UniqueEmail')
class UcDedicatedUniqueEmail implements IConstraint {
    define(builder: IConstraintBuilder): void {
        builder.unique(unique =>
            unique
                .on(UcDedicatedUserRegistered, e => e.email)
                .on(UcDedicatedUserEmailChanged, e => e.newEmail)
                .ignoreCasing()
                .removedWith(UcDedicatedUserRemoved)
                .withMessage('That email address is already in use.'));
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/understanding-constraints/dedicated-class.md)

</TabItem>
</Tabs>

Read it top to bottom and it says exactly what the rule is: one named constraint, fed by two events with differently named properties, case-insensitive, released when the user is removed, with a friendly message when it fires. The one-of-a-kind variant has a builder form too — `builder.Unique<UserRegistered>()` enforces a single `UserRegistered` per event source.

Like the attributes, `IConstraint` implementations are **discovered automatically** when the client starts — no registration call. And crucially, both styles compile down to the *same* kernel-side constraint; which one you pick is purely about where the rule reads best. The full builder surface — names, messages, message callbacks, removal — is in the [declarative constraints reference](/chronicle/constraints/declarative/).

## Constraints that close streams

Uniqueness is not the only rule the kernel can hold for you. A **closing constraint** declares that appending a particular event type ends a stream scope: the event is committed, the scope is closed in the same step, and every later append inside it is rejected with a `StreamClosed` violation. A fact such as `BooksClosed` can then end a period without a second call that another append could slip in front of. See [Closing events](/chronicle/closing-streams/closing-events/) and, for the attribute and builder forms, the [model-bound](/chronicle/constraints/model-bound/closing-stream/) and [declarative](/chronicle/constraints/declarative/closing-stream/) references.

## What a violation looks like

A violated constraint surfaces as a **failed append** — the result tells you which constraint fired and carries the message you declared. A command turns that into a friendly "that email's taken" on the form, instead of a 500. The step-by-step recipe, including handling the failure, is [Enforce a unique value](/chronicle/scenarios/enforce-a-unique-value/).

## Privacy and PII protection

Chronicle protects sensitive information in constraint indexes by **hashing all constraint values** before storage. When you declare a unique constraint on an email address, phone number, or other potentially sensitive data, Chronicle:

1. Concatenates the property values (if the constraint spans multiple properties).
2. Normalizes the value (lowercases it if `IgnoreCasing()` is enabled).
3. Computes a **SHA-256 hash** of the result.
4. Stores only the hash in the constraint index, never the raw value.

This means the kernel can still enforce uniqueness — two identical values produce the same hash — but the constraint index itself contains no PII. The actual property values remain visible only in the events themselves, where Chronicle's encryption and compliance features already protect them.

**Note:** The hash transformation is transparent to your code — you declare constraints on plain properties, and storage uses hashes under the hood. The same instinct shapes what a violation says: Chronicle's own message names the constraint and the offending *property*, never the value it collided on. A protected value is therefore never echoed back by a message Chronicle composed — and the caller doesn't need it echoed, because it is the value it just tried to append.

## Constraints and concurrency

Constraints are one half of data integrity; the other half is **concurrency**. They guard against different things:

- A **constraint** protects a *value* across every event source in an event sequence — no two users in the event log may ever hold the same email, no matter who appends or when. The check is scoped to one namespace and one event sequence: two tenants in different namespaces can hold the same value, and so can two different event sequences.
- A **concurrency scope** protects a *moment* — when two requests race to append to the same event source, the one that acted on stale state is rejected instead of silently interleaving.
- A **decision read** protects a *rule over state*: "at most three loans per member" or "no more than the seats available". It folds the state, notes the log position before it does, and rejects the append if a relevant event arrived since. A constraint cannot count, so this is the tool for those rules.

All three checks run in the kernel at append time, and all surface as a failed append you can handle. When you find yourself worried about "two requests at once" rather than "the same value twice," reach for [concurrency scopes](/chronicle/events/concurrency/) or [decision reads](/chronicle/read-models/decision-reads/). [Choosing how a rule holds](/chronicle/dynamic-consistency-boundary/choosing/) compares them.

## The pattern to take with you

Notice the shape of the answer: the hard part lives in the **kernel**, is declared close to the **event**, and applies to **every** client with no coordination. That's the same principle as the rest of Chronicle — the event log is the source of truth, and the rules that protect it live *with* it, not scattered through application code. The same shape answers the other question that makes people nervous about event sourcing — changing an event's shape after it's written — in [Understanding event evolution](/chronicle/understanding-event-evolution/).
