Skip to content

Contexts & types

A .koi file is a list of bounded context blocks. Inside a context you declare the types of your domain — value objects, entities, aggregates, enums, events, services, read models, and more. This page is the ground-floor reference: the context block itself, the full menu of type families, and the three field forms (name: Type, defaults, derived members) that appear in every body.

For the constructs that build on top of these — factories, commands, specs, repositories, the context map — see the dedicated reference pages linked throughout.

A context is a bounded context: one ubiquitous language, one namespace. Every declaration in a .koi file lives inside a context, and contexts are the primary unit of compilation. The compiler turns each context Name { … } into exactly one C# namespace of the same name, with each declared type emitted into its own file under a Name/ folder.

A context name carries no equality or identity of its own — it is purely a grouping and a namespace. The same context name may appear in several .koi files; in a directory build they merge into one namespace, so a context split across files needs no imports for its own types. See Multi-file & modules (§16).

program
: program_member* EOF
;
program_member
: context_decl
| context_map_decl
;
contextDecl
: 'context' Identifier ( 'version' IntLiteral )? '{' contextMember* '}'
;
contextMember
: importDecl
| moduleDecl
| typeDecl
| specDecl
| serviceDecl
| policyDecl
| readmodelDecl
| queryDecl
| publishDecl
| subscribeDecl
;
moduleDecl
: 'module' Identifier '{' moduleMember* '}'
;
moduleMember
: typeDecl
| moduleDecl
;
typeDecl
: valueDecl
| quantityDecl
| entityDecl
| aggregateDecl
| enumDecl
| eventDecl
| integrationEventDecl
;
member
: softName ':' type_ref ( '=' expression )?
;
type_ref
: ( typeName '.' )? typeName ( '<' type_ref ( ',' type_ref )? '>' )? '?'?
;

The context keyword opens a bounded context. Everything between the braces is a contextMember — imports, modules, type declarations, specs, services, policies, read models, queries, and publish/subscribe wiring. A minimal context is:

context Ordering {
value Money { amount: Decimal currency: Currency }
enum Currency { EUR, USD, GBP }
}

Each context Name { … } compiles to exactly one C# namespace of the same name, with each declared type emitted into its own file under a Name/ folder (Ordering/Money.cs, Ordering/Currency.cs, …).

A context may carry an optional version N between its name and the {:

context Ordering version 1 {
// …
}

The version is metadata only — it does not change the emitted C# at all (a versioned context emits byte-identical code to an unversioned one). It surfaces in the generated glossary heading and feeds the @since ceiling check (see Versioning (§18)). The literal is a bare integer; omit the clause entirely for an unversioned context.

Everything you declare inside a context is one of these families. The table is the quick map; each row links to a fuller treatment.

DeclarationEmitsReference
value X { … }sealed class — get-only props, validating constructor, value equalityValue objects (§5)
quantity X { … }a value object with a Decimal amount + enum unit and unit-checked + - * /Value objects (§5)
entity X identified by XId { … }sealed class with identity-only equality + a generated XId value objectEntities & identity (§6)
aggregate A root R { … }nested types; root R implements IAggregateRoot; an I<R>Repository contractAggregates (§7)
enum E { … }a self-contained smart enum (sealed class, All/FromName/FromValue, value equality)Enums (§8)
event E { … }a domain-event record recorded into the aggregate’s DomainEventsCommands, events & state (§11)
integration event E { … }sealed record : IIntegrationEvent — a published, cross-boundary contractContext maps & integration (§17)
readmodel M from Src { … }a flat, value-equal DTO record + a static ToM(this Src) projection mapperApplication & CQRS (§15)
query Q(criteria): List<M>a query DTO record handled via the shared generic IQueryHandler<TQuery,TResult>Application & CQRS (§15)
service S { … }an application interface IS (use cases) and/or a domain class S (operations)Specs, services & policies (§13)
spec Name on T = expra reusable named boolean predicate over T, referenceable by nameSpecs, services & policies (§13)
policy Name when E then T.cmd(…)a handler interface + an abstract seam for a cross-aggregate reactionSpecs, services & policies (§13)
module M { … }groups types into a <Context>.<Module> sub-namespace and folderMulti-file & modules (§16)
import Ctx.{ T } / import Ctx.*a precise cross-context reference, emitting a tidy usingMulti-file & modules (§16)

A handful of these are context-level only (service, readmodel, query, spec, policy, import, module, integration event, the context-map publishes/subscribes). The tactical families — value, quantity, entity, enum, event — also live happily inside an aggregate or module body.

A type body is a whitespace-separated list of members. (Commas between members are optional and conventionally omitted everywhere except enum member lists.) A member takes the form:

member
: softName ':' type_ref ( '=' expression )?
;

The = expression part is optional. When present, it is either a default (a literal or bare enum member — no reference to sibling fields) or a derived field (the expression references other members of the same type). See §4.3 for the exact distinction.

A type_ref names a built-in primitive, a user-defined type, a generic collection, or a nullable variant:

type_ref
: ( typeName '.' )? typeName ( '<' type_ref ( ',' type_ref )? '>' )? '?'?
;

The workhorse form name: Type declares a typed, get-only property plus a matching constructor parameter.

value Money {
amount: Decimal
currency: Currency
}

A trailing ? makes the field optional (a nullable type in C#); List<T> / Set<T> collect:

entity Product identified by ProductCode as natural(String) {
name: String
description: String? // optional
tags: Set<String> // a uniqueness set
}

A constant after = becomes a constructor parameter with a default value:

entity Order identified by OrderId {
status: OrderStatus = Draft // default value
}

The right-hand side is a literal or a bare enum member. (An enum default is emitted as a nullable parameter coalesced to the smart-enum instance, since a smart-enum value isn’t a compile-time constant.)

When the = right-hand side references other members of the same type, the field is a derived (computed) get-only property — it is not a constructor parameter:

value OrderLine {
product: ProductId
quantity: Int
unitPrice: Money
lineTotal: Money = unitPrice * quantity // derived
payable: Money = if quantity >= 10 then lineTotal * 0.9 else lineTotal
}

Derived members use the expression sublanguage (§9): arithmetic, comparisons, &&/||/!, member access (name.trim, lines.count), coalescing (description ?? name), presence checks (sale.isPresent), collection ops (lines.sum(l => l.payable)), and the if … then … else … form. They may reference plain fields and other derived fields.

A type body may also carry invariant guards, which become constructor checks that throw DomainInvariantViolationException:

value Price {
amount: Decimal
currency: Currency
invariant amount >= 0 "a price cannot be negative"
}
value Sku {
code: String
invariant code matches /^[A-Z]{3}-[0-9]{4}$/ "SKU must look like ABC-1234"
}

invariant and matches are the two fully reserved keywords — unlike the type-family keywords, they can never be used as field names. The full invariant repertoire (regex, when conditions, named specs) is covered under Value objects (§5), Invariants (§10), and Specs, services & policies (§13).

Koine has two comment forms:

// A line comment — ignored entirely.
/// A doc comment — captured and surfaced in the generated glossary
/// and carried through as a /// XML summary on the emitted member.
value Money {
amount: Decimal
/// How many minor units; never negative.
decimals: Int
}

// comments are dropped. /// doc comments attach to the declaration or member that follows and flow into both the glossary and the emitted C# <summary> documentation.

Most Koine keywords are soft: they are only keywords in the position where they introduce a declaration, and remain usable as ordinary field names everywhere else. So this is valid:

context Inventory {
value Tag {
version: Int
since: Int
deprecated: String
quantity: Int
service: Int
query: Int
}
}

Soft keywords include context, value, quantity, entity, aggregate, enum, event, by, root, command, create, spec, on, service, operation, usecase, policy, as, natural, sequence, guid, versioned, repository, operations, find, readmodel, from, query, import, module, when, then, publishes, subscribes, integration, acl, version, since, deprecated, and more.

The exceptions:

  • invariant and matches are reserved — never field names.
  • List, Set, Map, Range are reserved type names (built-in generics).
  • A handful of hard-Identifier positions — a type / command / state / enum-member name — must be plain identifiers and cannot reuse a declaration keyword.
  • A versioned aggregate root cannot have a member literally named version (it collides with the synthesized Version property).

Each context Name { … } compiles to exactly one C# namespace Name, with each declared type emitted into its own .cs file under a Name/ output folder (Ordering/Money.cs, Ordering/Currency.cs, …). A directory build that finds the same context name in multiple files merges them all into that single namespace.

The built-in scalar and collection types map to C# as follows:

KoineC#Notes
Stringstring
Intint
Decimaldecimalmoney / quantities
Boolbool
InstantDateTimeOffset
List<T>IReadOnlyList<T>defensively copied in the constructor
Set<T>IReadOnlySet<T>defensively copied in the constructor
Map<K,V>IReadOnlyDictionary<K,V>defensively copied in the constructor
Range<T>generated Range<T> value objectover an orderable T: Int, Decimal, Instant
T?nullable Tan optional field
<XId>generated ID value objecta record wrapping a Guid by default

A *Id type name used as a field type (e.g. ProductId) is generated as an ID value object even when no entity declares it via identified by — see Entities & identity (§6). The orderable types (Int, Decimal, Instant) are exactly those usable in relational comparisons and as Range<T> element types.

Putting the pieces together — types, fields, defaults, derived members, doc comments, and a version clause:

/// Catalog bounded context — the products a shop sells.
context Catalog version 2 {
enum Currency(symbol: String, decimals: Int) {
EUR("€", 2)
USD("$", 2)
GBP("£", 2)
}
enum MassUnit { Gram, Kilogram }
enum Availability { InStock, OutOfStock, Discontinued }
/// A stock-keeping unit, validated by shape and normalized via string ops.
value Sku {
code: String
normalized: String = code.trim.upper // derived
invariant code.trim.length > 0 "a SKU cannot be blank"
invariant code matches /^[A-Z]{3}-[0-9]{4}$/ "SKU must look like ABC-1234"
}
value Price {
amount: Decimal
currency: Currency
invariant amount >= 0 "a price cannot be negative"
}
/// A quantity value object: a Decimal amount plus an enum unit.
quantity Weight {
amount: Decimal
unit: MassUnit
invariant amount >= 0 "a weight cannot be negative"
}
value SalePeriod {
window: Range<Instant>
}
aggregate ProductCatalog root Product {
entity Product identified by ProductCode as natural(String) {
sku: Sku
name: String
price: Price
weight: Weight
availability: Availability = InStock // default
description: String? // optional
tags: Set<String>
sale: SalePeriod?
@since(2) barcode: String? // added in v2 of the context
displayName: String = name.trim // derived
summary: String = description ?? name
isAvailable: Bool = availability == InStock
onSale: Bool = sale.isPresent
}
}
readmodel ProductCard from Product {
sku
name
price
available: Bool = availability == InStock
}
query ProductsByAvailability(availability: Availability): List<ProductCard>
query ProductByCode(code: ProductCode): ProductCard
}