Skip to content

Status: Stable Package: dev.vertique.core Artifact: vertique-core Depends on: nothing — this is the framework root API reference: javadoc.io

vertique-core is the vocabulary every other Vertique module is written against. It supplies the Vert.x dependency-injection seam, the framework exception hierarchy, the typed-configuration contract, the extension-ordering and lifecycle contracts, the event-bus dispatch types, and the shared annotation vocabularies for validation, sanitization, and JSON profiles.

Core is almost entirely declaration. Most of what it defines is implemented elsewhere: the context holder ships in dev.vertique:vertique-context, the config parser in dev.vertique:vertique-config-core, bean validation in dev.vertique:vertique-validation, health aggregation in dev.vertique:vertique-management. Depending on core gives an application the contracts and the annotations, not the runtime behind them.


Every Vertique application already depends on vertique-core transitively — it arrives with any starter or feature module. Depend on it directly when:

  • you are writing a Dagger module that binds Vertx, @VertxConfig JsonObject, or EventBus;
  • you throw or catch framework exceptions and want the semantic roots that drive HTTP mapping;
  • you parse a configuration section into a typed record through ConfigParser;
  • you implement a framework extension point declared here — a health check, a lifecycle step, a JsonMapperProfile, or a canonicalizer;
  • you annotate an operation with @ValidateWith, @Canonicalize, @Sanitize, or @JsonProfile.

Pair core with the module that implements the contract you are using: vertique-config-core for ConfigParser, vertique-validation for BeanValidator, vertique-json for the JSON profile registry, vertique-context and vertique-correlation for context propagation, vertique-management for health-check aggregation, vertique-services for typed event-bus service calls.


dev.vertique.core.async.Timeouts provides shared timeout boundaries for asynchronous work:

  • awaitCondition(vertx, condition, pollIntervalMs, timeoutMs, operation) polls a condition on a Vert.x timer, completing when it becomes true or failing with TimeoutException at the deadline.
  • withTimeout(future, timeoutMs, description) returns the future’s result or failure, bounded by the timeout.

Both timeout values must be positive. A timeout does not cancel the underlying asynchronous operation, so the caller still owns cleanup and failure policy. For example, a consumer can report the timeout while its close future continues to settle in the background.

Every bootstrap path funnels two values into the application’s Dagger graph: the Vertx instance and the root configuration JsonObject. VertiqueRuntime carries exactly those two, and VertiqueComponentFactory<C> turns them into the built component. Nothing host-specific crosses this seam — no ApplicationContext, no service locator. A host that embeds Vertique adapts its own beans through ordinary typed Dagger modules, so a missing adapter is a compile-time missing-binding error rather than a runtime lookup failure.

VertxModule is the Dagger module on the other side of the seam: constructed with the runtime’s two values, it publishes them plus the event bus and the low-level event-bus client.

Configuration is read only in @Provides methods, which parse a section into a typed record whose compact constructor validates it — so a bad value fails Dagger component construction rather than first use. Module internals depend on the typed record, never on the raw JsonObject. See Configuration.

Two ordering contracts, deliberately unrelated

Section titled “Two ordering contracts, deliberately unrelated”

OrderedExtension orders behavioral extensions (interceptors, customizers, observers). LifecycleOrdered orders lifecycle participants (startup and shutdown steps, verticle deployments). Both sort phase ascending → priority ascending → orderKey ascending, and both expose a static comparator(). They are separate interfaces because their phase() methods return different enums — ExtensionPhase and LifecyclePhase.

The rules an extension author must know:

  • Phase dominates priority absolutely. An APPLICATION extension with priority = Integer.MIN_VALUE still runs after every SYSTEM_FIRST extension. Do not try to emulate cross-phase ordering with priority.
  • Lower priority() runs first; the default is 0.
  • orderKey() defaults to getClass().getName(). It is the final tie-break, so ordering is deterministic across JVM restarts only when the key is unique per registered instance. Two instances of the same class tie; a lambda or method-reference registration gets a synthetic class name that is not stable across compilations. Give such participants distinct priority() values or override orderKey().
  • ExtensionPhase is not a security boundary. Any module on the classpath can declare SYSTEM_FIRST. The phase confers ordering, not privilege.

LifecyclePhase is the single phase vocabulary, in declaration (ordinal) order:

Phase Kind Purpose
CONFIGURE Non-verticle Build runtime configuration (e.g. install Jackson modules) before any deployment
VALIDATE Non-verticle Validate assembled configuration and wiring before any deployment
MIGRATE Non-verticle Run data/schema migrations before any deployment
BOOTSTRAP Verticle Framework bootstrapping (codec registration, config watchers)
INFRA Verticle Infrastructure verticles (management, health endpoints)
SERVICES Verticle Service-layer verticles (event-bus dispatch)
EDGE Verticle Edge verticles (HTTP, WebSocket)
AFTER_START Non-verticle Post-start work, after all verticles are deployed

isVerticlePhase() is true only for BOOTSTRAP, INFRA, SERVICES, and EDGE.

Startup interleaves per phase: for each phase in order, that phase’s startup steps run sequentially in comparator order and fail-fast, and only then are that phase’s verticles deployed. A step declared in SERVICES therefore runs before the SERVICES verticles, not after them. Shutdown runs the shutdown steps of the phases whose startup completed, in reversed comparator order, best-effort.

How a swallowed observer failure is logged

Section titled “How a swallowed observer failure is logged”

Observer-style callbacks — observers, listeners, hooks and completion scopes whose contract says their exceptions are “caught, logged, and swallowed” — are all reported through one framework-internal class, dev.vertique.core.extension.ObserverFailureReporter. It is public only so that the framework’s other modules can share it; applications do not use it. What it means for an implementer of such a callback:

  • An Exception, AssertionError or StackOverflowError is logged at WARN every time, naming the observer class, the callback and the failure’s class only. The failure itself, with its message and stack, is logged at DEBUG, because an exception message can contain payload text.
  • A LinkageError means the callback cannot run at all. It is logged at ERROR with the failure the first time it is seen for an observer class and callback, then at most once every five minutes (ObserverFailureReporter.REPORT_INTERVAL) with the number of failures in between. It is never silenced for good.
  • What happens to any other Error is stated by each callback’s own contract; most sites let it propagate.

Framework and application exceptions extend a semantic root from dev.vertique.core.exception rather than raw RuntimeException. The root chosen is what determines the HTTP status a REST boundary produces, so picking the most specific applicable root is the whole mechanism — see Failures, Constraints, and Common Mistakes.

A value that must ride along with a request or a durable dispatch implements the marker interface ContextValue. That marker gates the write path: ContextHolder.bind is <T extends ContextValue>, while current(Class<T>) is deliberately unbounded so anything stored can be read back. The five context-producing SPIs carry the same bound on their context-type parameter. The holder implementation and the encoder/decoder registries ship in dev.vertique:vertique-context.

ScopeKey and ScopeContext live in dev.vertique.core.scope. Applications use a key to name a data or execution partition — a tenant, project, workspace, or organization supplies the meaning. The key constructor rejects null with NullPointerException and other invalid text with IllegalArgumentException. A valid key has 1–255 Unicode code points, no ISO controls, no unpaired surrogate, no format character, at least one non-whitespace code point, and no leading or trailing whitespace (Character.isWhitespace or Character.isSpaceChar). Internal whitespace other than controls (including U+00A0, U+2028, and U+2029) is accepted. Construction preserves the exact string; equality uses that exact text. ScopeKey.DEFAULT equals new ScopeKey("default") and names an ordinary partition — it confers no privilege, fallback, or automatic binding.

ScopeContext wraps one non-null ScopeKey and implements ContextValue, so callers may bind it explicitly through the existing holder API. Construction itself installs no ambient state. When the application’s existing context runtime includes ContextRuntimeModule from vertique-context, its default durable codec pair carries an explicitly bound scope unless an explicit durable contribution claims the scope type or namespace. When active, the built-in pair captures at WORKFLOW, OUTBOX, and DELAYED_JOB, and decodes at WORKFLOW, OUTBOX_SERVICE, and DELAYED_JOB; it does neither at KAFKA or any other identifier. The pair does not affect generic snapshots or service-dispatch propagation. When the built-in pair is active, a raw scope body already in a Kafka carrier remains untyped and is not decoded; the existing authoritative ingress still clears a prior ambient scope for its lifetime, and closing its holder scope restores that binding. Applications that need scope at Kafka bind it after ingress.

On a later workflow drive, the caller’s ambient scope or valid explicit scope metadata can override the saved scope for that drive without changing the saved carrier or workflow scope fields. The saved value is not an immutable partition pin; applications that require one partition for an entire instance must enforce that policy before supplying or accepting context. Neither type asserts trust or access, and scope never selects a workflow. Key text is not neutralized for logging, so consumers encode values for their sink.


Dagger @Module constructed with the Vert.x instance and the root configuration; list it on the application @Component and supply it through the generated builder. Bindings provided:

Type Qualifier Description
Vertx — The Vert.x instance passed at construction
JsonObject @VertxConfig The root application configuration
EventBus — vertx.eventBus()
EventBusExceptionMapper — Translates Vert.x ReplyExceptions into typed exceptions
EventBusClient — Low-level dispatch transport

VertiqueRuntime and VertiqueComponentFactory<C>

Section titled “VertiqueRuntime and VertiqueComponentFactory<C>”
public record VertiqueRuntime(Vertx vertx, JsonObject config) {
public static VertiqueRuntime of(Vertx vertx, JsonObject config);
}
@FunctionalInterface
public interface VertiqueComponentFactory<C> {
C build(VertiqueRuntime runtime);
}
VertiqueComponentFactory<AppComponent> factory = rt -> DaggerAppComponent.builder()
.vertxModule(new VertxModule(rt.vertx(), rt.config()))
.build();
AppComponent component = factory.build(VertiqueRuntime.of(vertx, config));

Both record components are required: the compact constructor calls Objects.requireNonNull on each, so a null throws at construction rather than surfacing later as a missing binding. Prefer the static of factory, which delegates to the canonical constructor.

Concrete, hierarchy-aware, context-aware Throwable → Throwable translator registry. Usable directly for ad-hoc translation, and the base class the layer mappers in dev.vertique:vertique-rest-jaxrs, dev.vertique:vertique-services, and dev.vertique:vertique-db-core extend.

public class FailureMapper {
public FailureMapper();
public <T extends Throwable> FailureMapper on(Class<T> type, FailureTranslator<T> translator);
public <T extends Throwable> FailureMapper on(Class<T> type, ContextAwareFailureTranslator<T> translator);
public Throwable translate(Throwable throwable, String context);
public Throwable translate(Throwable throwable); // context defaults to throwable.getMessage()
public FailureTranslator<?> findTranslator(Class<? extends Throwable> exceptionClass);
protected Throwable fallback(Throwable throwable, String context);
}
var mapper = new FailureMapper();
mapper.on(DatabaseException.class, (e, ctx) -> new DataAccessException(ctx, e));
Throwable translated = mapper.translate(sqlFailure, "save user");
  • Lookup walks the superclass chain and stops below Object, so the most specific registered translator wins. Interfaces are not consulted.
  • Registration is last-wins and clears the whole lookup cache, which is what lets a later, more specific registration override an already-resolved broader one.
  • findTranslator returns null — not a no-op translator — when nothing in the hierarchy matches. fallback is what translate returns instead; the default returns the throwable unchanged.
  • translate dispatches on the runtime type, so behavior is identical regardless of which on(...) overload registered the translator. A two-argument lambda binds to the context-aware overload; a one-argument lambda binds to the plain one.

Sealed success/failure monad used for event-bus replies. Implementations are the records Result.Success<T> and Result.Failure<T>.

public sealed interface Result<T> {
static <T> Result<T> success(T value);
static <T> Result<T> failure(Throwable cause);
boolean isSuccess();
boolean isFailure();
T get(); // throws NoSuchElementException on a failure
Optional<T> toOptional();
Throwable cause(); // null on a success
<U> Result<U> map(Function<T, U> mapper);
<U> Result<U> flatMap(Function<T, Result<U>> mapper);
Result<T> recover(Function<Throwable, T> recoveryFn);
<U> U fold(Function<T, U> onSuccess, Function<Throwable, U> onFailure);
}

map, flatMap, and recover catch an Exception thrown by the supplied function and return it as a Failure. Result.failure(null) is rejected.

The event-bus carrier for framework dispatch and its typed context map.

public final class DispatchEnvelope<T> {
public static <T> DispatchEnvelope<T> of(T payload, DispatchMetadata metadata);
public static <T> DispatchEnvelope<T> of(T payload, DispatchMetadata metadata, String replyAddress);
public static <T> DispatchEnvelope<T> of(T payload); // empty metadata
public static DispatchEnvelope<Void> empty();
public T payload();
public DispatchMetadata metadata();
public Optional<String> replyAddress();
}
public final class DispatchMetadata {
public static DispatchMetadata of(Map<String, Object> dispatchContext);
public static DispatchMetadata empty();
public Map<String, Object> dispatchContext();
public <C> Optional<C> context(Class<C> type);
}

The dispatch context is keyed by fully-qualified type name; context(Class) is the typed read. DispatchMetadata.of copies the caller map, so later caller mutations cannot change what the receiver observes across the local hop. Annotating a type @DispatchContextValue (in dev.vertique.core.eventbus) makes it injectable as a service-handler method parameter.

Application code should not build envelopes by hand — use the typed client in dev.vertique:vertique-services, which captures currently-bound context values automatically.

@Singleton low-level transport over the framework’s dispatch codecs, exposing Future<Result<?>> request(String address, DispatchEnvelope<?> envelope, long sendTimeoutMs) and void send(String address, DispatchEnvelope<?> envelope). Failed requests are translated through EventBusExceptionMapper before the future fails.

This is a transport primitive, not an application API. Prefer the typed service client in dev.vertique:vertique-services, which adds supervisor checks, effective-timeout computation, and service-specific failure enrichment.

HTTP-agnostic validation contract; the implementation ships in dev.vertique:vertique-validation.

public interface BeanValidator {
<T> void validate(T object);
<T> void validate(T object, Class<?>... groups);
<T> List<ViolationDetail> check(T object);
<T> List<ViolationDetail> check(T object, Class<?>... groups);
List<ParameterViolation> checkParameters(Object instance, Method method, Object[] args, Class<?>... groups);
void validateParameters(Object instance, Method method, Object[] args, Class<?>... groups);
}

validate and validateParameters throw BeanValidationException on violation; check and checkParameters return the violations instead, empty meaning valid.

Type Shape
ViolationDetail record (@Nullable String path, String message, @Nullable String type, @Nullable Map<String, Object> args, ViolationPathForm form); the four-argument constructor and of(path, message) state JAVA_PROPERTY_PATH, a null form becomes JAVA_PROPERTY_PATH, atPointer(pointer, message, type, args) states JSON_POINTER and unlocated(message, type) has a null path. path is the producer’s own path in the stated form, or null when the violation locates nothing in any validated value (a whole-operation rule, a truncation marker); an application’s own rule identifier can be carried in type. form is not part of the JSON form of a detail
ParameterViolation record (int parameterIndex, ViolationDetail detail, @Nullable PropertyPath propertyPath); the two-argument constructor remains and leaves propertyPath null. propertyPath is an INTERNAL seam outside the Stable promise (see PropertyPath)
PropertyPath INTERNAL, outside the Stable promise; may change without a deprecation cycle. record (List<Segment> segments): a Java-side path from a validated value to a violated element, free of Jakarta Validation types and never holding a map key, set member or validated value. Segments: Property(String name, @Nullable Class<?> ownerType), Index(int index) (non-negative) and Opaque() (a map entry or set member); Segment is an open interface, so a consumer reads a kind it does not recognize as Opaque. root() is the empty path and isRoot() tests for it. toJavaPath() renders a property as its name (preceded by . unless it is the first rendered token), an index as [n], and an opaque or unrecognized segment as []; the root is "": items[0].zipCode, byKey[].name, lists[0][1], [2].name, tags[]. Not an application extension point
ViolationCarrier interface { List<ViolationDetail> violations() }, implemented by validation exceptions whose violations a transport may render; the list is unmodifiable and never null
ViolationPathForm enum { JAVA_PROPERTY_PATH, JSON_POINTER }: how a ViolationDetail writes its path. JAVA_PROPERTY_PATH is a Java property path of the validated object and is not a request location. JSON_POINTER is an RFC 6901 pointer into the document the producer validated, named by the producer’s own mapper; a producer that states it guarantees every path is "" or begins with / and contains no key the client chose. boolean accepts(String path) tells whether a path is well formed for the form: always true for JAVA_PROPERTY_PATH; for JSON_POINTER true only for "" or a /-prefixed path whose every ~ is followed by 0 or 1, with no ISO control character, no U+2028 or U+2029 and at most 2,000 characters (null is not accepted). New values and methods may be added, so a consumer that switches on it keeps a default branch
BeanValidationException extends ValidationException, implements ViolationCarrier; violations() is defensively copied and unmodifiable
JakartaPathPointers INTERNAL, may change without a deprecation cycle; not an application extension point. static Resolver resolver(ObjectMapper mapper, @Nullable InputFieldNameResolver names) opens a resolution session for one batch of violations (not thread-safe; its per-class lookups live only as long as the session); Resolver.toPointer(PropertyPath path, @Nullable Type rootType, @Nullable Object document) returns the RFC 6901 pointer of the deepest verified position, "" for the root. A segment is emitted only when it is verified, otherwise the pointer stops at the deepest verified ancestor (shorter, never different): a property is named by its primary wire name from the mapper’s deserialization introspection, and an unknown member or a member below a scalar, Object or unknown type ends the pointer; list and array positions are emitted per level (/lists/0/1, /2/name for a root list); a map entry, set member, or a position on anything but a list or array ends the pointer at that property, so no key text is ever emitted; Optional and other reference types are transparent; a recorded owner class is used only under flat type information on the declared type or on the holding property (@JsonTypeInfo with PROPERTY, EXISTING_PROPERTY or EXTERNAL_PROPERTY inclusion, or Id.DEDUCTION), when it is assignable to the declared base and the projection lists it among the base’s subtypes or cannot enumerate them — an assignable runtime class the projection does not list (or a failed subtype lookup) stops the pointer at the property holding the value, because such a subtype may rename even inherited members; flat type information with no recorded runtime class (always the case for a path string, and so for the path-string entry point below) stops the pointer at the holding property too, since the mapper may have bound any subtype; a recorded class outside the declared hierarchy, or a type without type information, keeps the declared type’s names; a value under WRAPPER_OBJECT or WRAPPER_ARRAY type information, @JsonIdentityInfo, a mapper-wide default typer, or (when the mapper enables UNWRAP_SINGLE_VALUE_ARRAYS) any bean below the root — and the root bean itself unless a document is checked to hold an object there — stops the pointer at the property holding it ("" at the root), and a list of such values at the list (/pets); names are escaped ~ → ~0 then / → ~1 with no percent-encoding, so an empty wire name gives /. A property is emitted only when the mapper writes it through a setter or a creator parameter (record components included), or through a field when the document holds a declared spelling of it; a read-only, getter-only or ignored property, a name the class (@JsonIgnoreProperties, detected through the annotation introspector and config overrides) or the holding property ignores, a member whose type the mapper ignores (@JsonIgnoreType or an ignored-type config override), a @JsonBackReference member, the @JsonAnySetter member and a @JsonUnwrapped member stop the pointer at their bean. A bean bound outside its reported properties — a class-level custom deserializer or converter, a builder (Lombok @Jacksonized included), a delegating creator, @JsonFormat(shape = ARRAY) on the class or in a config override — stops the pointer at the property holding it ("" at the root), as does a bean with a single-argument constructor or factory that is not its properties creator unless the document holds an object there (so single-component records resolve). A property bound through @JsonDeserialize(using / contentUsing / converter / contentConverter / as / contentAs) or @JsonFormat(shape = ARRAY) is emitted and ends the pointer; JsonNode, Map and Object properties are never descended. A list or array that skips null elements (contentNulls = SKIP from the default setter info, a config override or @JsonSetter) or accepts a single value as an array (ACCEPT_SINGLE_VALUE_AS_ARRAY on the mapper, in a config override of the sequence type or of Collection, or in the property’s @JsonFormat) gets no index, and neither does a list or array whose own type carries a class-level custom deserializer or converter, which may rebuild the elements in another order: {"items":[null,{"name":""}]} gives /items. The checks use public Jackson introspection only and are memoized per session; a deserializer installed by a registered Jackson module is not detected. The class-level check is the one JacksonFieldNameResolver uses for boundJavaNames (JacksonBindings, below). Without names (mapper-only) the document is ignored, so field-only properties and beans with a scalar creator stop the pointer; with the route’s projection a property must also be among its bound Java names and must not be one the projection promotes members out of; a failing projection stops the pointer and is remembered for the session. With the projection and a document (the bound body as JsonObject, JsonArray, Map, List or scalar) every step is checked against it: a member is looked up only inside an object the document holds there (a position holding null, a scalar or an array stops the pointer at that position: {"owner":null} → /owner); exactly one document key that is a declared spelling of the property (primary name or alias, case-folded when the mapper folds case) and maps back to it is emitted as sent (/zip, /ZipCode), two or more stop at the parent, and no other key — a Java name on a renaming route, a map key, an undeclared member — is ever emitted; when the bean matches member names with a case rule other than the mapper’s (@JsonFormat(with/without = ACCEPT_CASE_INSENSITIVE_PROPERTIES) on its class, in its config override or on the holding property), or the mapper matches case-insensitively and its locale lower-cases a key or declared spelling differently from Locale.ROOT, membership cannot be verified and the pointer stops at the parent whether the member is present or omitted; a list position past the end of the document’s array stops at the list. “Verified” means the mapper reads the member: through a setter or a creator parameter, or through a field when its declared spelling is present. An omitted member is named, by its primary wire name, only as the last segment of the path, inside an object the document holds, and only when its own binding member is a setter, creator parameter or record component ({"items":[{},{}]} → /items/1/name; on a SNAKE_CASE route {"zipCode":""} → /zip_code for a setter-bound zipCode), so hidden bound required members are named on omission; an omitted member that only a field writes (a public field, a field behind a renamed setter or creator parameter) and an omitted intermediate member or position stop at the parent. At most MAX_SEGMENTS (256) segments; per session, each owner class is introspected and asked of the projection once and each visited document object’s keys are indexed once, and a property’s matching keys in one object are computed once, stopping at the second match; list positions are read directly, never by walking the array; iterative; never throws (a failure yields the pointer verified so far). static String toPointer(String jakartaPath, @Nullable Type rootType, ObjectMapper mapper) parses the property-path string (a numeric bracket is a position, any other bracket an opaque marker, a negative or overflowing position or an unclosable bracket ends the parse, a container-element node contributes only its position) and resolves it in mapper-only mode without a document. static String loggablePath(@Nullable String) renders a path string for logs through the same parser, stopping at the first bracket of any kind, which renders [] (byKey[ZZKEY].name and byKey[0].ZZKEY].name → byKey[]; items[0].name → items[]: without the validated type a position cannot be told from a key shaped like one, so this fallback never renders positions); loggablePath(@Nullable PropertyPath) renders positions and continues past opaque segments (byKey[].name, lists[0][1]); both remove ISO control characters, U+2028/U+2029 and Unicode format characters (bidirectional overrides, zero-width characters) and cut to 512 characters
JacksonBindings INTERNAL, in dev.vertique.core.json, may change without a deprecation cycle; not an application extension point. static boolean bindsThroughItsProperties(DeserializationConfig config, BeanDescription description) reports whether the mapper binds a type through the properties its deserialization introspection reports: false for a class-level @JsonDeserialize(using = …), a builder, a @JsonCreator(mode = DELEGATING) creator or a single-parameter @JsonCreator whose parameter has no name. Shared by JacksonFieldNameResolver (a null boundJavaNames) and JakartaPathPointers, so one rule decides both; stateless

ViolationDetail deliberately excludes the invalid value so a violation can be serialized without leaking input. type and args are populated by the validation module and are null when it is not configured to resolve them.

ViolationCarrier is the seam a transport uses to render structured violations: the default REST mapping renders any ValidationException that implements it as a 400 with one error per violation. BeanValidationException implements it. Path, message, type and args reach clients verbatim, so an implementor builds every ViolationDetail it creates without a submitted value. A constraint message produced by an application’s own constraints passes through unchanged, so applications must not interpolate the validated value into a message. Each detail states the form of its own path in form(): JAVA_PROPERTY_PATH by default (as for BeanValidationException), or JSON_POINTER for a detail built with ViolationDetail.atPointer. A pointer is relative to the document its producer validated and is built with the producer’s own mapper, so it is not a pointer into the request body unless the code that knows where that document sits says so. A transport reports a location only where it knows that, and only for a pointer that ViolationPathForm.accepts admits. A producer emits only paths that ViolationPathForm.accepts admits.

@ValidateWith(Class<?>[] value) selects Bean Validation groups for a single method; the default group applies when it is absent.

Reflection-free metadata contracts shared by codegen consumers and runtime scanners. An aspect or interceptor author consumes them; application code does not implement them — generated code and the framework’s own scanners provide the implementations.

MethodMetadata:

Method Returns Group
name() String reflection-free
declaringType() Class<?> reflection-free
returnType() Class<?> reflection-free
parameterTypes() Class<?>[] reflection-free
parameters() List<ParameterMetadata> reflection-free
findAnnotation(Class<A>) Optional<A> reflection-free
hasAnnotation(Class<? extends Annotation>) boolean reflection-free
genericReturnType() Type reflection-free on generated metadata; reflective on scan metadata
asMethod() Method reflective

ParameterMetadata:

Method Returns Group
index() int reflection-free
name() String reflection-free
type() Class<?> reflection-free
findAnnotation(Class<A>) Optional<A> reflection-free
hasAnnotation(Class<? extends Annotation>) boolean reflection-free
genericType() Type reflective
annotationsLazy() Supplier<Annotation[]> reflective; default returns an empty array
  • The reflection-free group — including both annotation lookups and generated genericReturnType() — is safe on the critical path and needs no GraalVM reflect-config.json entry. asMethod(), ParameterMetadata.genericType(), and annotationsLazy() remain reflective; the scan-path implementation of genericReturnType() is reflective too.
  • On the codegen path, annotation lookups query compile-time-captured annotation literals, not live runtime annotations. An annotation added after compilation — by a dynamic proxy or a bytecode agent — is invisible. Only @Retention(RUNTIME) annotations are ever visible.
  • findAnnotation returns Optional, never null. Both findAnnotation and hasAnnotation take an annotation-typed Class (Class<A extends Annotation> and Class<? extends Annotation>).
  • parameterTypes() gives the erased signature without materializing ParameterMetadata objects; parameters() gives the full per-parameter view.

The resilience annotation vocabulary and its declaration metadata are provided by dev.vertique:vertique-resilience. Core deliberately remains independent of that artifact; applications using @Retry, @Timeout, or @CircuitBreaker should depend on the resilience module directly or through the module that enforces those declarations.

public record ScopeKey(String value) {
public static final ScopeKey DEFAULT; // equal to new ScopeKey("default")
}
public record ScopeContext(ScopeKey scopeKey) implements ContextValue {}

Validated exact-identity partition name and its holder-eligible composition. See Scope vocabulary. system is an ordinary accepted key, not a special variant. ScopeKey itself does not implement ContextValue.

Read-only view of the correlation identifiers bound to the current execution. It is a ContextValue and is marked @DispatchContextValue, so it propagates across service dispatch.

public interface CorrelationContext extends ContextValue {
CorrelationIdentifier requestId();
CorrelationIdentifier correlationId();
@Nullable CorrelationIdentifier causationId();
@Nullable TraceReference trace();
List<ProtocolCorrelationRef> protocolCorrelations();
@Nullable CorrelationSessionRef session();
Map<String, String> attributes();
CorrelationContextSnapshot snapshot();
static CorrelationContext unbound();
static CorrelationContext generated(String source);
}

CorrelationIdentifier is a record (String value, String source); both components must be non-blank. CorrelationContext.unbound() is the non-joinable sentinel (value "unavailable", source "unbound") for surfaces that are deliberately not request-tied. CorrelationContext.generated(String source) mints fresh UUID request/correlation ids for fail-closed emission sites that still need an audit join key when no ambient correlation is bound.

The runtime that binds this context, and the CorrelationIdGenerator SPI it uses, ship in dev.vertique:vertique-correlation. An application overrides ID generation by binding its own CorrelationIdGenerator — that module declares the binding as optional.

Time-ordered identifiers (RFC 9562 UUIDv7) as plain java.util.UUID values, for keys that should sort by creation time — for example database primary keys that append to the index instead of scattering across it.

UUID id = UUIDV7.randomUUID(); // drop-in for UUID.randomUUID()
long createdAt = UUIDV7.timestampMillis(id); // embedded Unix epoch milliseconds
  • Same layout as PostgreSQL uuidv7() and Hibernate’s version 7 generator. The 12 rand_a bits hold the fraction of the millisecond in 1/4096 steps (about 244 ns) and the 62 rand_b bits are random, so database-generated and application-generated keys sort together.
  • Monotonic per JVM. Every call returns a UUID strictly greater than all earlier results from the same JVM, including within one millisecond and across clock regressions. UUID.compareTo and the string form sort identically.
  • No cross-process guarantee. Different processes or hosts order by their clocks, then by random bits where the clocks give the same instant.
  • Not a secret. The UUID discloses its creation time and carries fewer random bits than a version 4 UUID. Do not use it as a token or session secret; keep UUID.randomUUID() for those.
  • The embedded timestamp can run ahead of the clock after a backwards clock step, by as much as the step, or during a sustained burst above about four million identifiers per second, until the clock catches up. It never runs behind the last issued value.
  • Thread-safe, short lock. Callers contend only for a brief critical section, so it is safe to call from an event-loop thread.
  • timestampMillis throws IllegalArgumentException for any UUID that is not version 7, variant 2, and randomUUID throws IllegalStateException if the system clock is outside the 48-bit range.

Transport-agnostic description of a request or response body, used where a module must inspect a payload without depending on REST or audit types.

public interface PayloadSource {
PayloadKind kind(); // ABSENT | BUFFERED | STREAMING
Optional<String> contentType();
OptionalLong declaredLength();
Optional<Buffer> bufferedView();
Optional<InputStream> bufferedStream();
byte[] copyPrefix(int maxBytes);
}
PayloadSource absent = PayloadSources.absent();
PayloadSource buffered = PayloadSources.buffered(bytes, "application/json");
PayloadSource streaming = PayloadSources.streaming("application/octet-stream", declaredLength);
  • No defensive copy anywhere. buffered(byte[], …) captures the array reference; a caller that needs immutability must copy before calling the factory. Mutating the array afterwards changes what readers observe.
  • bufferedView() is zero-copy only for a Buffer-backed source. For byte[] and ByteBuffer backings it lazily materializes and copies, so a hot path that only reads bytes should prefer bufferedStream().
  • copyPrefix is the one accessor that always copies; it exists for diagnostics, not for the data path.

A named ObjectMapper selected at a framework boundary, so one application can serialize different surfaces under different Jackson policies.

public record JsonProfileId(String value) {
public static final JsonProfileId SYSTEM; // "system"
public static JsonProfileId of(String value);
}
public interface JsonMapperProfileRegistry {
ObjectMapper mapper(JsonProfileId id);
JsonMapperProfile profile(JsonProfileId id);
Set<JsonProfileId> profileIds();
default void validateConfigured(@Nullable String profileId);
}

@JsonProfile("name") is @Target({TYPE, METHOD}) and selects a profile at a boundary that supports it. Which placements a boundary accepts is the boundary’s own contract: REST resources and MCP tools accept both TYPE and METHOD with method-level overriding type-level, while rest-client interfaces and Kafka listeners/producers accept TYPE only. JsonProfileId trims its value and rejects null or blank. Looking up an unknown id throws JsonProfileConfigurationException, which extends ConfigurationException. The registry implementation ships in dev.vertique:vertique-json.

JsonProfileId.SYSTEM ("system") is the reserved baseline id. The id "vertx" is retired: it was renamed system, and the registry rejects it — as a configured id and as an application-contributed profile id — with a message naming the rename.

A profile may declare, per Java class, the JSON Schema fragment describing the wire form its mapper actually produces or accepts — so a generated schema matches the profile’s serialization instead of the Java type’s default shape.

public final class JsonSchemaFragment {
public static JsonSchemaFragment parse(String schemaJson);
public String canonicalJson();
}
public final class JsonSchemaTypeOverride {
public enum Direction { INPUT, OUTPUT, BOTH }
public static JsonSchemaTypeOverride input(Class<?> javaType, JsonSchemaFragment fragment);
public static JsonSchemaTypeOverride output(Class<?> javaType, JsonSchemaFragment fragment);
public static JsonSchemaTypeOverride both(Class<?> javaType, JsonSchemaFragment fragment);
public Class<?> javaType();
public Direction direction();
public JsonSchemaFragment fragment();
}
JsonSchemaTypeOverride.both(
BigDecimal.class,
JsonSchemaFragment.parse(
"""
{"type":"string","format":"decimal","maxLength":100}
"""));

parse takes a non-null, non-blank JSON object holding one self-contained Draft 2020-12 fragment. It orders object keys recursively by String.compareTo (never reordering arrays), retains only the canonical compact string, and returns an immutable value; canonicalJson() returns that string and two fragments are equal when their canonical JSON is equal. Parsing is syntactic plus bounded structural validation — the consuming validator still compiles the completed schema.

parse rejects — with an IllegalArgumentException whose bounded message names the violated rule and never echoes the fragment — a null or blank input, malformed JSON, a non-object root, and any occurrence of $schema, $id, $anchor, $dynamicAnchor, $ref, $dynamicRef, or $defs. The schema generator owns the document dialect and definition graph, so a fragment-local reference would change meaning once embedded. That rejection is structural and deliberately over-broad: those key names are rejected at any depth, including where one is merely a property name inside a properties object.

JsonSchemaTypeOverride requires non-null arguments and describes one exact raw class — no assignability or subtype matching, and never a map key. An override for BigDecimal reaches a BigDecimal property and the element of a resolved List<BigDecimal>, but not a BigDecimal subclass nor the key type of Map<BigDecimal, String>. INPUT and OUTPUT select one generation direction; BOTH selects both, and conflicts with a direction-specific declaration for the same class. The generator that consumes these values ships in dev.vertique:vertique-json-schema.

Vert.x routes every Json.encode, Json.decodeValue, new JsonObject(String|Buffer), JsonObject.encode(), mapTo and mapFrom call through one process-wide codec. vertique-core registers its own codec for that role and points it at a swappable ObjectMapper; VertiqueJson is how framework and application code reach that mapper.

public final class VertiqueJson {
public static ObjectMapper mapper(); // live read
public static boolean ownsCodec();
public static Optional<JsonProfileId> profile();
public static void install(JsonProfileId id, ObjectMapper mapper);
public static void resetForTests(); // gated, see below
}

Before installation the codec delegates to Vert.x’s own mapper, so a process that never installs behaves exactly like stock Vert.x. dev.vertique:vertique-json contributes the CONFIGURE-phase step that installs the profile named by json.systemProfile; from that point the whole process encodes and decodes through that profile’s mapper. Mappers are swapped, never reconfigured — Jackson forbids reconfiguring a mapper after first use — and Vert.x’s own mapper is never mutated.

mapper() is a live read: call it at use time, or capture it only in an object constructed during or after CONFIGURE. A static initializer or a Dagger provider runs earlier and would capture the pre-install mapper.

install is keyed by profile id and enforces three rules on every call, whoever the caller is — the rules bind the act of installing, not the installed instance’s future state (a live ObjectMapper can still be reconfigured by any in-process code; the JSON runtime re-checks default typing at VALIDATE, and a graph without it logs a WARN there when nothing was installed):

  • A different id than the one already installed is refused with an IllegalStateException naming both ids and both installing classes — one process has one JSON codec. The same id installs the new, equivalent instance and logs the swap at INFO, so a second application booted in the same JVM under the same system profile starts normally.
  • The mapper must register Vert.x’s Jackson module (VertxJsonSupport.module() in dev.vertique:vertique-json), otherwise JsonObject, JsonArray and Buffer values would be bean-serialized instead of round-tripping — IllegalArgumentException.
  • The mapper must not activate Jackson default typing, which would let any decoded payload choose the type it instantiates — IllegalArgumentException. Annotation-driven polymorphism with @JsonTypeInfo stays available.

Registration is a packaging concern. The codec reaches Vert.x through META-INF/services/io.vertx.core.spi.JsonFactory, loaded from the thread-context classloader when Vert.x initializes its Json class, and the framework’s factory declares a low order() so an application-registered factory does not displace it. A shaded or uber-jar build must merge service files (Maven Shade’s ServicesResourceTransformer, or the equivalent for the packaging tool in use); dropping them silently leaves Vert.x on its own codec. A container that hides the framework from that classloader has the same effect. ownsCodec() reports the outcome, and the install step fails the boot when it is false.

Outside the installed-mapper claim. Vert.x’s DatabindCodec.createParser/fromParser static helpers, and the JacksonCodec.fromString(String)/fromBuffer(Buffer) streaming overloads that return Object, are bound to Vert.x’s own raw mapper and factory. Code that must use the installed mapper calls VertiqueJson.mapper().

Buffer decodes copy. To keep a buffer decode as strict as a string decode, the codec builds its parser from the installed mapper’s factory over a byte copy of the buffer, so JsonObject(Buffer) and Json.decodeValue(Buffer, …) transiently hold twice the payload. Bound the input: set vertx.jackson.defaultReadMaxDocumentLength and a BodyHandler body limit on network edges.

resetForTests() is a gated test seam. It restores the pre-install delegate, and it throws IllegalStateException unless the JVM was started with -Dvertique.json.codec.allowReset=true. The flag is read once when VertiqueJson is initialized, so setting the property at runtime cannot open the seam. Set it only in a build’s surefire/failsafe configuration, never for a deployed application.


Every extension below is contributed through Dagger multibinding unless stated otherwise.

Reports the health of one component. Classify each check with the @Liveness or @Readiness qualifier; HealthCheckModule declares both empty-by-default sets so a module can contribute without depending on dev.vertique:vertique-management, which is what reads them.

public interface HealthCheck {
String name();
Future<HealthCheckResult> check();
}
public final class DatabaseHealthCheck implements HealthCheck {
@Inject public DatabaseHealthCheck() {}
@Override public String name() { return "database"; }
@Override public Future<HealthCheckResult> check() {
return Future.succeededFuture(HealthCheckResult.up(Map.of("pool", "ready")));
}
}
@Provides @IntoSet @Readiness
static HealthCheck databaseHealth(DatabaseHealthCheck check) {
return check;
}

HealthCheckResult is a record (HealthStatus status, Map<String, Object> data) with the factories up(), up(Map), down(), down(String error), down(Throwable cause), and down(Map); status must not be null, and data is copied and never null. The copy is unmodifiable and keeps the supplied map’s iteration order (pass a LinkedHashMap to control the order entries appear in the probe response); a null key or value is rejected with a NullPointerException. down(String) treats a null error as “no message” and yields empty data rather than throwing, so down(throwable.getMessage()) is safe for a message-less exception. down(Throwable) puts the throwable’s message under the error key, falling back to its fully qualified class name when the message is null — and equally when getMessage() itself throws an exception, so a failure that cannot describe itself still yields a DOWN result instead of a second failure. It rejects a null cause with a NullPointerException. Return a completed future carrying a DOWN result rather than a failed future — a failed future or a thrown exception is still reported as DOWN, but with the error text instead of your diagnostic data. name() must be unique within its qualifier set and must not return null; the management endpoint reports a null name under the check’s implementing class name.

ApplicationStartupStep and ApplicationShutdownStep

Section titled “ApplicationStartupStep and ApplicationShutdownStep”

Non-verticle units of startup and teardown work.

public interface ApplicationStartupStep extends LifecycleOrdered {
Future<Void> start();
}
public interface ApplicationShutdownStep extends LifecycleOrdered {
Future<Void> stop();
}
@Singleton
public final class SchemaMigrationStep implements ApplicationStartupStep {
private final Migrator migrator;
@Inject public SchemaMigrationStep(Migrator migrator) { this.migrator = migrator; }
@Override public LifecyclePhase phase() { return LifecyclePhase.MIGRATE; }
@Override public Future<Void> start() { return migrator.migrate(); }
}
@Provides @Singleton @IntoSet
static ApplicationStartupStep schemaMigration(SchemaMigrationStep step) {
return step;
}

A step normally declares one of the non-verticle phases — CONFIGURE, VALIDATE, MIGRATE, or AFTER_START. The multibinding sets are declared by dev.vertique:vertique-deploy and driven by dev.vertique:vertique-application; core contributes into them without depending on either.

Behavior-free marker for the constructible-as-validation pattern: a class that declares its module’s required bindings as @Inject constructor parameters proves those bindings exist the moment it is constructed.

public interface ComposeValidator {}
@Singleton
public final class MyModuleComposeValidator implements ComposeValidator {
@Inject public MyModuleComposeValidator(MyRequiredService service) {
if (service.mode() != Mode.READY) {
throw new IllegalStateException("MyRequiredService must be configured in READY mode");
}
}
}
@Provides @Singleton @IntoSet
static ComposeValidator myModuleValidator(MyModuleComposeValidator v) {
return v;
}

ComposeValidationStep materializes the whole set during the VALIDATE phase, which constructs every contributed validator. A missing binding is a Dagger compile error; a violated invariant is whatever the constructor throws, reported as a failed VALIDATE-phase startup. For validators to run, the application component must include CoreLifecycleStepsModule and be driven by the standard lifecycle runner in dev.vertique:vertique-application.

String normalization (semantics-preserving) and content removal (not semantics-preserving) applied to inbound values. Implementations must be stateless and thread-safe; a Canonicalizer must also be deterministic and idempotent.

@FunctionalInterface
public interface Canonicalizer {
String canonicalize(String value, InputValueContext context);
}
@FunctionalInterface
public interface Sanitizer {
String sanitize(String value, InputValueContext context);
}

Register each implementation as a binding record keyed by its own type — the type() component is the lookup key used to resolve the classes named in @Canonicalize / @Sanitize. Declare the chain with those annotations, both @Target({TYPE, FIELD, RECORD_COMPONENT, PARAMETER, METHOD, ANNOTATION_TYPE}), so they also compose into custom annotations:

@Provides @IntoSet
static CanonicalizerBinding trimCanonicalizer(TrimCanonicalizer c) {
return new CanonicalizerBinding(TrimCanonicalizer.class, c);
}
@Path("/users") // type-level: applies to the whole resource
@Canonicalize({TrimCanonicalizer.class, NfkcCanonicalizer.class})
public class UserResource { }
public record CreateUserRequest( // component-level
@Canonicalize(TrimCanonicalizer.class)
@Sanitize(BasicHtmlSanitizer.class)
String bio) {}

A Sanitizer is registered the same way, through SanitizerBinding.

@SkipCanonicalization and @SkipSanitization opt an element out of processing inherited from an enclosing type. InputValueContext is a record (InputLocation location, String path, String logicalName, Class<?> ownerType); InputLocation is PATH, QUERY, HEADER, COOKIE, FORM, BODY, BEAN_PARAM, PAYLOAD. PAYLOAD marks values from message or protocol payloads (e.g. WebSocket messages); REST request bodies remain BODY. A custom Canonicalizer, Sanitizer, or CharacterPolicy that branches on InputLocation.BODY must also handle PAYLOAD to keep covering message-oriented inputs — WebSocket messages reported BODY before PAYLOAD existed.

path and logicalName are deliberately different views of the same value. path is the wire path — the dot-separated keys as the caller sent them — so a diagnostic points at what actually arrived. logicalName is the Java property name whenever the value came from a property the processing engine matched, and the wire name otherwise. A Canonicalizer or Sanitizer that branches on logicalName is therefore branching on the Java name, not on a @JsonProperty-renamed key; branch on path when the wire form is what matters. Collection elements carry the element path in both components.

Codec-neutral projection from a wire property name to the Java property name that declares its input policies.

@FunctionalInterface
public interface InputFieldNameResolver {
/** Returns every wire name unchanged, for transports that do not rename. */
InputFieldNameResolver IDENTITY = (ownerType, wireName) -> wireName;
String logicalName(Class<?> ownerType, String wireName);
/** Composes one owner type's projection at registration; no-op unless overridden. */
default void precompute(Class<?> ownerType) {}
/** Wire keys of ownerType that bind into a field of another type; empty unless overridden. */
default Map<String, PromotedField> promotedFields(Class<?> ownerType) {
return Map.of();
}
/** Where one wire key is bound, or null; override to apply a wire-side rule such as folding. */
default PromotedField promotedField(Class<?> ownerType, String wireName) {
return promotedFields(ownerType).get(wireName);
}
/** The Java names the codec binds wire keys of ownerType into; null when not enumerable. */
@Nullable
default Set<String> boundJavaNames(Class<?> ownerType) {
return null;
}
/** Wire keys bound into a Java property the codec cannot name; empty unless overridden. */
default Set<String> unroutableWireNames(Class<?> ownerType) {
return Set.of();
}
/** Subtypes the codec may bind in place of a polymorphic base; empty unless overridden. */
default Set<Class<?>> polymorphicSubtypes(Class<?> ownerType) {
return Set.of();
}
/** A key that arrives on one type but is bound into a field declared on another. */
record PromotedField(Class<?> declaringType, String fieldName, List<String> enclosingPath) {}
}

Policies declared with @Canonicalize / @Sanitize are keyed by the Java property name, but an intermediate parsed from the wire is keyed by whatever the codec published — @JsonProperty("user_name"), a SNAKE_CASE naming strategy, or a @JsonAlias. Without a projection between the two, a policy declared on a renamed field silently never runs. This contract is the seam that closes that gap, and declaring it here — beside InputLocation, Canonicalizer and Sanitizer — is what keeps both vertique-input-processing and vertique-sanitization free of any codec dependency: each codec-backed implementation lives in the module that already owns that codec (JacksonFieldNameResolver in dev.vertique:vertique-json).

Six properties are part of the contract:

Property Contract
Direction Wire → Java, the only direction able to express an alias’s many-to-one mapping
Totality An unrecognized wireName — an undeclared extra key, a Map-typed field’s key, or a name that is already the Java property name — is returned unchanged; an implementation never returns null and never throws
Threading and cost Stateless (or effectively immutable), reentrant, consulted once per intermediate key on the request path — it must not block and should serve every call from a precomputed projection
Warm-up precompute(Class) composes one owner type’s projection ahead of the request path; it defaults to a no-op, so a resolver that needs no per-class state is unaffected. The input-processing engine calls it at registration for every owner type it may consult, and an implementation may fail fast there — the failure is deliberately a startup failure rather than a per-request one
Bound names boundJavaNames(Class) enumerates the Java property names the codec binds wire keys of an owner into, or returns null when the projection cannot enumerate them. The engine keys metadata on the Java field name and a codec on the property name it derives from accessors; the two diverge for setStreet writing streetName, and neither side can derive that mapping. The engine checks at registration that every field carrying a declared chain is among the bound names and fails startup naming the field when it is not. IDENTITY returns null and is trusted, as is a codec that binds a type outside its declaration view. unroutableWireNames(Class) names the keys a codec binds into a property it cannot name — a creator parameter carrying only a wire name — and the engine refuses at registration an owner with such a key that declares a chain on any field, because which field the parameter writes is undecidable
Polymorphic subtypes polymorphicSubtypes(Class) reports the concrete subtypes the codec may bind in place of a polymorphic base, or the empty set. The engine resolves policy metadata from the declared type and never inspects the runtime subtype, so it asks at registration and refuses a base whose subtype declares a chain or skip the base does not carry. The default is the empty set, so a resolver that knows no polymorphism — including IDENTITY — is unaffected. An implementation answers from class annotations alone: the engine asks it of every reachable type, including ones whose property projection was never composed. A subtype discoverable only from a type id at runtime is not enumerable and is absent
Promotion promotedFields(Class) reports the keys of an owner type that the codec promoted out of a nested member, so they arrive flat while their policies are declared on the inner type — Jackson’s @JsonUnwrapped is the case it exists for. The map is keyed by the wire name, and a promoted key is never added to the projection: logicalName returns it unchanged, as its totality contract already requires, so the owner’s metadata misses and the engine falls through. promotedField(Class, String) performs the lookup, which is what keeps case folding inside the projection. PromotedField.enclosingPath() names the owner-side members traversed, so the engine descends through them and their chains and skip flags still apply. Both default to promoting nothing

The JSON pointer resolver (JakartaPathPointers) is a second consumer: it may read logicalName, boundJavaNames, promotedFields and polymorphicSubtypes on the error path, when it names the request members a constraint violation concerns, and treats a call that throws as unverifiable.

Use InputFieldNameResolver.IDENTITY for any transport whose intermediate keys are already Java property names, including every call site that processes a bare String, where there is no object whose fields could be renamed.

The engine consults promotedField only after the owner’s own metadata has no entry for a key, so an ordinary field costs nothing. Keying by the promoted field’s Java name would not work: that name is local to the inner type, so two promoted members of different types sharing a field name would collide even when a prefix gives them distinct wire keys, and a promoted key could shadow a same-named field of the owner.

PromotedField.fieldName() is the Java property name the codec binds the key into. Registration verifies through boundJavaNames that it equals the field name for every governed field of the declaring type, and verifies the enclosing path is one the engine can descend wherever routing the key would apply a policy; a promoted key that fails either is refused at startup rather than routed wrongly, as is one on a type served by a generated input processor, whose field-name switch is emitted from the owner’s own declared fields.

Validates that a string contains only permitted characters.

public interface CharacterPolicy {
CharacterPolicyResult validate(String value, InputValueContext context);
}
@Provides @IntoSet
static CharacterPolicyBinding asciiPolicy(AsciiCharacterPolicy p) {
return new CharacterPolicyBinding(AsciiCharacterPolicy.class, p);
}

Return CharacterPolicyResult.passed() or CharacterPolicyResult.failed(index, codePoint, reason) naming the first offending character. @SkipAllowedCharacters opts an element out of character validation inherited from its type.

When a policy runs from Bean Validation (the @AllowedCharacters constraint), its InputValueContext is synthetic: location() is always BODY whatever the value’s real origin, and ownerType() is always Object.class. Validation sees the value after it has been separated from its transport provenance, and one validation pass can cover values from several locations at once, so no accurate location exists to report. Do not branch on location() in a policy. Canonicalizers and sanitizers run earlier and do receive accurate provenance.

dev.vertique:vertique-resilience also owns the canonical BackoffStrategy and RetryPolicy, the small contracts used by consumer modules to calculate retry delays and determine retry eligibility.

FailureTranslator and ContextAwareFailureTranslator

Section titled “FailureTranslator and ContextAwareFailureTranslator”
@FunctionalInterface
public interface FailureTranslator<T extends Throwable> {
Throwable translate(T throwable);
}
@FunctionalInterface
public interface ContextAwareFailureTranslator<T extends Throwable> extends FailureTranslator<T> {
Throwable translate(T throwable, String context);
@Override
default Throwable translate(T throwable) {
return translate(throwable, throwable.getMessage());
}
}

Both register on a FailureMapper through on(...). Layer-specific customizer SPIs — which contribute translators into the framework’s REST, service, and database mappers through Dagger — are declared by those modules; see dev.vertique:vertique-rest-jaxrs, dev.vertique:vertique-services, and dev.vertique:vertique-db-core.

Contributes one named ObjectMapper profile.

public interface JsonMapperProfile {
JsonProfileId id();
ObjectMapper mapper();
default List<JsonSchemaTypeOverride> jsonSchemaTypeOverrides();
}

The registry that collects profiles ships in dev.vertique:vertique-json; contribute a profile through that module’s multibinding. JsonProfileId.SYSTEM ("system") is reserved for the baseline profile; vertique and vertique-strict are reserved too. mapper() returns one stable instance per profile — the same reference on every call — and callers never mutate it. A registry implementation must register all three reserved ids; no application profile may activate Jackson default typing or claim the retired vertx id.

jsonSchemaTypeOverrides() declares the schema overrides described under JSON schema overrides on a profile. It defaults to an empty list, so a profile that only implements id() and mapper() stays valid. An implementation that overrides it returns the same stable, non-null, unmodifiable list on every call and defensively copies any caller-supplied collection.

Implement these to make a typed value travel across a dispatch or durable boundary. Every one is bounded <T extends ContextValue> on its context type; the registries that collect them live in dev.vertique:vertique-context.

SPI Contract
ServiceDispatchContextEncoder<T> Class<T> type(), String key() (defaults to the FQCN), Object encode(T, ServiceDispatchEncodeContext)
ServiceDispatchContextDecoder<T> Class<T> type(), String key(), ContextDecodeResult<T> decode(Object, ServiceDispatchDecodeContext)
DurableContextMetadataEncoder<T> Class<T> type(), String namespace(), DurableMetadata encode(T, DurableEncodeContext)
DurableContextMetadataDecoder<T> Class<T> type(), String namespace(), ContextDecodeResult<T> decode(DurableMetadata, DurableDecodeContext), default boolean acceptsExplicitCarrier()
ContextValueAdapter<T> Class<T> type(), Object snapshot(T), T restoreFromSnapshot(Object), default T duplicate(T)
InboundContextInitializer ContextHolder.Scope initialize(InboundContextInitializationContext)
public record RequestLabelContext(String label) implements ContextValue {}
public final class RequestLabelDispatchEncoder
implements ServiceDispatchContextEncoder<RequestLabelContext> {
@Inject public RequestLabelDispatchEncoder() {}
@Override public Class<RequestLabelContext> type() { return RequestLabelContext.class; }
@Override public Object encode(RequestLabelContext value, ServiceDispatchEncodeContext context) {
return value.label();
}
}

ScopeContext is partition vocabulary eligible for the same explicit holder write path. This core foundation does not register a scope encoder; the conditional default durable codec pair is supplied by vertique-context’s ContextRuntimeModule.

Decoders report problems by returning ContextDecodeResult.failure(warnings) rather than throwing; the record is (Optional<T> value, List<ContextDecodeWarning> warnings) with the factories empty(), of(value), and failure(warnings). ContextDecodeWarning is a record (String key, String value, String reason).

DispatchBoundary supplies the boundary identifiers an encoder or decoder can branch on: MCP, SERVICE_DISPATCH, KAFKA, OUTBOX, OUTBOX_SERVICE, DELAYED_JOB, WORKFLOW, CAMEL. The same class also names the ingress kinds an entry point binds as the request’s ambient invocation origin for authorization — REST ("rest") and WEBSOCKET ("websocket") alongside the boundary identifiers above — so a transport, an encoder, and a policy all read one vocabulary.


vertique-core reads no configuration section of its own. It defines the contract every other module’s configuration is parsed through.

The root configuration object is bound once, qualified @VertxConfig, by VertxModule. Read it only in a @Provides method, navigate to your section, and parse it through an injected ConfigParser:

@Provides
@Singleton
static HelloConfig helloConfig(@VertxConfig JsonObject config, ConfigParser parser) {
return parser.parse(JsonConfigPaths.navigateObject(config, "hello"), HelloConfig.class);
}

The ConfigParser binding is provided by ConfigParsingModule in dev.vertique:vertique-config-core; include it once in the application component. Never use JsonObject.mapTo or a hand-rolled ObjectMapper at this boundary — the config mapper is isolated from the Vert.x and REST mappers and is deliberately coercion-lenient, so "port": "9090" binds whether or not another mapper in the process has been tuned strict.

Method Behavior
<T> T parse(JsonObject section, Class<T> type) Parses one section into a typed record. A null/empty section yields the type’s default shape. Throws ConfigurationException on failure.
<T> List<T> parseKeyedObject(JsonObject section, String identityProp, Class<T> elementType) Parses a section that is a keyed object {key:{…}}, injecting each entry key into the named identity property.
<T> List<T> parseKeyedObject(JsonObject section, String identityProp, Class<T> elementType, Map<String, Object> fixedProps) Same, plus constant properties injected into every element before deserialization — needed when a record’s compact constructor requires fields beyond the key.
// Field-keyed: @KeyedBy on the List<T> component of the parsed record
KafkaConfig kafka = parser.parse(JsonConfigPaths.navigateObject(config, "kafka"), KafkaConfig.class);
// Section-root-keyed: the keys are the section itself
List<RestClientConfig> clients = parser.parseKeyedObject(
JsonConfigPaths.navigateObject(config, "restClient"), "name", RestClientConfig.class);
// With fixed props for a record whose identity is a (type, name) pair
List<ServiceConfig> services =
parser.parseKeyedObject(typeGroup, "name", ServiceConfig.class, Map.of("type", typeKey));

parse(null, T) is safe and returns the type’s Jackson default shape; parseKeyedObject(null, …) returns List.of(). Open property bags whose schema the framework does not own must be read directly from the source JsonObject and attached afterwards.

Annotation (@Target({FIELD, METHOD}), in dev.vertique.core.json) on a List<T> record component whose external JSON is a keyed object. The object key is injected into the named identity property of each element. On a class bound through a builder (a Lombok @Builder @Jacksonized class, for example) Jackson reads the annotation from the builder setter rather than the field, so place it on a hand-written builder method that takes the list; a field annotation is not seen there.

// External JSON: { "consumers": { "orders": {...}, "refunds": {...} } }
record KafkaConfig(@KeyedBy("name") List<KafkaConsumerConfig> consumers) {}

The element type must declare a settable property matching the annotation value. A conflict between an explicit JSON value and the injected key is a ConfigurationException; equal values are accepted. The annotated value itself must be a JSON object: a scalar or an array, including an empty array, is rejected, while an empty object binds to an empty list. The deserializer backing it ships in dev.vertique:vertique-json.

Method Semantics
navigateObject(JsonObject root, String... segments) Tolerant subtree traversal: returns an empty JsonObject when a segment key is absent, and skips blank/null segments. Throws ConfigurationException when a segment key is present but bound to a non-object — a scalar, an array, or explicit JSON null.
resolve(JsonObject root, String dottedPath) Strict dotted-path lookup returning a LookupResult(status, value, path, failingSegment).

LookupStatus is PRESENT, MISSING, or INVALID_SHAPE. INVALID_SHAPE means traversal crossed a non-object intermediate segment. PRESENT covers any leaf, including a JsonObject or JsonArray — a caller expecting a scalar must check.

Dagger @Qualifier for overriding the ObjectMapper that backs config parsing. Bind an @ConfigMapper ObjectMapper anywhere in the application component; the config module reads it optionally and re-layers the framework’s mandatory modules and lenient policy over it before first use.

The seam is entirely optional — an application that binds nothing gets the framework’s lenient default. Because the framework finalizes the supplied mapper in place, do not share that instance concurrently for any other purpose.

Shared redactor so every module’s config toString() masks the same things. MASK is "***", and the input is never mutated — masking is applied to a copy used only for the returned string.

Method Behavior
isSensitivePath(String dottedPath) true when the lower-cased path contains password, secret, token, passphrase, credential, jaas.config, user.info, private.key, keystore, or truststore; or when its separator-stripped, lower-cased form contains apikey, accesskey, or secretkey.
redactBag(JsonObject bag) Renders a bag as a log-safe String, descending through nested objects and arrays and masking every leaf whose full dotted path is sensitive.
redactUri(String uri) Masks authority userinfo (user:pass between // and @) and the value of any credential-bearing query parameter. Best-effort; never throws.

Matching is substring-based in both forms, so ssl.trustStoreOptions.password and aws.access-key are recognized at any depth.

Builds the nested JsonObject the framework consumes from the flat dotted/bracketed keys an embedding host exposes. build(Map<String, String> flatKeys) is purely syntactic — every leaf is stored as a String, and type coercion is ConfigParser’s job downstream.

Segment form Interpretation
Bare segment, including all-digit (2026) Object key
[N], N a non-negative integer Array index
[content], quoted or containing a dot Literal map key (dots preserved, surrounding quotes stripped)
a.b=1, a.c=2 → {"a":{"b":"1","c":"2"}}
years.2026.total=5 → {"years":{"2026":{"total":"5"}}}
servers[0].host=h, servers[1].host=k → {"servers":[{"host":"h"},{"host":"k"}]}
audit.bindings[http.server].dim[0]=x → {"audit":{"bindings":{"http.server":{"dim":["x"]}}}}

Fail-fast rules, all raising ConfigurationException: array indices must be contiguous from 0 with no gap, duplicate, or mixing of [N] with object keys at the same node; a path used as both a leaf and a parent collides; a null input map is rejected. Every message names offending keys/paths only, never values. Input order is irrelevant — keys are processed in a stable sorted order, so collision detection does not depend on map iteration order. A null value for a key is stored as JSON null.

Immutable record evaluated at runtime by code generated from @ConditionalOnProperty. It is public and stable, but it is not for hand-written Dagger bindings — the annotation is codegen-only.

public record PropertyCondition(String name, String havingValue, boolean matchIfMissing) {
public static boolean matchesAll(JsonObject config, PropertyCondition[] conditions);
}
Component Annotation default Meaning
name — (required) Dotted config property path, e.g. feature.adminApi.enabled
havingValue "true" Expected scalar; compared with String.valueOf(value).equals(havingValue)
matchIfMissing false When true, a missing path counts as a match

matchesAll ANDs the array and returns true for an empty or null array. A MISSING path returns false unless matchIfMissing is set, in which case evaluation continues. An INVALID_SHAPE path, or a present leaf that is a JsonObject/JsonArray, throws ConfigurationException.


Failures, Constraints, and Common Mistakes

Section titled “Failures, Constraints, and Common Mistakes”
VertiqueException (RuntimeException)
├── ValidationException — invalid input or data
│ ├── BusinessRuleException — business/domain-rule violation
│ │ └── DurableEncodeRejectedException — a durable-context encoder rejected the whole
│ │ capture/merge operation
│ └── MalformedDurableMetadataException — durable carrier or namespace body present but not a
│ JSON object
├── ConflictException — resource state conflict
├── NotFoundException — resource absent
├── ConfigurationException — startup/wiring/contract error
├── TechnicalException — runtime/infrastructure failure
│ └── UnavailableException — capability currently unavailable
├── TooManyRequestsException — rate/quota limit exceeded (429 analogue of
│ UnavailableException's 503 role) — extends
│ VertiqueException directly, never ValidationException/
│ BusinessRuleException, so an application's own
│ ExceptionMapper<ValidationException> can never
│ out-rank the 429 default
└── VertiqueSecurityException — grouping root for the security family
├── UnauthorizedException — authentication required or credential invalid
└── ForbiddenException — authenticated but not authorized

Every root is concrete — it may be thrown directly or subclassed. The one exception is VertiqueSecurityException: catch it to handle any security denial, but throw UnauthorizedException or ForbiddenException instead of the grouping root. Prefer the most specific applicable root; a new module exception should never extend raw RuntimeException.

The default mapper in dev.vertique:vertique-rest-jaxrs registers these core types:

Exception Status
ValidationException (and BusinessRuleException, BeanValidationException) 400
UnauthorizedException 401
ForbiddenException 403
NotFoundException 404
ConflictException 409
TooManyRequestsException 429
UnavailableException 503
Throwable (fallback) 500

Mapping is hierarchy-aware, so a subclass inherits its nearest registered ancestor’s status. TechnicalException, ConfigurationException, and a bare VertiqueSecurityException have no explicit registration and therefore reach the Throwable fallback as 500. ForbiddenException is registered by fully-qualified name to avoid clashing with jakarta.ws.rs.ForbiddenException. The REST defaults reference only core, JAX-RS, and REST-owned types — a database exception that escapes a repository surfaces as 500, so translate it to a core semantic type at your service boundary.

EventBusExceptionMapper translates Vert.x ReplyExceptions; the set of reply failures is a fixed enum, so this mapper is not extensible. Non-ReplyException throwables pass through unchanged.

Reply failure Translated to Root
TIMEOUT EventBusTimeoutException TechnicalException
NO_HANDLERS EventBusAddressUnavailableException UnavailableException
RECIPIENT_FAILURE, ERROR EventBusDispatchException TechnicalException

Each carries address(). Service-layer callers receive enriched subclasses from dev.vertique:vertique-services that add the failing contract.

The resilience annotation semantics, declaration fields, and retry bounds are documented by dev.vertique:vertique-resilience and the consumer modules that enforce them. Core does not own the runtime or annotation vocabulary.

  • Do not read the root @VertxConfig JsonObject outside a boundary provider. Module internals take typed config records. Do not disambiguate same-typed bindings with @Named — declare a dedicated @Qualifier.
  • Do not use priority to emulate cross-phase ordering. Phase dominates priority absolutely in both ordering contracts.
  • Give lambda-registered extensions a stable identity. The default orderKey() of a lambda or method reference is a synthetic class name that is not stable across compilations, so ties break unpredictably. Override orderKey() or assign distinct priority() values.
  • The dispatch codecs are local-only. LocalMessageCodec throws UnsupportedOperationException from both wire methods, so a clustered event bus fails loudly rather than degrading silently; transform returns the object reference unchanged, which means no defensive copy occurs on a local hop. Registration of the dispatch.envelope and dispatch.result codec names is owned by whichever deployment path an application installs — dev.vertique:vertique-services, dev.vertique:vertique-kafka-core, or dev.vertique:vertique-job-delayed — each tolerating an already-registered name. Core itself registers neither, so EventBusClient is usable only once one of those modules has deployed.
  • PayloadSources never copies. Copy the array yourself before calling buffered(byte[], …) if you need immutability, and prefer bufferedStream() over bufferedView() on hot paths for non-Buffer backings.
  • A VerticleDeployment requires a verticle phase. Constructing one with CONFIGURE, VALIDATE, MIGRATE, or AFTER_START is rejected at construction time.
  • Do not capture VertiqueJson.mapper() in a Dagger provider or a static initializer. Both run before the CONFIGURE phase installs the system profile, so the captured instance is Vert.x’s raw mapper. Read it at use time, or capture it in an object built at CONFIGURE or later.
  • Do not configure Jackson by mutating a shared mapper. Contribute a JsonMapperProfile and select it through json.systemProfile and/or json.jsonProfile. Jackson forbids reconfiguring a mapper after first use, and the process codec has already parsed the application’s configuration by the time any startup step runs.
  • Do not implement MethodMetadata or ParameterMetadata in application code. They are provided by generated code and by the framework’s own scanners.
  • Never log a secret-bearing config record without redaction. Route the value through ConfigSecretRenderer in the record’s toString() rather than relying on @JsonIgnore, which on a record would also block deserialization and make the value unreadable from configuration.

vertique-core depends on no other Vertique module — it is the root of the dependency graph, and every compile-scope dependency it declares lands on every consumer’s classpath.

Dependency Why
io.vertx:vertx-core Vertx, EventBus, Future, Buffer, JsonObject — the substrate every contract here is expressed in
com.google.dagger:dagger @Module, @Provides, @Multibinds for VertxModule, HealthCheckModule, CoreLifecycleStepsModule
jakarta.inject:jakarta.inject-api @Inject, @Singleton, @Qualifier, Provider on the injectable types and qualifiers
jakarta.annotation:jakarta.annotation-api @Nullable on nullable record components, SPI parameters, and return values
jakarta.ws.rs:jakarta.ws.rs-api Declared at compile scope; core’s own sources do not reference it, so every consumer receives the JAX-RS API on its classpath transitively
com.fasterxml.jackson.core:jackson-databind ObjectMapper on the profile contracts and the process codec, and the serialization annotations on result records
org.slf4j:slf4j-api Logging in VertiqueJson and the other core collaborators
org.projectlombok:lombok provided scope, so it never reaches runtime; core’s own sources do not use it