Services
View MarkdownVertique 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.
@ServiceContractnames 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
@ServiceOperationnames 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@Bulkheadfromdev.vertique.resilience.annotationapply per contract or per method. On a concrete Dagger-managed handler, put@Resilienton 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;maxRetriesaccepts0through100, 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.
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.
Generate the application
Section titled “Generate the application”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.
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=falseExpected result: BUILD SUCCESS, and a new services-app directory containing:
services-app/AGENTS.mdservices-app/README.mdservices-app/config/application.jsonservices-app/pom.xmlservices-app/src/main/java/com/example/servicesapp/AppComponent.javaservices-app/src/main/java/com/example/servicesapp/AppModule.javaservices-app/src/main/java/com/example/servicesapp/package-info.javaservices-app/src/main/java/com/example/servicesapp/service/GreetingClient.javaservices-app/src/main/java/com/example/servicesapp/service/GreetingService.javaservices-app/src/main/java/com/example/servicesapp/service/GreetingServiceImpl.javaservices-app/src/main/java/com/example/servicesapp/service/package-info.javaservices-app/src/test/java/com/example/servicesapp/ApplicationIT.javaIts pom.xml declares vertique-starter-services and vertique-launcher in production scope.
vertique-application-test and junit-jupiter provide the test harness.
Define a service contract
Section titled “Define a service contract”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;
@Singletonpublic 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.
Include the generated Dagger module
Section titled “Include the generated Dagger module”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 and call the service
Section titled “Inject and call the service”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);Add resilience to a service handler
Section titled “Add resilience to a service handler”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.
Run the tests
Section titled “Run the tests”Working directory: services-app/.
cd services-appmvn -ntp verifyExpected 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.
Run the application
Section titled “Run the application”Working directory: services-app/.
mvn -ntp exec:javaThis 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):
curl -s http://localhost:9090/health/readyExpected 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.
Threading
Section titled “Threading”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.
Learn more
Section titled “Learn more”vertique-servicesmodule reference:@ServiceContract,@ServiceOperation, resilience policy annotations, andServiceClientFactory.vertique-resiliencemodule reference: canonical resilience annotations, runtime policies, execution budgets, and observer events.vertique-codegen-resiliencemodule reference: compile-time anchor, placement, policy-name, and proxyability validation.vertique-codegen-servicesmodule reference: the annotation processor that generatesGeneratedServicesModule, typed-client bindings, and each{Contract}_ContractContributor.vertique-starter-servicesmodule reference: the three modulesServicesApplicationModulecomposes.- Vertique services application archetype: the archetype’s own reference, including which dependencies the generated project declares.
- Quickstart
- Application model
- What is Vertique?
Continue reading
Section titled “Continue reading”- Previous: JAX-RS compatibility
- Next: MCP