---
title: Close the books
editUrl: https://github.com/Cratis/Chronicle/edit/main/Documentation/scenarios/close-the-books.mdx
description: End a period, version or batch with a domain fact, then continue in the next stream without a rollover race.
---

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


**Goal:** a ledger, a monthly report or a billing run works in periods. When a period is finished, nothing more may be appended to it, and new facts must go to the next period.

This recipe uses closing streams and gives the kernel no rollover policy. You decide what the next stream is called. The kernel guarantees that the old one stays closed.

## The shape

1. The closing fact (or a carry-over fact) ends the old scope. It is an event type declared with `[ClosesStream]`, so the append and the closure happen in the same step.
2. The application derives the next stream id. It can come from the period itself (`2026-05`), from a counter, or from a read model that tracks the current open instance.
3. The first fact in the new stream carries over what the old one ended with, for example the closing balance.

## Close the period and open the next

The closing event takes the stream id from its `Period` property, so it closes the period it names whichever stream it is appended to:

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

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

[EventType]
[ClosesStream(EventStreamIdFrom = nameof(Period))]
public record CloseTheBooksBooksClosed(string Period, decimal ClosingBalance);

[EventType]
public record CloseTheBooksPeriodOpened(string Period, decimal OpeningBalance);

public class CloseTheBooksLedger(IEventLog eventLog)
{
    static readonly EventStreamType _ledger = new("ledger");

    public async Task<AppendResult> CloseApril(EventSourceId ledgerId, decimal closingBalance)
    {
        // The closing fact closes (ledger, "ledger", "2026-04") in the same step as the append.
        var closing = await eventLog.Append(
            ledgerId,
            new CloseTheBooksBooksClosed("2026-04", closingBalance),
            _ledger,
            new EventStreamId("2026-04"));
        if (!closing.IsSuccess) return closing;

        // The application chooses the next stream id and records the carry-over as its first fact.
        return await eventLog.Append(
            ledgerId,
            new CloseTheBooksPeriodOpened("2026-05", closingBalance),
            _ledger,
            new EventStreamId("2026-05"));
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/scenarios/close-the-books/close-and-open-next.md)

</TabItem>
</Tabs>

After the first append succeeds, every append to `ledger` / `2026-04` for that ledger is refused with `StreamClosed`. Other ledgers and the `2026-05` stream are unaffected. The two appends are separate calls. If the process stops between them, the old period is closed and the new one is not open yet. Make the second step safe to repeat: before opening the new period, check whether it already has its opening fact.

## Keep appending to the right period

Producers that post entries need the current period. Take it from a read model of the open instance rather than from the clock, so a late-closing period keeps accepting entries until it is actually closed.

A read model lags the log slightly. Right after a rollover, a producer can still read the old period and append to it. The kernel then rejects the append with `StreamClosed`. Treat that as a normal outcome: read the current period again and retry.

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

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

[EventType]
public record CloseTheBooksEntryPosted(string Period, decimal Amount);

public interface ICloseTheBooksCurrentPeriod
{
    Task<string> For(EventSourceId ledgerId);
}

public class CloseTheBooksPosting(IEventLog eventLog, ICloseTheBooksCurrentPeriod currentPeriod)
{
    public async Task<AppendResult> Post(EventSourceId ledgerId, decimal amount)
    {
        AppendResult result;
        var attempts = 0;

        do
        {
            // The current period comes from a read model of the open instance.
            var period = await currentPeriod.For(ledgerId);
            result = await eventLog.Append(
                ledgerId,
                new CloseTheBooksEntryPosted(period, amount),
                new EventStreamType("ledger"),
                new EventStreamId(period));
            attempts++;
        }
        while (IsClosed(result) && attempts < 3);

        return result;
    }

    static bool IsClosed(AppendResult result) =>
        result.ConstraintViolations.Any(violation => violation.ConstraintType == ConstraintType.StreamClosed);
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/scenarios/close-the-books/retry-on-closed.md)

</TabItem>
</Tabs>

The rejection is the safety net. Without the closure the late entry would be committed to a period that is already final.

## Allow a correction

If a closed period sometimes needs a correction, declare a reopening event with `ReopenedBy`. It is a fact in the log with its own reason, and it lifts only the closure that the closing event wrote. See [Closing events](/chronicle/closing-streams/closing-events/#reopen-with-an-event). A period that an operator closed manually with `CompleteStream` stays closed.

## Carry-over and mementos

Today the carry-over is an ordinary event, as in the first example. A memento, a snapshot of the old stream's state written as the first event of the new one, is planned and tracked in [Chronicle issue 1370](https://github.com/Cratis/Chronicle/issues/1370). It will replace the hand-written carry-over fact, not the closing step.

## See also

- [Closing events](/chronicle/closing-streams/closing-events/): dimensions, `EventStreamIdFrom`, reopening.
- [Closing streams](/chronicle/closing-streams/): the guarantees and limits.
- [Enforce a unique value](/chronicle/scenarios/enforce-a-unique-value/): the other constraint recipe.
