Vernac language reference
This reference describes the Vernac 0.5.0 language and generated API. Use matching compiler and runtime versions. Source checkouts default to 0.5.0-SNAPSHOT; released consumers select matching 0.5.0 artifacts after publication. Obsolete experimental syntax is not supported. The specification index identifies the normative document for each rule. A discrepancy between implementation and contract is a defect to investigate, not permission to silently redefine the contract.
Examples without a namespace or referenced type declarations are fragments to insert into an existing model. From model to application contains a complete model. For an introduction, follow the tutorials.
On this page
- Files, namespaces and imports
- Names and generated packages
- Types and optional values
- IDs
- Value objects
- Enum values
- Named collections
- Behavior and validation
- Entities and aggregates
- Repositories and queries
- Persistence and migrations
- Usecases
- Domain services
- Events and listeners
- Ports, adapters, DTOs and mappings
- Compiler and IDE boundary
- HTTP inbound adapters
- Offset and cursor pagination
- Inbound error policies
- Reusable type modules (0.4.0)
- Application composition status
- Export and implement a port across artifacts (0.4.0)
- Select an exported adapter with Spring profiles (0.4.0)
- Activate nonpersistent module functions (0.4.0)
- Prepare a module migration bundle (0.4.0 foundation)
- Persistent module contracts (0.4.0 foundation)
- Executable persistent-module lifecycle example
- Exported events and module listeners (0.4.0)
- Durable event ownership and startup
- HTTP entrypoints supplied by modules
- Module authoring and dependency refresh checklist
- Ubiquitous-language terms (0.4.0)
- Domain diagram tooling (0.4.0)
- Vernac source debugging (0.5.0 tooling)
- Server-rendered inbound MVC (0.5.0)
Files, namespaces and imports
The conventional source root is src/main/vernac. Files are UTF-8, end in .vernac,
and declare a namespace matching their source-root-relative directory:
namespace org.example.delivery;
import org.example.people.OwnerName;
import org.example.shared.*;
This file belongs under org/example/delivery/. A namespace may span files; names
in it are visible without imports. Type identity is namespace plus simple name,
not the filename. Explicit/wildcard imports refer to Vernac types, never arbitrary
Java classes. Fully qualified Vernac references are supported. Unknown, ambiguous,
duplicate and redundant own-namespace imports are diagnosed according to the
namespaces specification.
Declarations are resolved project-wide independently of source order. Imports do not recursively textually include files. Namespace is an organizational concept; it does not automatically create a DDD bounded context.
Namespace/import statements require semicolons. Declaration terminators may be optional where the grammar permits; examples use semicolons consistently where useful. Per-definition Java package overrides are not supported by the reviewed pipeline.
Names and generated packages
Vernac supports the specified Java-compatible Unicode identifier alphabet without normalizing spelling. Vernac keywords may be namespace segments when valid for the Java target. Java keywords, forbidden control characters and API collisions are rejected. Built-in type names cannot be redeclared as Vernac types.
| Definition | Public Java location |
|---|---|
| ID, value, enum, entity, aggregate, event, named collection | <namespace>.domain |
| Domain service, repository interface, port interface | <namespace>.domain |
| Usecase | <namespace>.usecase |
| JDBC repository | <namespace>.adapter.outbound.jdbc |
| Outbound adapter | <namespace>.adapter.outbound |
| Listener | <namespace>.adapter.inbound.event |
| Domain access interfaces | <namespace>.domain.access |
| Usecase access interfaces | <namespace>.usecase.access |
| Adapter DTO holder | <namespace>.adapter.outbound.dto.<Adapter>Dtos |
Omitted field/parameter/dependency names derive from the simple type name:
StatusReason → statusReason, URLValue → urlValue, int → intValue.
An explicit name takes precedence. Single-field values have no value exception.
value Title(String); exposes string(); value Title(String value); exposes value().
Collection names default to the element name plus s, without dictionary pluralization.
Repository names default to <Aggregate>Repository.
Effective names must be unique and legal in the generated Java API. Vernac does not
number or silently rename collisions. Entity/aggregate identity is always named id,
independently of how an ID is named when used as an ordinary field.
See names and Unicode.
Types and optional values
Ordinary value fields accept the explicit built-in type catalog: Java primitives and boxed equivalents, String, BigInteger, BigDecimal, UUID, Currency, and the listed immutable java.time types. The catalog is not a general Java import mechanism.
| Context | Modeled data |
|---|---|
| Value object | Approved built-ins, IDs, enums, other VOs and collections of values |
| Entity or aggregate | IDs, enums, VOs, entities and eligible named collections |
| Event payload / usecase input and output / port signature | IDs, VOs, enums and collections of values |
| Adapter schema | Approved transport scalars, local DTOs and technical collections |
An entity/aggregate cannot contain an aggregate, directly or through a collection. Use an aggregate ID instead. VOs cannot contain mutable entities, even through an immutable collection. Direct entity containment cycles are forbidden; ID references are not containment. A primitive/built-in entity state field requires a domain wrapper.
T? is optional. Domain storage, factory inputs and all optional operation/implementation/helper
parameters use @Nullable T;
getters return Optional<T>. int? is rejected; use Integer? or an optional domain
wrapper. Generated top-level types are @NullMarked. An Optional container itself
must never be null. DTO/result records have their separately specified Optional
component contracts, not nullable record accessors.
IDs
id TourId;
id StopId set StopIds;
An ID is a final UUID wrapper with a private constructor. Its API includes
create(), of(UUID), of(String), value(), asString(), identity-value equality,
hashing and rendering. ID state/behavior cannot be customized. A named list or set
may be requested explicitly. See ID rules.
Value objects
value Title(String) validates {
require(!self.string().isBlank(), "Title must not be blank");
};
value Money(BigDecimal amount, Currency currency);
value Notes(String? description, String? comment);
A VO generates a final class with private construction, final fields, of(...),
record-style getters, equals/hashCode over all fields and toString. It is not
a Java record. There are no mut fields or setters; fieldless VOs are rejected.
All-optional VOs are allowed and are distinct from absence of the VO itself; they
have only the full factory, not an additional zero-argument factory.
The full factory accepts all fields. If required and optional fields coexist, one required-only overload
delegates to the full factory; combinations are not enumerated. Null checks precede
validation. Failed construction returns no object. Ordinary VOs have no redundant
asString/asUuid helper; UUID single-value helpers are specified in the
value contract.
Enum values
value TourStatus = PLANNED | COMPLETED;
Enums become Java enums with @NullMarked and the ValueObject marker. Standard
name(), values(), valueOf(...) and == apply. There is no generated dbValue().
Constants are unique valid names; uppercase naming is a recommendation, not a rule.
String or name can be a constant without declaring a type or a method.
Enums may have behavior and a named collection. They do not have custom state, constructors, constant-specific bodies or validates blocks. Persistence codes and external mappings are separate concerns. See enum contract.
Named collections
value Tag(String) list Tags, set SelectedTags, list ArchivedTags;
id TourId set TourIds;
Each comma-separated list/set clause declares its own collection type and may
have its own behavior block. Names are independently optional; value Wert(String)
list, set Werte; generates Werts and Werte. Conflicting defaults or explicit names
are errors. Multiple clauses require the 0.4.0 compiler.
Collections are explicit final domain classes. Lists preserve order/multiplicity; sets retain the first equal instance. Input structure is copied, output views are unmodifiable, and transformations leave the original collection unchanged. Mutable entity elements are not deep-copied.
The API includes empty, of, iteration, stream, filter, matching, find,
contains, count, plus, plusAll, minus, minusAll and Java collection views.
Lists also provide get, first, last, distinct and duplicates.
duplicates() returns all occurrences of repeated values, including the first;
duplicates().distinct() selects one representative per group.
Entity/aggregate collections add by(id), contains(id), minusId(id) and
minusAllId(id). Collection by returns Optional; repository byId has a different
missing-result contract. Exact signatures, set equality and ordering are defined
in the collection contract.
Behavior and validation
value ProductName(String) behavior {
java imports { java.util.Locale; }
public String display() {
return self.string().toUpperCase(Locale.ROOT) + suffix();
}
private String suffix() { return "!"; }
};
Inline Java is generated into a separate companion, not inserted with privileged
access to private domain state. Public VO/enum/collection methods receive self;
private helpers receive only explicitly declared arguments. Public behavior may
instead declare implemented by fully.qualified.Class; or a simple class name
resolved through local Java imports, otherwise the generated caller package. The external static method
receives the same self and arguments as its inline equivalent.
Java imports belong in the behavior block. They do not change the resolution of modeled fields/signatures and cannot shadow protected Vernac/built-in names. Nullness is checked at generated boundaries and can be checked statically using NullAway, as configured in the basic template. A null required/Optional result is a BehaviorContractException, not a business rejection.
validates { require(condition, "message"); } runs through a separate read view
using self.field(). It does not expose setters or emit. These checks provide API
boundaries, not a purity/security proof of arbitrary Java. See behavior.
Entities and aggregates
id StopId;
value Address(String);
entity Stop[StopId](mut Address) behavior {
read Address currentAddress() { return self.address(); }
modify void relocate(Address address) { self.address(address); }
} list Stops;
Entities and aggregates use read and modify for public behavior; private helpers
remain private. Read methods receive Read views; modify methods receive Access
views combining Read and Write. The domain object implements neither access
interface. Field setters are not public domain methods.
create(fields...) always creates the ID internally. reconstitute(id, fields...)
retains identity and validates again. Aggregates additionally reconstruct technical
version and creation/update timestamps; entities do not gain aggregate metadata.
Required-only create overloads omit optional fields, never accept an external ID.
Equality and hashing depend on identity; toString includes identity and domain fields. Aggregate timestamps start equal, and effective modifications update the modification time. Technical optimistic version access belongs to infrastructure. See entities and aggregates.
Validation failures and object state
Modify operations change live objects. Validation occurs at successful outermost modify completion, including participating nested receivers. Failed behavior or validation does not restore fields or metadata. Do not continue using a failed graph as though it had rolled back. Abort the operation and obtain fresh state when needed. Database rollback does not repair Java objects or undo external I/O.
Read protection is shallow: returned children are ordinary entity instances, not recursively immutable proxies. The contract does not enforce aggregate ownership against every direct Java reference. Candidate-state rollback is not implemented.
Repositories and queries
repository TourRepository for Tour {
find Tours withStatus(TourStatus status)
where tourStatus = :status order by title;
find Tour? titled(Title title) where title = :title;
}
Declare the referenced aggregate and result collection explicitly. The generated
interface provides byId, save and delete; JDBC requires a surrounding transaction
(MANDATORY). byId returns the complete graph or AggregateNotFoundException. Save
uses optimistic locking; it does not silently reinsert a concurrently deleted root.
Find names do not imply predicates. Supported predicates include =, !=, <,
<=, >, >=, is absent, is present, like, contains, starts with and
ends with, combined with not, and, or and parentheses. Precedence is not,
then and, then or. Fields reference eligible root values or id, createdAt,
updatedAt; :name references a typed parameter. Capabilities depend on the scalar.
No arbitrary SQL, nested traversal or partial-aggregate projections are implied.
Offset-qualified queries are described in the pagination section below.
Find returns a declared aggregate collection or an optional singleton. Multiple matches for a singleton raise NonUniqueQueryResultException; this does not create a database uniqueness constraint. Ordered queries require list results. A list query without order produces a warning; ties remain unspecified. Contained-list position is independent of the order of root query results.
Equality follows the documented Java semantics, including decimal scale, signed floating zero and temporal components. Absent optional fields do not match a non-optional comparison parameter. Text contains/starts/ends treat text literally; LIKE uses explicit pattern semantics. See repository queries.
Persistence and migrations
Repository-backed aggregates map to quoted PostgreSQL schemas named by namespace. VOs flatten recursively. Optional structures use presence information when necessary; entity state and relationships are distinct so references can share state. Complete loads preserve shared Java identity within an aggregate and check concurrent changes. There is no lazy loading. Scalar representation limits are explicit.
| Maven goal | Effect |
|---|---|
vernac:compile |
Generate Java; reconcile compiler-owned source files |
vernac:schema |
Generate expected schema under target; no database changes |
vernac:migration |
Write a candidate migration and versioned model history |
vernac:schema-check |
Replay migrations and compare owned schema using disposable databases |
Migrations are candidates, never deployment guarantees. The application owner MUST
review, test and approve them. force permits generation despite recognized data-loss
risk; it does not invent a safe conversion or provide approval. Backfills and enum
code changes require deliberate decisions. See persistence,
scalar limits and migration tutorial.
Usecases
usecase CompleteTour(TourId) uses TourRepository tours behavior {
execute {
var tour = self.tours().byId(tourId);
tour.complete();
self.tours().save(tour);
}
}
A usecase is one Spring operation with REQUIRED propagation. Inputs/results are
domain values, not entity graphs. Omit returns for void; returns Title returns a
value; returns Title? returns Optional; returns (TourId, Title?) creates a nested
Result record with of factories. Results are explicit, not inferred from Java.
uses has comma-separated dependencies without parentheses. Access dependencies
through self. Exactly one execute is required; private helpers and Java imports
are allowed. execute implemented by ...; delegates to static external Java.
Optional validates sees only input getters. Spring interception is required for
transaction semantics; nested REQUIRED operations join the same physical transaction.
See usecase contract.
Domain services
A stateless service Name uses ... behavior { ... } generates a Spring domain bean
with named public operations, private helpers and optional per-operation preconditions.
Unlike usecases, services do not open transactions and can take entities/aggregates.
Each public implementation receives self Access and its parameters. Dependencies
remain constructor-injected; external methods use the same delegation protocol.
Services do not currently classify methods as read/modify. See service contract.
Events and listeners
event TourCompleted(TourId);
recorded event DispatchRecorded(TourId);
deferred listener NotifyDispatch on TourCompleted behavior {
execute { System.out.println("Completed: " + event.tourId()); }
}
The listener example logs for illustration; logging is not transactional or exactly-once.
An event is immutable and has shared runtime EventId and occurredAt metadata.
create creates an occurrence; reconstitute retains identity/time and validates.
Equality is occurrence identity, not payload equality. Events can validate but have
no behavior. Fieldless events are allowed.
self.emit requires an active transactional EventContext. Generated usecases establish
it; modify behavior, services and listeners may participate. Read views, validators
and VO behavior cannot emit. Publication is independent of save.
A named synchronous listener ... on Event runs immediately in the current transaction.
A deferred listener creates a durable job handled after commit. Each subscriber has
its own acknowledgement/retries. Event storage is relational per event type, not Java
serialization. recorded retains an event without requiring a deferred subscriber.
Workers claim jobs with leases and fencing tokens. Deferred handler work and successful acknowledgement share a transaction; retry handling and external effects have the limits in the event contract. Exactly-once external delivery, strict global ordering and automatic payload evolution are not promised.
Ports, adapters, DTOs and mappings
A port Name { Result operation(Input); } declares an outbound domain capability.
A separate adapter Name implements Port uses ... { behavior { ... } } implements
all operations, with self access for inline/external methods. Technical dependencies
use java uses inside behavior after optional java imports. Adapters do not open
transactions or introduce automatic retries.
Inside an adapter, schema Name (...) defines local transport records. Fields support
named "wire-name", naming snake_case on the schema, optional T?, absent as null
and patch T/patch T?. PATCH distinguishes unchanged, set and (when legal) clear.
Required fields reject missing/null/wrong-type JSON; unknown fields are ignored.
Mappings use one direction: mapping name: Target <- Source { field <- source.path; }.
Exact single-value wrapping/unwrapping is allowed; unit conversion and normalization
are not invented. Nested/collection conversion uses explicit named mappings when
required. A mapping can instead use a Java body or external implementation.
An adapter can declare technical properties before its implementation:
configuration {
URI baseUri from "carrier.base-url";
}
Spring reads required values at construction; self.baseUri() exposes the typed
value in behavior. Supported types are String, URI, Duration, Path, int, long and
boolean. See configuration.
An adapter may replace behavior with rest { client name; base name; ... }.
Each operation declares, in order, HTTP verb/route; path/query/header bindings;
optional body mapping; response mapping; optional on 404 absent; optional error
rules or Java error hook. Supported verbs are GET, POST, PUT, PATCH and DELETE.
Schemas and mappings may precede or follow the implementation.
Declarative error rules use when ... then reject DomainCode.VALUE "message";
or fail local|remote|unknown CATEGORY "message";. Ordered rules match only when
their condition is true; missing/malformed body operands are not blindly treated
as false. Business codes are declared enums. HTTP status alone cannot determine
business meaning or responsibility. No complete request payload is captured by default.
See ports, DTOs, mappings, REST syntax and HTTP runtime for the complete APIs and rules.
Compiler and IDE boundary
Maven generates Java and reconciles owned .java output using a manifest. It does
not delete arbitrary .class files or JARs; use Maven clean for a clean bytecode build.
The LSP shares semantic analysis for diagnostics, completion and navigation. IntelliJ
adds embedded-Java assistance and source-backed navigation; generated APIs must be
built/imported for Java resolution. Arbitrary Java bodies remain subject to javac
and configured static analysis.
FAQ · Specification · Tutorials
HTTP inbound adapters
adapter Name inbound http uses Usecase dependency { ... } defines named endpoints
with typed path/query/header bindings, an optional DTO body, explicit assignments to
usecase inputs and a response mapping. Assignments retain target-left <- orientation.
The adapter creates no implicit Result; <Usecase>.Result is a supported response
mapping source for explicitly declared tuple results. Transactions remain in usecases.
See the contract and complete tutorial.
Offset and cursor pagination
repository IssueRepository for Issue {
find offset Issues withStatus(IssueStatus status)
where issueStatus = :status order by createdAt desc, id asc;
find offset Issues countedWithStatus(IssueStatus status)
where issueStatus = :status order by createdAt desc, id asc counting;
}
Requires a declared list of complete aggregates and an explicit order ending in root id. Without counting the method returns OffsetQuery<Issue, Issues>; with counting it returns CountedOffsetQuery<Issue, Issues>. Both bind the filters and expose page(request) and lazy pages(request). Ordinary finds retain their original API.
Use OffsetRequest.ofPage(3, 20) or ofOffset(40, 20) in a Java body, importing from
org.vernac.runtime.pagination. Pages retain their concrete items list. Counted pages
add totalElements/totalPages; count and root selection share one SQL statement snapshot.
Each load requires an active transaction, reconstructs full graphs and checks versions.
A concurrent root change may raise an optimistic-lock conflict. Independent loads are
not a frozen snapshot. Generated repositories bind vernac.pagination.max-page-size
(default 1000), rejecting larger requests before SQL.
Cursor declarations use the same list and explicit final-id requirements:
find cursor Issues following(IssueStatus status)
where issueStatus = :status order by createdAt desc, id asc;
The method returns CursorQuery<Issue, Issues>. In a Java body, call
page(CursorRequest.first(20)) or continue with CursorRequest.after(cursor, 20).
CursorPage exposes items(), nextCursor() and nextRequest(); it has no count or absolute
page number. All cursor sort keys must be required. Each direction and physical time
component is respected; decimal prefix comparison follows numeric order, not scale.
The saved boundary survives deletion of its root. Tokens bind to the query, filters,
complete owned schema fingerprint and codec version, and are encrypted/authenticated.
Configure vernac.pagination.cursor-key (Base64 of 32 random bytes) for durable/shared
continuations. Blank uses a process key and loses validity after restart/on another
replica. Invalid tokens raise InvalidCursorException. Tokens never authorize access.
Paginated usecase results and HTTP/DTO mappings are implemented as described below.
PageNumber/PageSize/PageCursor are technical Java types, not modeled signature built-ins.
See the tutorial and normative contract.
Usecase result components and DTO fields
usecase ReadBoard(PageNumber openPage, PageNumber closedPage, PageSize)
returns (Title heading, counted offset Summaries openItems,
counted offset Summaries closedItems, cursor Summaries recentItems)
uses IssueRepository behavior { execute { /* return Result.of(...); */ } }
Summaries must be a declared list of immutable domain values (IDs/VOs/enums). Pages
are required; aggregates/sets/optional pages are rejected. The single-return form
returns counted offset Summaries is also supported. These result qualifiers are
not input/domain-field built-ins. PageNumber/PageSize in this sketch are application
VOs; project their getters into runtime request factories explicitly.
DTO page fields use schema BoardDto(counted offset list<ItemDto> openItems, ...);.
A mapping assignment openItems <- source.openItems using item; applies the named
entry conversion and retains metadata. Different variants, page flattening and page
internal traversal fail. HTTP still returns a named local schema, whose generated
codecs serialize completed envelopes without a global Jackson module. Invalid query
cursors return 400/INVALID_CURSOR; ordinary domain validation remains 422.
The complete tutorial includes the model, usecase and GET.
Inbound error policies
HTTP adapters and endpoints support ordered errors { when ... then respond ...; }
policies. Endpoint rules are checked first, then adapter rules, then safe defaults.
Selectors: rejection EnumType.CONSTANT, integration local|remote|unknown CATEGORY,
invalid request, invalid cursor, validation, not found, optimistic conflict,
domain rejection. A category is an existing IntegrationCategory constant. Typed
rejections match PortRejectedException; no implicit typed usecase rejection is added.
then respond 409; preserves the safe default code/message. then respond 409
"PUBLIC_CODE" "Public message"; replaces them. Both strings are required together.
Statuses are 400–599. Optional otherwise default; retains the final default fallback.
A policy block or errors implemented by ...; may follow an endpoint response, or
appear last in an adapter. The forms are exclusive per scope. Response-mapping failures
stay technical; policies never change transactions, retries or authentication.
See the normative inbound contract.
Reusable type modules (0.4.0)
A producer declares vernac-module.vmodule beside its POM, not below
src/main/vernac. Its separate grammar is module qualified.identifier { export
fully.qualified.Type; ... }. Explicit exports currently support IDs, VOs, enums
and named value collections, plus port interfaces. Publicly referenced local types and collection elements
need their own exports. No wildcard exports or foreign re-exports.
A normal compile-scoped Maven dependency makes public contracts available to ordinary Vernac imports, including compile-visible transitive dependencies. Type identities do not gain a module prefix. The compiler consumes META-INF/vernac/module.json and never regenerates dependency classes. External VO structure participates in local schema flattening/diffs. Module dependencies do not activate beans or migrations.
Build producer and consumer with the current Vernac Maven plugin. In IntelliJ, New → Vernac Module Definition creates the project file. Build after dependency changes to refresh the shared resolved editor index. For a runnable two-artifact example see type modules.
Application composition status
The project wizard distinguishes applications and reusable type libraries. An
application starts with an empty vernac-application.vapplication; a type library
gets vernac-module.vmodule and explicit exports. The application parser accepts activate, fixed bind ... to ... and
bind ... from configuration "key". Identity checks and IDE support are available,
and adapter bindings are operational. Explicit activate entries register module
usecases, services, repositories, listeners and HTTP entrypoints, including required
functional dependencies; persistent histories are coordinated before startup. See the module contract.
Export and implement a port across artifacts (0.4.0)
Contract-only modules can also export ports. Explicitly export every local type used
in the port’s public methods. Consumers import the port and its parameter/result
values, then write adapter Name implements ImportedPort { behavior { ... } } using
normal port/adapter syntax. The dependency supplies the original compiled interface.
Adding a port operation requires implementation rebuilds. Contract metadata is now
module-contracts-9; rebuild older development module artifacts. Outbound adapter publication and application bindings are supported as described below;
Persistent and nonpersistent functional activation are supported; module histories run
in dependency order before the application history. Module event contracts, recorded payloads and synchronous/deferred
receivers are supported. Inbound HTTP entrypoints are internal module components, enabled only through activation.
Select an exported adapter with Spring profiles (0.4.0)
Modules may export outbound adapters that implement exported ports, including ports
from other Maven dependencies. Their bodies are compiled only by their producer;
consumers use the published implementation class. Register a fixed or property-driven
binding in the consuming project’s vernac-application.vapplication:
application example.app {
bind example.shared.PaymentProvider
from configuration "billing.payment-provider";
}
The property value is an exported/local adapter’s qualified Vernac identity, e.g.
example.providers.NoopPaymentProvider, not a Java class name. Fixed syntax is
bind example.shared.PaymentProvider to example.providers.NoopPaymentProvider;.
Only the selected adapter is registered. Spring resolves its constructor dependencies
and required configuration. Unselected module adapters are not component-scanned.
The generated Boot auto-configuration registers the binding automatically; a plain
Spring context imports the generated <application name>.configuration.__VernacApplication
explicitly. Persistent and nonpersistent components support activate; the startup
coordinator runs required module histories before dependants and the application.
Use mvn clean install with the current compiler to rebuild earlier metadata artifacts.
Activate nonpersistent module functions (0.4.0)
A module may export domain services and usecases in addition to values, ports and adapters. Export all local types used in public signatures and inline result fields. Private service dependencies need no export. Consumer imports use original Vernac identities; implementations come from compiled dependency classes.
application example.shop {
activate example.billing;
bind example.billing.PaymentProvider from configuration "billing.payment-provider";
}
Activation registers all services/usecases in that module, including internal helpers, and transitively activates modules supplying declared functional dependencies. Type imports and port implementations do not imply activation. Selected adapters remain profile-controlled. An application-supplied transaction manager is required for executing usecases; REQUIRED calls join the current transaction. Module-owned entity/aggregate/repository contracts can be published, imported and activated with coordinated migration histories. Module event contracts and synchronous/deferred receivers are supported; payload tables belong to producers and the shared delivery table belongs to the application. See the executable functional model.
Prepare a module migration bundle (0.4.0 foundation)
The standalone Maven goal vernac:module-persistence packages an ordered, checksum-
verified SQL/snapshot chain and expected schema beneath
META-INF/vernac/modules/<logical-module>/. It does not apply SQL or enable persistent
module exports. In projects with a .vmodule, schema/migration/check/compile goals
now default SQL sources to src/main/vernac-schema/migration, outside ordinary Maven
resources. Applications retain src/main/resources/db/migration. An explicit
vernac.migrations override remains available.
Generate/review the next migration before preparing a changed schema. Every module SQL file needs its source snapshot; repeatable migrations are not supported here. Persistent contract publication automatically prepares/verifies the bundle. Persistent activation coordinates verified histories at startup. Recorded module events and synchronous/deferred receivers use those verified histories; the shared delivery schema is generated only by the application. See the persistence preparation contract.
Persistent module contracts (0.4.0 foundation)
Entities, aggregates, their collections and repositories may be explicitly exported. Export all local ID, field and public signature types. Consumers use the compiled classes/interface from the dependency and MUST NOT define a local repository for a foreign aggregate. Imported repositories contribute no local tables or generated JDBC classes. External VOs in local aggregates still flatten into locally owned tables.
Persistent publication requires a matching source migration history and automatically
packages/verifies the isolated bundle. Field mutability, read/modify modes and repository
query/pagination signatures are part of module-contracts-9 compatibility metadata.
Explicit persistent activation coordinates verified module histories in dependency order,
then application migrations, before dependent beans become callable. It requires the
application DataSource, transaction manager and Spring Boot Flyway initializer. No SQL is run by import,
compile or packaging. Do not bypass activation with dependency component scanning.
Executable persistent-module lifecycle example
See examples/persistent-modules: two Maven producer artifacts export values and usecases while retaining private repositories; the application activates the dependant module explicitly. Required modules migrate before dependants, then the application’s own history runs. If an exported VO shape changes, every owner embedding it MUST generate and review its own migration. Rebuild dependants against the changed public contract; a required field needs an explicit backfill for existing rows. Importing a value alone does not activate its producer.
Exported events and module listeners (0.4.0)
A module MAY export an event or recorded event and MUST explicitly export every local payload type
in its public contract. Consumers import the original qualified event name, construct
it through the compiled factory, emit it from an active usecase context, or declare a
synchronous or deferred listener on it. No dependency event classes are regenerated.
Listeners are internal runtime receivers rather than exported callable APIs. Activating the receiver’s module registers its listeners under qualified identities. A listener-only module can be activated. Deferred receivers require the producer’s compiled storage component and activate that required producer transitively. Event imports alone do not activate a publisher or receiver. Declared usecase/service/repository dependencies of active listeners can activate the required functional modules transitively.
A synchronous listener executes immediately in the emitting usecase’s transaction. Its exception fails that transaction. A deferred listener has its own persisted job, processing transaction, confirmation and retry. Several active receivers may observe the same event; their deferred jobs succeed/fail independently.
An illustrative publisher model (the receiving code below uses this public contract):
namespace example.billing;
id InvoiceId;
value CustomerName(String text);
event InvoiceOpened(InvoiceId, CustomerName);
namespace example.observer;
import example.billing.InvoiceOpened;
synchronous listener ObserveInvoiceOpened on InvoiceOpened behavior {
java imports { org.slf4j.LoggerFactory; }
execute {
LoggerFactory.getLogger("example.observer").info("Invoice {} opened", event.invoiceId());
}
}
The receiving artifact declares module example.observer {} if it exports no callable
API, and the application uses activate example.observer;. Metadata is
module-contracts-9; rebuild producer and dependant artifacts together.
Durable event ownership and startup
For a .vmodule producer, every local event has a prepared payload table and compiled
storage adapter, including plain events whose later consumers are not known yet.
Creating a table does not imply recording: emission persists the payload only for a
recorded event or at least one active deferred recipient. Module schemas MUST NOT
contain vernac.@delivery; the consuming application’s migration owns that shared
table. Merely importing a module event does not activate its producer or execute SQL.
A module deferred listener depends on the producer’s EVENT_STORAGE component. Local application deferred listeners on dependency events require explicit activation of the producer. Likewise, code emitting an imported recorded event must arrange producer activation; a Java import alone supplies no runtime storage adapter. Do not declare repositories or storage classes for foreign event payloads in the consumer.
The application determines its delivery schema from local deferred listeners and its activated/profile-eligible module graph. Generate/review application migrations after changing this graph. One Boot worker polls all active recipients only after required module and application migrations/validation have completed. It preserves the existing lease, at-least-once, per-recipient acknowledgment, retry and blocked-job semantics. Receivers must make external effects idempotent; SQL rollback does not undo them.
Record flags and storage capability participate in module-contracts-9 metadata. Rebuild all producers/dependants. Structural event/VO changes can make pending payloads incompatible; migrate deliberately or introduce a new versioned event type. Review event evolution independently of an ordinary aggregate upgrade.
HTTP entrypoints supplied by modules
A module may contain ordinary adapter ... inbound http definitions. These are internal
entrypoints, not exported callable types. Activate the owning logical module in the
application’s .vapplication file to register its controllers and scoped error policies.
Required usecase modules activate transitively. A dependency or type import alone exposes
no routes, even under component scanning. DTOs, mappings and errors remain producer-owned;
consumer compilation does not regenerate them. The application supplies
vernac-runtime-http and spring-boot-starter-web. Route paths are unchanged; ambiguous
routes fail at MVC startup. See the persistent module walkthrough
for the API module and a real cross-module HTTP call.
Module authoring and dependency refresh checklist
- A
.vmoduleexports public contracts; a.vapplicationselects/activates runtime composition. Do not put application bindings in a producer module. - Build/install the producer, declare the consumer Maven dependency, then build the consumer. Maven selects versions; the LSP reads the build-written index.
- Export every local type referenced by a public signature, including result fields and collection types. Internal services/listeners/HTTP adapters need no public export.
- Use Go To Declaration on an activation identity for a read-only module overview. Type navigation opens available producer sources or a marked contract view.
- After dependency builds, completion/navigation detect changed indexes. Incompatible metadata is diagnosed on open files. Never edit generated contract views as a fix.
- Keep module SQL histories immutable after publication. When an exported VO changes, inspect all owners that persist it: the producer’s tables and each consumer’s own flattened columns require separately reviewed migrations/backfills.
- This module scope is implemented. SaaS permission/quota policies, ready-made billing providers, distributed transactions and cross-artifact refactoring are separate work.
Ubiquitous-language terms (0.4.0)
term Name { description "Meaning."; example "A concrete case."; note "An exclusion."; }
is a top-level documentation declaration in an ordinary namespaced .vernac file.
Exactly one nonblank description is required; examples and notes are repeatable and
optional. Quoted names such as term "Outstanding balance" permit spaces. Names must
not be blank, contain controls or have surrounding whitespace. Name equality is exact
and case-sensitive; term Name and term "Name" are duplicates in the same namespace.
Terms are not types. They may coexist with same-named types and may describe concepts without classes. Only exact namespace/name equality associates documentation with a model definition; there are no explicit links, inherited meanings or synonym aliases. Do not invent term imports or exports. Module metadata carries documentation for exported types automatically; it does not publish standalone or internal-only terms.
Vernac type hover includes associated text, including imported module types without sources JARs. JavaDoc is added to associated generated domain/usecase/outbound classes. Maven compilation writes target/vernac-glossary.md and target/vernac-glossary.json. Text changes do not alter structural API fingerprints or SQL schemas. Module metadata is module-contracts-9; rebuild development producers and consumers. No LLM integration is implied. See the term contract and the tutorial.
Domain diagram tooling (0.4.0)
mvn vernac:diagram exports every local namespace; -Dvernac.diagramNamespace=example.orders
selects one. Output defaults to target/vernac-diagrams, with Markdown/Mermaid, .mmd,
PlantUML .puml and versioned JSON per namespace. Diagrams come from resolved declarations,
not generated Java or guessed behavior. Referenced outside types are boundary nodes.
IntelliJ’s Show Domain Diagram action includes unsaved source snapshots and opens Markdown;
enable its Mermaid preview extension if needed. No rendered image or live model explorer
is implied. See the diagram tutorial.
Vernac source debugging (0.5.0 tooling)
The 0.5.0 plugin/compiler can map inline domain behavior breakpoints to original Vernac lines using build-owned metadata. Build with debug line/local-variable information and use matching source/generated/runtime artifacts. This is not new DSL syntax and does not imply full debugger coverage or hot swap. See the prototype scope and the executable example it links.
For mapped behavior frames, debugger expressions use Java syntax and the generated
method scope (self, arguments and locals). Breakpoint logging and suspension are
standard IntelliJ options; see the debugger walkthrough
for Evaluate Expression, conditions and non-suspending logpoints.
A guided debugging tutorial covers the 0.5.0
integration and states its exact coverage. Inline usecase execute blocks, domain service
operations and their private helpers also have mappings. User-authored require rules
on VOs, entities, aggregates, usecases and services map their opening require line to
one generated condition entry point with a Read/input self. Synchronous/deferred
listener execute blocks and private helpers are also mapped; the actual worker stack
is preserved. Inline outbound adapter methods/helpers, Java mapping bodies in both
directions, explicit declarative field assignments, inbound execute argument assignments
and event require rules are also source mapped. Mapping frames expose source;
inbound controllers retain their actual Java variables. Pure transport configuration
has no artificial breakpoint. Adapter-method preconditions remain unsupported by the
language contract. Debugging tooling does not
change transaction, validation or asynchronous execution semantics.
Expression editing is separate from runtime evaluation. The 0.5.0 plugin routes expression requests to the verified Java scope of their Vernac line to address red variables and missing Ctrl+Space completion. Core expression editing and adapter/mapping debugging have been confirmed in the installed IDE. Runtime mapping tests alone do not establish every IntelliJ editor interaction.
Read/Access and validation-input facades have a stored-property inspection renderer. It preserves the actual Java facade type and contract, reads recognized field paths without invoking debuggee methods, and adds no field-write capability. Optional children are explicitly nullable stored-value projections, with accessor-based Watch expressions. Normal stepping skips only verified pure facade reads, not modification wrappers, validators or callbacks. Contributor mode restores raw Java views and disables this filter. Native tree/Watch/stepping acceptance is separate from the JVM inspector test.
The 0.5.0 plugin also supplies native PSI references for declared Vernac types in DSL signatures/fields/imports/dependencies and repository aggregate targets. These use compiler namespace/import/export visibility and exact UTF-16 type-name ranges for native Ctrl-hover navigation feedback. Injected Java, strings and enum constants are not reinterpreted as DSL type references. Existing LSP documentation, completion and mapping-specific navigation remain in place. These navigation references do not implement complete cross-language rename or reference search.
Java reference types from the approved built-in table also have native navigation
references in DSL type positions, including behavior signatures. These resolve to
the actual Java class through IntelliJ’s module/JDK scope. Explicit implemented by
class names resolve in the same scope. Primitive types have no class target;
unknown classes do not get guessed links. Java bodies retain injected-Java
resolution. This editor support does not expand the permitted DSL type set or
provide cross-language renaming.
The 0.5.0 New Project wizard offers a runnable Spring Boot Web Application, alongside the minimal model and type-module templates. The web starter combines aggregates, paginated queries and usecases with a declarative inbound MVC adapter and Thymeleaf templates. See the starter walkthrough.
Server-rendered inbound MVC (0.5.0)
Use adapter Name inbound mvc uses ... with base, a required error view, and
GET/POST endpoints. Wire bindings and execute { parameter <- group.field; } follow
the JSON boundary’s exact conversions. Add form scalar bindings, absent-only
default values on non-optional query/form bindings, and sensitive before fields
that must not enter a rendered input model. Headers are never exposed in that model.
A value response uses render "board" model page;; a void response omits the model.
A static application-local redirect "/"; uses 303 after POST and 302 after GET.
Optional results require an explicit absent view/status 404 or 410. Ordered shared
error selectors can render an error view or redisplay a named GET page loader,
with explicit public code/message and 4xx/5xx status. Redisplay is endpoint-local,
does not retry the POST and ends at a safe 500 fallback if its loader fails.
Generated MVC controllers remain in .adapter.inbound.http and own no transaction.
Templates receive typed usecase results, safe error.code()/message() and only
allowed raw inputs entries such as inputs['form.title']. No exception text or
arbitrary request map is exposed automatically. The initial scope is scalar
URL-encoded GET/POST forms, not multipart uploads, repeated collection fields,
dynamic redirects or template fragments. See the contract
and complete tutorial.
Shared Java implementation rules (0.5.0)
All optional custom-operation parameters, including usecase execute, external
delegates and private helpers, are nullable T. Optional getters and results remain
non-null Optional