Skip to content

Factories

A factory is a named way to create an aggregate. Instead of handing callers a raw constructor, you declare a create method that states its preconditions, fills the fields it cares about, and records a creation event. Koine emits it as a public static method on the entity — and the moment any factory exists, the all-args constructor becomes private, so the factory is the only legal door in.

This is the Domain-Driven Design “Factory” pattern: the aggregate is always born valid, named after the business action that brought it into being (Order.Open(...), not new Order(...)).

A factory is declared inside an entity block — after the entity’s members, invariants, states, and command declarations. It belongs to the entity, not to aggregate: a factory is an entity member, never a direct child of aggregate. By default the host entity needs a generatable identity — a Guid-backed id (the default) — because the factory mints that identity for you with IdType.New() (C#) / IdType::generate() (Rust). A natural(String), natural(Int), or sequence key has no meaningful client-side generator (a natural key is caller-supplied; a sequence key is store-assigned), so a create factory on such an entity is allowed only when it takes the identity as an explicit identity-typed parameter (e.g. create register(id: BookId, …)) — the parameter then supplies the id and nothing is minted; otherwise it is rejected at compile time (see §12.3.5).

Most factories sit on an aggregate root (as below), but a standalone, context-level entity may host one too. The one wrinkle is emit: a creation event can only be raised from a standalone entity or an aggregate root — a non-root nested entity may still declare a factory, but it cannot emit.

A factory is introduced with the create keyword followed by a name and an optional parameter list:

factoryDecl
: 'create' Identifier ( '(' paramList? ')' )? '{' factoryStmt* '}'
;
factoryStmt
: requiresClause
| initialization
| emitClause
;
requiresClause
: 'requires' expression StringLiteral?
;
initialization
: softName '->' expression
;
emitClause
: 'emit' Identifier ( '(' emitArgList? ')' )?
;
emitArgList
: emitArg ( ',' emitArg )*
;
emitArg
: softName ':' expression
;

The body of a factory is a fixed sequence of clauses: zero or more requires guards (§12.3.2), zero or more field initializations (field -> expr) (§12.5), and zero or more emit clauses (§12.6).

The name Identifier is PascalCased in the emitted C# static method. The parameter list follows the same softName ':' type_ref convention as commands (see Commands, events & state (§11)). The expression grammar used in requiresClause and initialization is specified in Expressions (§9).

context Sales {
value OrderLine { product: ProductId quantity: Int }
enum OrderStatus { Draft, Placed, Shipped, Cancelled }
aggregate Order root Order {
entity Order identified by OrderId {
customer: CustomerId
lines: List<OrderLine>
status: OrderStatus = Draft
create forCustomer(customer: CustomerId, lines: List<OrderLine>) {
requires !lines.isEmpty "cannot open an empty order"
emit OrderOpened(orderId: id, customer: customer, lineCount: lines.count)
}
}
event OrderOpened {
orderId: OrderId
customer: CustomerId
lineCount: Int
}
}
}

The factory parameters and a synthetic id are the only names in scope inside the factory body. Entity members are not in scope — the aggregate does not exist yet, so there is no this to read from.

Each requires <expr> "msg" clause is a precondition. Guards are checked before construction and throw DomainInvariantViolationException if the expression evaluates to false. The optional StringLiteral becomes the rule message on the exception. Multiple guards are evaluated in declaration order.

The precedence Koine uses to fill each field, highest to lowest:

  1. An explicit field -> expr initialization.
  2. A same-named, same-typed parameter (auto-bind; see §12.4.1).
  3. A field default (= value) or an optional T?.
  4. default! for a required field the factory never set — which also raises an UninitializedFactoryField warning (non-fatal; it still compiles).

So a required field is satisfied by either an auto-binding parameter or an explicit field -> expr. If you see the warning, you have a required field with no value flowing into it.

With no factory, the all-args constructor is public and callers can new the entity directly:

public Order(OrderId id, CustomerId customer, IReadOnlyList<OrderLine> lines, ...)

The instant you declare any create, that same constructor is emitted private:

private Order(OrderId id, CustomerId customer, IReadOnlyList<OrderLine> lines, OrderStatus? status = null)

This is automatic — there is no extra keyword. The toggle is purely the presence or absence of create declarations on the entity. Adding one factory closes the door for everyone; external code must now go through Order.Open(...).

  • id is reserved — unless it is a non-Guid identity. A factory parameter named id is allowed only when its type is the entity’s identity type and that identity is non-Guid (create register(id: BookId, …) on a natural/sequence key): it then supplies the explicit identity and binds to the synthetic id local. On a Guid identity the factory still mints var id = <Id>.New();, so any parameter named id — even of the identity type — collides with that local and is rejected; and a parameter named id whose type is not the identity type is rejected everywhere. (Binding is by parameter type, not the literal name, so the explicit-id parameter may be named anything: id, bookId, no, …)
  • The identity must be generatable — or passed in explicitly. A factory auto-generates the new aggregate’s identity, but only the default Guid-backed id has a meaningful generator (IdType.New() / IdType::generate()). A create factory on an entity whose identity is as natural(String), as natural(Int), or as sequence is therefore allowed only when it accepts the identity as an explicit parameter of the identity type (create register(id: BookId, …)) — no generator is emitted, the parameter is threaded as the id. KOI0808 fires only when such a non-Guid factory provides no identity-typed parameter (the key would have to be minted client-side, which it cannot be), and declaring more than one is ambiguous and is its own error (KOI0809). A Guid factory always mints, so it is unaffected by both rules: a parameter of its own identity type is an ordinary reference (e.g. reply(parent: CommentId, …)), never the new id.
  • The factory name emits a public static method, so it must not collide with a field, a property, a command name, or a synthesized member like getHashCode. Two factories with the same name is a duplicate-factory error.
  • Placement is strict. Factories come after members, invariants, states, and commands inside the entity block — and never directly under aggregate.
  • create is a soft keyword. It is still legal as a field name (e.g. create: Int in a value). The same holds for requires and emit.
  • Empty bodies are fine. create make { } parses and compiles; unset fields fall back to defaults (and required ones trigger the UninitializedFactoryField warning).

The factory compiles to a public static method with a fixed body order: generate the identity, run the guards, construct via the private constructor with named arguments, record events, return.

create forCustomer(customer: CustomerId, lines: List<OrderLine>) {
requires !lines.isEmpty "cannot open an empty order"
emit OrderOpened(orderId: id, customer: customer, lineCount: lines.count)
}

emits:

public static Order Open(CustomerId customer, IReadOnlyList<OrderLine> lines)
{
var id = OrderId.New();
if (!(!(lines.Count == 0)))
throw new DomainInvariantViolationException(
type: nameof(Order),
rule: "cannot open an empty order");
var instance = new Order(id, customer: customer, lines: lines);
instance._domainEvents.Add(new OrderOpened(id, customer, lines.Count));
return instance;
}

Notice three things:

  • The identity is generated for you — by default. On the default Guid path, var id = OrderId.New(); is the first statement and the caller never passes an id. When the factory instead declares an explicit identity-typed parameter, that parameter binds to id and no New() is emitted (see the explicit-id example below, and §12.3.5).
  • Construction uses named arguments. Each field is passed positionally-by-name, so the order of declaration in the factory does not have to match the constructor.
  • Events are recorded after construction, qualified on the freshly built instance.

For a natural(String), natural(Int), or sequence key, the factory accepts the identity as an explicit parameter of the identity type — the parameter is the id, so no generator runs:

context Catalog {
entity Book identified by BookId as natural(String) {
isbn: Isbn
title: String
create register(id: BookId, isbn: Isbn, title: String) {
isbn -> isbn
title -> title
}
}
}

emits — note there is no BookId.New(); the id parameter is threaded straight through:

public static Book Register(BookId id, Isbn isbn, string title)
{
var instance = new Book(id, isbn: isbn, title: title);
return instance;
}

You do not always need an explicit ->. If a factory parameter’s name and type match a field, it auto-binds to that field. In the Open example, customer and lines are passed straight through (customer: customer, lines: lines) without a single line of initialization.

create forCustomer(customer: CustomerId, lines: List<OrderLine>) {
emit OrderOpened(orderId: id, customer: customer, lineCount: lines.count)
}

Use field -> expr to set a constructor field from an expression. Each initialization becomes a named constructor argument (field: <expr>).

context C {
entity E identified by EId {
n: Int
create make(v: Int) {
requires v > 0 "positive"
n -> v
}
}
}

emits:

public static E Make(int v)
{
var id = EId.New();
if (!(v > 0))
throw new DomainInvariantViolationException(type: nameof(E), rule: "positive");
var instance = new E(id, n: v);
return instance;
}

A few rules for init targets:

  • The target must be a real, constructor-settable field. Initializing an unknown field is an error.
  • A derived field (one with a computed default, e.g. doubled: Int = n + n) cannot be initialized — it is recomputed from other fields, not stored. See Value objects (§5) for derived members.
  • The RHS type must match the field’s type, or you get a type-mismatch error (e.g. n -> "x" for an Int).
  • Initializing the same field twice is a duplicate-initialization error.

12.5.1 Folding over a parameter collection

Section titled “12.5.1 Folding over a parameter collection”

Because the parameter’s declared type tells the translator the element type, you can aggregate a collection of value objects directly in an initialization:

context Sales {
value Money { amount: Decimal }
value Line { price: Money }
aggregate Cart root Cart {
entity Cart identified by CartId {
total: Money
create forLines(lines: List<Line>) {
total -> lines.sum(l => l.price)
}
}
}
}

When the selector returns a value object, .sum(...) folds with the VO’s + operator rather than a numeric sum:

total: lines.Select(l => l.Price).Aggregate((a, b) => a + b)

emit Evt(arg: expr, ...) records a domain event after the instance is constructed. The event type must be declared as a sibling member of the aggregate, and the payload may reference id and the factory parameters.

emit OrderOpened(orderId: id, customer: customer, lineCount: lines.count)

Event field names become PascalCase C# properties, and emitting any event is what triggers Koine to generate the DomainEvents infrastructure on the root. See Aggregates (§7) for how DomainEvents is collected and cleared.