Close a stream of a definition
Use this page to end one stream of a definition, such as one month of an account, and keep the rest of the event source open. The mechanism, its guarantees and its limits are the general ones in Closing streams. This page shows how to aim it at a definition’s streams.
A closure is a scope made of up to four values: event source id, event source type, event stream type and event stream id. A definition supplies the names for three of them. The closure refuses an append when every value the closure sets equals the append’s value. Closing the Transactions stream with id 2026-04 of account A refuses only that account’s April transactions.
Close with an event
Section titled “Close with an event”When a domain fact ends the stream, declare it with [ClosesStream] on the event type. The kernel writes the event and the closure together. By default the closed scope is the appended event’s event source id, stream type and stream id, so append the closing event through the definition, to the stream it ends:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;using Cratis.Chronicle.EventSequences;
[EventType]public record MoneyDeposited(decimal Amount);
// Appending this event closes the scope it is appended to: its event source id, stream type and stream id.[EventType][ClosesStream]public record MonthClosed(decimal ClosingBalance);
// AccountEventSource is the definition from the previous example.public class MonthEndWithAnEvent(IEventLog eventLog){ public async Task<bool> CloseAndTryAgain(EventSourceId accountId, string month, decimal balance) { await eventLog.Append<AccountEventSource>( accountId, new MonthClosed(balance), eventStream: "Transactions", eventStreamId: month);
var late = await eventLog.Append<AccountEventSource>( accountId, new MoneyDeposited(10m), eventStream: "Transactions", eventStreamId: month);
// The late deposit is refused. The next month's stream is unaffected. return late.ConstraintViolations.Any(violation => violation.ConstraintType == ConstraintType.StreamClosed); }}The closing event is accepted. Every later append to the same account’s 2026-04 stream is refused with a StreamClosed constraint violation. Other months and other accounts are unaffected. The declaration options, such as ReopenedBy and EventStreamIdFrom, are in Closing events.
Close with a call
Section titled “Close with a call”For an ad-hoc close that no domain event represents, build a ClosedStreamScope and call CompleteStream. Take the names from the definition so that they cannot drift from what the appends write:
using Cratis.Chronicle;using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.EventSources;
[EventSource("Account")][EventStream("Transactions")]public class AccountEventSource : IEventSource;
public class MonthEnd(IEventStore eventStore){ public async Task<bool> CloseMonth(EventSourceId accountId, string month) { // Take the names from the definition, so a rename cannot leave a closure behind with the old name. var definition = eventStore.EventSources.GetFor(typeof(AccountEventSource)); var transactions = definition.FindStream("Transactions")!;
var scope = new ClosedStreamScope( EventSourceId: accountId, EventSourceType: definition.EventSourceType, EventStreamType: transactions.EventStreamType, EventStreamId: month);
var closed = false; var result = await eventStore.EventLog.CompleteStream(scope); result.Switch( _ => closed = true, error => closed = error == CompleteStreamError.AlreadyCompleted); return closed; }}EventSourceType here is the definition’s name. Leave it out to close the stream for every event source type that uses the same stream type and stream id. Add expectedTailSequenceNumber to close only if nothing was appended since the last fact you know of, as described in Scoped closing.
Things to know
Section titled “Things to know”- There is no typed
CompleteStream<TSource>. You build the scope from the definition’s values. - Closing a stream does not touch the definition. A closure is stored by value, so renaming a stream later leaves the old closure on the old name. See Questions and pitfalls.
- A scope that is only the default stream, with no event source id, is refused with
DefaultStreamCannotBeCompleted. Appends without a stream go to the default stream, so close the whole event source by id if you want those closed.
Next steps
Section titled “Next steps”- Closing streams
- Scoped closing
- Scenarios: a process that closes its own stream.
- Dynamic Consistency Boundary: a closed stream as a boundary that ends. See also Dynamic Consistency Boundary in Chronicle.