Skip to content

Vertique Services is the framework’s contract-based execution model: define a capability as a typed Java interface, inject it like any other dependency, and an ordinary method call dispatches over the Vert.x event bus into a managed service verticle.

  • Contracts are plain interfaces. @ServiceContract names the capability; the compile-time generator produces the Dagger bindings and dispatch metadata, and you name one generated module in your component.
  • Context travels with the call. Registered context values are captured at the call site and restored while the implementation runs on the other side of the event bus.
  • Stable operation identities. Explicit @ServiceOperation names give Kafka listeners, cron and delayed jobs, workflows, and outbox relays durable targets, and event-bus addresses do not leak into application code.
  • Resilience follows the entry point. On a service-contract interface, @Timeout, @Retry, @CircuitBreaker, and @Bulkhead from dev.vertique.resilience.annotation apply per contract or per method. On a concrete Dagger-managed handler, put @Resilient on the method to activate the generated AOP path, then add the declaration annotations or select a named policy. A declaration on a concrete method without the anchor is a compile error. A timeout is per supplier attempt; maxRetries accepts 0 through 100, and retry is appropriate only when the operation is replay-safe or idempotent.
  • Scale per contract. Each contract has its own configured verticle-instance count. Instances share the implementation object, so scaling adds concurrent execution and does not give each instance isolated state. Implementations stay safe for concurrent calls.
  • Non-blocking by default. Implementations run on the Vert.x event loop. Worker mode is an explicit opt-in for blocking work.

A typed service contract flows through a generated Dagger client binding, context-aware event-bus dispatch, and managed service verticle instances

By the end of this page you will have generated a Vertique services application, defined a contract, injected it into application code, and called it through the generated execution path.

vertique-archetype-services and the starter it depends on are published to Maven Central at 0.3.0, so archetype:generate resolves them without a local checkout.

Run this command from an empty directory of your choice. It creates a new services-app project inside it. VERTIQUE_VERSION is declared once and reused for both the archetype version and the generated project’s vertiqueVersion property, matching Quickstart’s convention.

Terminal window
VERTIQUE_VERSION=0.3.0
mvn -B -ntp archetype:generate \
-DarchetypeGroupId=dev.vertique \
-DarchetypeArtifactId=vertique-archetype-services \
-DarchetypeVersion=$VERTIQUE_VERSION \
-DgroupId=com.example \
-DartifactId=services-app \
-Dversion=0.1.0-SNAPSHOT \
-Dpackage=com.example.servicesapp \
-DvertiqueVersion=$VERTIQUE_VERSION \
-DinteractiveMode=false

Expected result: BUILD SUCCESS, and a new services-app directory containing:

services-app/AGENTS.md
services-app/README.md
services-app/config/application.json
services-app/pom.xml
services-app/src/main/java/com/example/servicesapp/AppComponent.java
services-app/src/main/java/com/example/servicesapp/AppModule.java
services-app/src/main/java/com/example/servicesapp/package-info.java
services-app/src/main/java/com/example/servicesapp/service/GreetingClient.java
services-app/src/main/java/com/example/servicesapp/service/GreetingService.java
services-app/src/main/java/com/example/servicesapp/service/GreetingServiceImpl.java
services-app/src/main/java/com/example/servicesapp/service/package-info.java
services-app/src/test/java/com/example/servicesapp/ApplicationIT.java

Its pom.xml declares vertique-starter-services and vertique-launcher in production scope. vertique-application-test and junit-jupiter provide the test harness.

GreetingService is a plain Java interface, annotated with @ServiceContract to give it a contract identity and @ServiceOperation to give its one operation a stable identity:

package com.example.servicesapp.service;
import dev.vertique.services.ServiceContract;
import dev.vertique.services.ServiceOperation;
import io.vertx.core.Future;
@ServiceContract(namespace = "sample", value = "greeting")
public interface GreetingService {
// "greet" is already the default runtime name; the annotation makes it a stable target.
@ServiceOperation("greet")
Future<String> greet(String name);
}

GreetingServiceImpl implements it directly and is the only implementation the generated project contributes:

package com.example.servicesapp.service;
import io.vertx.core.Future;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@Singleton
public final class GreetingServiceImpl implements GreetingService {
@Inject
public GreetingServiceImpl() {}
@Override
public Future<String> greet(String name) {
return Future.succeededFuture("Hello, " + name + "!");
}
}

namespace = "sample" and value = "greeting" identify the service. When @ServiceOperation is omitted, the runtime operation name defaults to the Java method name, so greet still dispatches to services/sample/greeting/greet. Declaring @ServiceOperation("greet") makes that operation name an explicit stable identity, allowing Kafka, cron, delayed jobs, workflows, and outbox relays to persist and resolve it. Add a second method the same way when it also needs a durable target. Ordinary typed service calls do not require the annotation.

The services code generator creates GeneratedServicesModule. Add it to AppComponent alongside ServicesApplicationModule. This registers the service implementation and makes the GreetingService contract injectable without an application-owned provider.

@VertiqueApp
@Singleton
@Component(
modules = {
ServicesApplicationModule.class,
AppModule.class,
GeneratedServicesModule.class
})
interface AppComponent extends VertiqueApplicationComponent {
GreetingClient greetingClient();
}

Inject GreetingService like any other Dagger dependency and call its typed methods. Vertique handles dispatch and context propagation behind the contract:

public final class GreetingClient {
private final GreetingService greetings;
@Inject
public GreetingClient(GreetingService greetings) {
this.greetings = greetings;
}
public Future<String> greet(String name) {
return greetings.greet(name);
}
}
// A Dagger component can expose the injected client for application code.
GreetingClient greetingClient = component.greetingClient();
String greeting = greetingClient.greet("Vertique")
.toCompletionStage()
.toCompletableFuture()
.get(10, TimeUnit.SECONDS);
assertEquals("Hello, Vertique!", greeting);

The service-contract and concrete-handler paths are intentionally different:

Entry point Resilience behavior
@ServiceContract interface Services reads @Timeout, @Retry, @CircuitBreaker, and @Bulkhead declarations directly, without a subclass proxy.
Concrete ServiceHandler bean @Resilient is the method-level aspect anchor. The AOP processor generates the proxy, and the declaration annotations supply the pipeline values.

The public vertique-example-services example uses a named policy on a concrete handler. This abridged method is copied from ResilienceProbeServiceHandler. Its one-millisecond delays keep the integration proof short and are not a production backoff recommendation:

@Resilient(policy = "probe")
@Retry(maxRetries = 2, delayMs = 1, maxDelayMs = 1)
public Future<String> probe(String key) {
int attempt = attemptsByKey
.computeIfAbsent(key, ignored -> new AtomicInteger())
.incrementAndGet();
if (attempt < 5) {
return Future.failedFuture("transient failure");
}
return Future.succeededFuture("ok:" + key);
}

@Resilient(policy = "probe") selects the resilience.policies.probe tier described in Configuration. An anchor with no policy name must have at least one resilience declaration. An anchor-only method with neither is rejected at compile time. An unknown policy name fails application startup; it does not silently disable the operation.

Install ResilienceModule and ResilienceAopModule in the application component. Add ResiliencePoliciesModule when the application supplies named tiers, and include the generated GeneratedAopModule alongside the generated services module. The example’s AppComponent shows the complete composition. Applications inheriting vertique-app-parent receive the AOP and resilience processors through the standard processor facade; custom-parent applications use the vertique-codegen-all recipe.

Working directory: services-app/.

Terminal window
cd services-app
mvn -ntp verify

Expected result: BUILD SUCCESS, with the test summary reporting Tests run: 3, Failures: 0, Errors: 0, Skipped: 0. invokesGreetingThroughTypedProxy calls the injected GreetingService through the generated client binding; the remaining tests cover the event-loop default and working directory configuration.

Working directory: services-app/.

Terminal window
mvn -ntp exec:java

This starts the application in the foreground and keeps running until you stop it, so leave this terminal open. Confirm that the managed GreetingService deployment is ready from a separate terminal through the management port (9090 by default):

Terminal window
curl -s http://localhost:9090/health/ready

Expected result:

{"status":"UP","checks":[{"name":"services","status":"UP","data":{"greeting":"UP"}}]}

Return to the terminal running mvn -ntp exec:java and press Ctrl+C to stop it.

Service implementations run on the non-blocking Vert.x event loop by default, and GreetingServiceImpl above is written for it: it returns an already-completed Future and never blocks. The instances value scales this service contract’s verticle independently of other services in the application. The generated config/application.json reflects both choices: its services.contracts.sample.greeting section sets instances and has no worker opt-in:

{
"management": { "port": 9090, "enabled": true },
"services": {
"contracts": {
"sample": {
"greeting": { "instances": 2 }
}
}
}
}

As Configuration explains, mvn -ntp exec:java resolves config/ against the running process’s own working directory, so a run from services-app/ reads this file. To move the service to a worker thread, set services.contracts.sample.greeting.worker to true in it. Do that only once GreetingServiceImpl is changed to perform blocking work, such as a JDBC call, a filesystem read, or CPU-bound computation. Worker mode is an opt-in for blocking implementations and is not the default execution model. The generated project’s own README.md documents this alongside its run, verify, package, and container-image commands.

Service policy is enforced server-side by the common vertique-resilience runtime. JSON configuration can override annotation values for an environment, but it does not turn resilience on for an operation with no annotation. Durable jobs and Redis deadline coordination keep their own delivery/deadline semantics and do not install a service resilience pipeline implicitly.