Domain service contract
A service declares a stateless domain capability whose responsibility does not
naturally belong to an entity or value object. It exposes named operations, even
when there is only one. There is no special execute method, implicit return
inference, custom package or generated Result DTO.
service TourPlanning
behavior {
public void moveStop(Tour source, Tour target, StopId)
validates {
require(!self.source().id().equals(self.target().id()),
"Source and target tour must differ");
} {
var stop = source.allStops().by(stopId).orElseThrow();
source.removeStop(stopId);
target.addStop(stop);
}
}
Public API and dependencies
A Spring @Service bean is generated in <namespace>.domain. It has no mutable
per-call state. Required dependencies declared by uses Type name, OtherType
are constructor-injected into private final fields. Omitted names use the shared
Vernac naming convention. Conflicts require explicit names.
Dependencies may be domain services, domain ports or repository interfaces. Usecases and concrete adapters are not service dependencies. Repository access is possible, but application orchestration and transaction ownership normally remain in the usecase. Dependencies use the shared namespace resolver and must satisfy their own declaration contracts.
Only public operations appear on the bean. Dependency getters are exposed on
<namespace>.domain.access.<Service>Access, not on the bean. A private inner
class implements that interface; the service itself does not implement it.
The Access interface exposes dependency getters and public service operations.
Calls through self.otherOperation(...) return through the bean wrapper and run
that operation’s null checks, preconditions and result checks again. Recursive
calls behave like ordinary Java recursion; the compiler does not prove termination.
Services do not introduce transactions. A usecase provides the transaction when needed; pure calculations can run without one. Repository MANDATORY behavior is unchanged. Services are responsible for using aggregate behavior rather than bypassing aggregate invariants. No automatic object rollback is provided.
Implementation
behavior contains optional java imports { qualified.Type; }, followed by
public operations and private inline helpers. Each operation declares its return
type and parameters explicitly; parameter names may be inferred. read and
modify are not service modifiers, and mut parameters are not supported.
IDs, values, enums, collections, entities and aggregates are supported in signatures,
as are approved built-in Java types. Signature types use namespace resolution.
Java imports support implementation bodies; they do not bypass Vernac type checks.
Inline code lives in __VernacBehavior_<Service>, a separate implementation class.
Public implementations receive self of the Access type, followed by their
parameters. They cannot access the bean’s private fields. Private helpers are
static, have only their declared parameters, and receive any needed dependency
explicitly, e.g. helper(self.policy(), input).
An external implementation is selected per public operation:
public Title choose(Title title) implemented by com.example.TitleSelection;
It provides public static Title choose(ServiceAccess self, Title title).
Implementation names follow the shared Java import/package resolution contract.
It is not required to be a Spring bean. Delegation still passes through the
normal generated behavior companion. Constructors/injection in the external
class are not part of this protocol. Private helpers cannot use implemented by.
All generated types are NullMarked. Public optional input parameters are
@Nullable T; implementation and private-helper optional parameters are also
@Nullable T, with no implicit Optional wrapping. Optional results are Optional<T> throughout. Required reference
inputs use DomainChecks; null required/Optional public results use
BehaviorContractException. Empty Optional is valid. Primitives are not null-checked;
optional primitive types require a boxed type or domain wrapper.
Per-operation preconditions
A public operation may place validates { require(...); } between its signature
and its inline body or implemented-by clause. Preconditions run after input null
checks and before delegation, for both implementation forms.
In validations, self is an input view, NOT the service Access view. It exposes only parameter getters; optional parameters return Optional. It has no dependency getters or service operations. Bare parameter references receive a Vernac diagnostic. The generated input interfaces are nested technical types within ServiceAccess; their numbered names are not an application API and can change when methods move.
Protection is shallow. Entity/aggregate parameter getters return the actual objects, and collections can contain mutable objects. Calling a getter does not make the reachable graph immutable. Developers must keep preconditions free of mutation and external effects. Existing Read interfaces also return ordinary child objects, so recursively read-only wrappers were deliberately deferred to avoid a large refactoring. This is a design aid, not a Java purity/security proof.
Consistency and follow-up
Services and usecases now both expose dependencies through self Access. Service Access also offers emit, which requires the surrounding transaction and EventContext normally established by a usecase. Services still do not open transactions themselves. Revisit service read/modify classification later, so explicitly read-only operations can lose this capability without rewriting the whole access model.
Tooling and tests
The compiler validates names, resolved types, dependency kinds, overload collisions, imports and explicit validation access. Java compilation checks method bodies and external signatures. Editor projection supplies Access/input-view contexts and nullable implementation parameters. Navigation from Java service types and public methods targets the corresponding Vernac declaration.
Tests cover parsing, semantic errors, generated Java compilation, real Spring constructor injection, inline/external calls, self calls, Optional/null contracts, preconditions and identity-preserving transfer between aggregates. The transfer test operates in memory; it is not a cross-aggregate PostgreSQL integration test.