vernac / architecture, expressed.
Browse documentation

Domain Field Types and Nullness

Status: normative requirements for the 0.4.0 language revision.

Related specification: Namespaces and Imports.

This specification defines modeled field eligibility and nullness. It does not extend the separate persistence/transport capability catalogs.

1. Purpose and scope

Vernac models domain state with explicit, meaningful types and removes the boilerplate required to implement them in Java. A controlled set of built-in Java value types provides the foundation for value objects.

These are deliberate Vernac modeling constraints, not a claim that DDD itself prescribes a Java type whitelist or forbids primitive fields in every entity.

The rules below apply to user-declared state fields. They do not automatically apply to method parameters, return types, adapter schemas, event payloads or embedded Java implementation code. Those contexts require separate decisions. Generated technical fields, such as version numbers and timestamps, may use appropriate Java types directly.

MUST and MUST NOT indicate requirements; SHOULD indicates a recommendation.

2. Allowed domain field categories

Containing definition Allowed field categories
Value object Built-in scalar/value types, Vernac IDs, Vernac enums, other value objects, and value-object collections
Entity Vernac IDs, Vernac enums, value objects, contained entities, and corresponding Vernac collections
Aggregate Vernac IDs, Vernac enums, value objects, contained entities, and corresponding Vernac collections

Entities and aggregates MUST NOT directly declare primitive or built-in Java value fields as user-defined state. Introduce a meaningful value object instead.

namespace org.example.tasks;

id TaskId;
id CustomerId;
value Title(String);
value DueDate(LocalDate);
value Status = PENDING | COMPLETED;

aggregate Task[TaskId](
    Title title,
    DueDate? dueDate,
    CustomerId owner,
    mut Status status
);

IDs and enums do not require an additional value-object wrapper.

A value object MUST NOT contain an entity or aggregate as a field. Its contained values and any collection of value objects must preserve its immutability. A final reference alone is not a sufficient immutability guarantee.

Contained entities belong to their aggregate’s consistency boundary. References to other aggregate roots use IDs rather than direct aggregate objects. A collection of contained entities is consequently different from a collection of IDs referring to external aggregates. General collection declaration and ownership rules are specified separately; this document adds no new collection syntax.

3. Built-in value types

The following types are available without user imports. Each built-in has a known Java mapping; the generator supplies imports where necessary.

Category Vernac names Java mapping
Boolean boolean Java primitive
Integers byte, short, int, long Java primitives
Floating point float, double Java primitives
Character char Java primitive; one UTF-16 code unit
Boxed primitives Boolean, Byte, Short, Integer, Long, Float, Double, Character Corresponding java.lang classes
Text String java.lang.String
Arbitrary-precision integer BigInteger java.math.BigInteger
Decimal BigDecimal java.math.BigDecimal
UUID UUID java.util.UUID
Instant Instant java.time.Instant
Local date/time LocalDate, LocalTime, LocalDateTime Corresponding java.time classes
Offset date/time OffsetDateTime, OffsetTime Corresponding java.time classes
Zoned date/time ZonedDateTime java.time.ZonedDateTime
Time spans Duration, Period Corresponding java.time classes
Calendar components Year, YearMonth, MonthDay, Month, DayOfWeek Corresponding java.time types
Time zones ZoneId, ZoneOffset Corresponding java.time classes
Currency Currency java.util.Currency

Examples:

value Title(String);
value Quantity(int);
value Temperature(double);
value Money(BigDecimal amount, Currency currency);
value DeliveryDate(LocalDate);

This is an explicit catalog, not permission to use all classes from these Java packages. Being immutable does not automatically make an arbitrary Java class an accepted built-in.

The catalog provides technical value semantics. Domain-specific restrictions, such as nonblank text or nonnegative quantities, remain the responsibility of the modeled value object’s validation rules.

4. Type resolution and rejection

The compiler MUST resolve field types to known identities and categories before generation. A simple name, imported name or fully qualified name is subject to the same field-category restrictions.

A fully qualified name MUST NOT bypass the catalog:

value Wrapped(org.example.MyVernacValueObjectType);

This is allowed if the referenced type is a known Vernac value object.

value Wrapped(org.example.MeineJavaKlasse);

This is rejected if the name does not resolve to a supported type. General Java classpath imports are not introduced for domain field declarations.

Distinguish diagnostics:

Without Java classpath analysis, do not claim that an unknown name necessarily identifies a Java class. Explain the supported categories instead.

These failures MUST be reported by Vernac at the field/type location, rather than relying on javac to report missing imports or invalid generated code.

5. Optionality and nullness

Reference-type uses are non-null by default. ? explicitly permits null:

value Measurement(Integer? value);

Integer without ? remains non-null. Primitive types cannot be nullable:

value Measurement(int? value);

This MUST fail with a Vernac diagnostic suggesting Integer?. Apply the corresponding rule to all eight primitive/wrapper pairs.

Do not silently translate int? to Integer?.

The generator MUST annotate every generated top-level class, interface, enum and any other top-level type declaration with JSpecify @NullMarked. Package-level package-info.java generation is not required for this policy. This avoids shared-file ownership conflicts across generation units and hand-written code.

Type uses that permit null MUST receive @Nullable, consistently across fields, factory/constructor parameters, accessors and other affected signatures. This includes Java API contracts such as:

@Override
public boolean equals(@Nullable Object other) {
    // Generated equality implementation.
}

JSpecify annotations describe nullness; they do not enforce runtime checks. Generated construction, reconstitution and mutation paths MUST enforce required non-null state at runtime as appropriate. Exact validation timing and exception contracts are specified with the corresponding feature.

No Java-annotation syntax is introduced into Vernac source by this policy. Nullness annotations are generated from language semantics.

6. Equality and validation

Ordinary value objects use equality and hashing over all their state fields. The built-in fields retain their Java value semantics; the generator MUST NOT silently normalize values to change equality.

Decimal values

BigDecimal.equals includes scale. Consequently, values representing 1.0 and 1.00 are unequal under the default policy, even though compareTo regards them as numerically equal.

Any domain-specific normalization must be explicit. A normalization facility is not specified here. Do not generate compareTo-based equality while retaining an incompatible hash implementation.

Floating-point values

Generated equality for primitive float and double MUST follow the corresponding wrapper equality semantics: NaN values compare equal for value object equality, and positive and negative zero are distinct. Hashing MUST be consistent with that equality.

NaN and infinity are not universally forbidden. An individual domain type’s validation decides whether they are meaningful.

7. Generated representation

An ordinary value-object definition generates a Java class, not a record, with:

The enum form generates a Java enum:

value Status = PENDING | COMPLETED;

It does not use the ordinary value-object class/factory representation.

Entity and aggregate definitions generate identity-bearing Java classes. Their identity, factories, mutation behavior and relationship handling are specified separately; this document does not redefine those contracts.

8. Initially excluded field types

The following are not accepted as direct value-object field types:

void as a method return type is a separate context. Restrictions here do not imply that Java collections or library helpers are forbidden inside embedded Java implementation code.

9. Persistence is a separate capability

An allowed domain type does not automatically imply a supported JDBC mapping. When repository generation encounters an unsupported mapping, Vernac MUST emit a diagnostic identifying the affected field path and type. It MUST NOT silently fall back to an arbitrary SQL type or defer the problem to JDBC at runtime.

Persistence rules, SQL type mappings, serialization, enum persistence values and migrations are defined during the persistence review.

10. IDE, documentation and acceptance checks

Compiler and plugin must share the same type catalog and category rules. Completion SHOULD prioritize types valid for the current field context. Diagnostics MUST agree with command-line compilation and point to the source field/type, including when the type is imported or fully qualified.

Before completion, provide:

Tests must cover:

Existing tests remain unless their expectations are explicitly superseded by these accepted language changes. Parser-only success is not sufficient; generated Java and relevant runtime behavior must be tested.

Built-in names are reserved against Vernac declarations. Built-ins use their catalog names; fully qualified Java class spellings are not alternative domain-type aliases. A fully qualified Vernac identity must resolve in the project index.

Operation signatures, event payloads, collections and adapter schemas have their own contracts in the specification index. Java imports inside implementation blocks do not add modeled field types. User-defined Java value-type integration remains outside this catalog. EventId is the shared runtime event identity, governed by the event contract.

References