Specs, services & policies
13.1 General
Section titled “13.1 General”Three constructs let you model behaviour that doesn’t live inside a single entity: a spec names a reusable business rule, a service holds stateless cross-entity operations, and a policy documents how one aggregate reacts to another’s domain event. All three are declared at context scope (specs may also sit at aggregate scope) and stay free of imperative C# — Koine emits predicates, pure methods, and seams, never hand-written orchestration.
13.2 Syntax
Section titled “13.2 Syntax”spec_declaration : 'spec' Identifier 'on' type_name '=' expression ;
service_declaration : 'service' Identifier '{' service_member* '}' ;
service_member : operation_decl | usecase_decl ;
operation_decl : 'operation' Identifier '(' param_list? ')' ':' type_ref ( '=' expression )? ;
policy_declaration : 'policy' Identifier 'when' Identifier 'then' policy_reaction ;
policy_reaction : type_name '.' soft_name ( '(' policy_arg_list? ')' )? ;
policy_arg_list : policy_arg ( ',' policy_arg )* ;
policy_arg : soft_name ':' expression ;A spec binds a name to a boolean expression evaluated in terms of the named type_name’s
members. A service groups one or more service_member entries: each member is either an
operation_decl (a typed, optionally pure function) or a usecase_decl (an application-layer use
case that emits an async command/query handler — see Application layer & CQRS (§15)
for the full usecase grammar and emitted shape). An operation has a typed parameter list, a return
type_ref, and an optional = expression body that makes it pure. A policy names the event
keyword (when Identifier) and the target reaction (type_name.command(...)); each policy_arg
passes a named argument rooted in the event’s fields. The expression grammar is specified in
Expressions (§9).
13.3 Semantics
Section titled “13.3 Semantics”13.3.1 Spec semantics
Section titled “13.3.1 Spec semantics”A spec N on T = <bool> binds a boolean predicate to a name that can be referenced by name inside an
invariant (§10), a command requires clause, or another spec. A
referenced spec is inlined at its use site — composition is flattening, not a method call chain.
You compose specs with the boolean operators &&, ||, and !:
context Shop { value Order { lineCount: Int total: Int invariant IsLarge "order must be large" } spec IsLarge on Order = lineCount > 10 || total > 1000}When IsLarge is named as an invariant, its predicate is inlined into the Order constructor; a
violation throws DomainInvariantViolationException.
13.3.2 Service semantics
Section titled “13.3.2 Service semantics”A service is a home for logic that belongs to no single object. Operation parameter and return types are validated against the context’s types — a service may reference value objects, entities, enums, and (as types) aggregates.
The shape of the emitted class depends on the operations it contains:
| Operations | Emitted as |
|---|---|
| All have expression bodies (pure) | public sealed class with concrete methods |
| Any operation is bodyless | public abstract class with that operation as an abstract method seam |
A bodyless operation is a deliberate seam — you declare the contract in the model and implement it in C#:
service ExchangeRateService { operation convert(amount: Money, rate: Decimal): Money = amount * rate // pure operation latestRate(from: String, to: String): Decimal // seam (bodyless)}Because latestRate has no body, the whole service becomes an abstract class and LatestRate an
abstract method for the consumer to override.
13.3.3 Policy semantics
Section titled “13.3.3 Policy semantics”A policy expresses a cross-aggregate reaction in the ubiquitous language: when this event happens,
that command runs on another aggregate. The argument expression in each policy_arg is rooted in
the event’s fields.
13.4 Translation to C#
Section titled “13.4 Translation to C#”The table below summarises the emitted shape for each construct:
| Construct | Scope | Emitted file | Emitted shape |
|---|---|---|---|
spec N on T = <bool> | context or aggregate | <Context>Specifications.cs | static bool N(this T x) extension-method predicate |
service N { operation … } | context | <Service>.cs | sealed class (all pure) or abstract class (any seam) |
policy N when E then … | context | <N>Policy.cs | I<N>Policy interface + abstract partial class <N>Policy |
13.4.1 Spec translation
Section titled “13.4.1 Spec translation”Each spec becomes one boolean extension method on a per-context static class
<Context>Specifications, with the target instance bound to the parameter x. Because it extends
the target type, you can call it fluently (customer.IsVip()) or statically
(CustomersSpecifications.IsVip(customer)):
spec IsVip on Customer = tier == Goldpublic static class CustomersSpecifications{ public static bool IsVip(this Customer x) => x.Tier == LoyaltyTier.Gold;}
// usage: customer.IsVip() (with `using Customers;` in scope)If a spec body uses a LINQ collection operation, the consuming file pulls in using System.Linq;
automatically.
13.4.2 Service translation
Section titled “13.4.2 Service translation”Because every operation in the example below is pure (has an expression body), the service emits as a
sealed class with expression-bodied methods. Operation names become PascalCase methods; parameters
stay camelCase:
service LoyaltyService { operation discountRate(tier: LoyaltyTier): Decimal = if tier == Gold then 0.10 else if tier == Silver then 0.05 else 0.0}public sealed class LoyaltyService{ public decimal DiscountRate(LoyaltyTier tier) => ((tier == LoyaltyTier.Gold) ? 0.10m : ((tier == LoyaltyTier.Silver) ? 0.05m : 0.0m));}13.4.3 Policy translation
Section titled “13.4.3 Policy translation”A policy emits a handler interface plus an abstract seam — the intended call is recorded as documentation, not generated:
policy PostToLedger when PaymentCaptured then Ledger.record(amount: capturedAmount)public interface IPostToLedgerPolicy{ void Handle(PaymentCaptured e);}
public abstract partial class PostToLedgerPolicy : IPostToLedgerPolicy{ /// <remarks>Intended reaction: Ledger.record(amount: e.CapturedAmount).</remarks> public abstract void Handle(PaymentCaptured e);}The policy type is PascalCase(name) + "Policy"; the interface prefixes I. Inside the Handle
body the event is the parameter e, so the documented reaction roots its arguments at e
(e.CapturedAmount). You implement the wiring in a partial class.
13.5 Full example
Section titled “13.5 Full example”Copy-pasteable context combining all three constructs:
context Customers { enum LoyaltyTier { Bronze, Silver, Gold }
entity Customer identified by CustomerId { name: String tier: LoyaltyTier = Bronze }
event CustomerUpgraded { customer: CustomerId tier: LoyaltyTier }
aggregate Rewards root RewardAccount { entity RewardAccount identified by RewardAccountId { customer: CustomerId points: Int
command grant(amount: Int) { points -> amount } } }
// A reusable rule, inlined wherever it's referenced. spec IsVip on Customer = tier == Gold
// Stateless, pure cross-entity logic. service LoyaltyService { operation discountRate(tier: LoyaltyTier): Decimal = if tier == Gold then 0.10 else if tier == Silver then 0.05 else 0.0 }
// A documented cross-aggregate reaction (handler seam emitted). policy GrantWelcomePoints when CustomerUpgraded then Rewards.grant(amount: 100)}See also
Section titled “See also”- Aggregates, commands & events (§11) — the event and command constructs that specs and policies build on.
- Invariants (§10) — the guard expression grammar shared by specs, value objects, and entities.
- Expressions (§9) — the expression grammar used in spec bodies and operation bodies.
- Value objects (§5) — value objects that service operations may receive and return.
- Application layer & CQRS (§15) — for
usecasedeclarations that emit asyncI<Service>interfaces andIUnitOfWork. - Repositories & concurrency (§14) — the repository declarations that round out the tactical toolkit.