A modular HTTP client toolkit for Java 17+ with fluent and declarative APIs, pluggable transports, composable decorators, and support for sync, async, and reactive applications.
Java has no shortage of HTTP clients. But each one forces a trade-off:
- JDK HttpClient - low-level, no serialization, no interceptors, no declarative API
- Spring WebClient / RestClient - optimized for Spring applications and programming models
- Quarkus REST Client - tightly aligned with Quarkus and JAX-RS-style declarative clients
- OkHttp / Apache HttpClient - transport only, you build everything else yourself
- Feign - declarative only, no fluent API, limited reactive support
Ark gives you one client model that survives framework changes, transport changes, and execution-model changes.
Ark separates the concerns that other clients bundle together:
| Concern | Ark's approach |
|---|---|
| How to build requests | Fluent API or declarative interfaces - your choice |
| How to send them | Pluggable transports - JDK, Reactor Netty, Vert.x, Apache |
| How to serialize | Pluggable serializers - Jackson, JSON-B, or your own |
| How to execute | Sync, async, Reactor, Mutiny, Vert.x Future - same API |
| How to compose behavior | Decorators chain via transport.with(...) - retry, metrics, your own |
| Where to run | Spring Boot, Quarkus, or standalone - same code |
One mental model. Any stack. No lock-in.
- One client model across stacks - use Ark in Spring, Quarkus, and plain Java
- Fluent when you want control - explicit request composition with full access to headers, params, and body
- Declarative when you want contracts -
@RegisterArkClientwith Spring@HttpExchangeor JAX-RS annotations - Transport-agnostic - plug in JDK, Reactor Netty, Vert.x, or Apache HttpClient
- Execution-model aware - sync, async, Reactor, Mutiny, and Vert.x Future
- Generic
Transport<R>contract - every execution model implements the same interface parameterized on its return wrapper - Composable decorators -
transport.with(Retry.of(policy, ops))chain works across all 5 models; bring your own (Metrics,Cache,CircuitBreaker) - Production-ready features - TLS, retry, redacted logging, typed exceptions, per-client config
- Async stacktraces include the caller site - no more "lost" call frames in
CompletableFuturefailures - Native-image friendly - designed to work well in GraalVM-based deployments
Ark supports multiple ways to define HTTP clients.
Use the fluent API when you want full control over request composition.
import org.springframework.http.MediaType; // or jakarta.ws.rs.core.MediaType
import xyz.juandiii.ark.core.Ark;
import xyz.juandiii.ark.core.ArkClient;
import xyz.juandiii.ark.jackson.JacksonSerializer;
import xyz.juandiii.ark.transport.jdk.ArkJdkSyncTransport;
import java.net.http.HttpClient;
Ark client = ArkClient.builder()
.serializer(new JacksonSerializer(new ObjectMapper()))
.transport(new ArkJdkSyncTransport(HttpClient.newBuilder().build()))
.baseUrl("https://api.example.com")
.build();
User user = client.get("/users/1")
.accept(MediaType.APPLICATION_JSON_VALUE)
.retrieve()
.body(User.class);Define an interface with @RegisterArkClient and inject it directly:
@RegisterArkClient(baseUrl = "${api.users.url}")
@HttpExchange("/users")
public interface UserApi {
@GetExchange("/{id}")
User getUser(@PathVariable String id);
}// Spring
public UserController(UserApi userApi) { ... }
// Quarkus
@Inject UserApi userApi;Supports Spring @HttpExchange and JAX-RS @Path/@GET/@POST annotations.
If any method on the interface returns CompletableFuture<T>, the Spring starter auto-wires an AsyncArkClient proxy instead of ArkClient. Zero extra configuration:
@RegisterArkClient(configKey = "users-api")
@HttpExchange("/users")
public interface UserApi {
@GetExchange("/{id}")
CompletableFuture<User> getUser(@PathVariable String id); // ← async
}IDE hint: if IntelliJ reports
Could not autowire. No beans of 'UserApi' type found., the bean does exist at runtime — Ark registers it dynamically. Add@org.springframework.stereotype.Componenton the interface alongside@RegisterArkClientto silence the warning. Spring's default scan skips interfaces, so no double-registration. See docs/spring-boot.md → IDE autowiring hint.
@RegisterArkClient(baseUrl = "${api.users.url}")
@Path("/users")
@Produces("application/json")
public interface UserApi {
@GET
@Path("/{id}")
User getUser(@PathParam("id") String id);
}// Quarkus
@Inject UserApi userApi;| Attribute | Default | Description |
|---|---|---|
configKey |
"" |
Key for per-client config in application.properties |
baseUrl |
"" |
Base URL, supports ${property} placeholders |
httpVersion |
HTTP_2 |
HTTP/1.1 or HTTP/2 |
connectTimeout |
10 |
Connection timeout (seconds) |
readTimeout |
30 |
Read timeout (seconds) |
interceptors |
{} |
Interceptor classes (auto-detects Request/Response) |
Every Transport<R> exposes a .with(...) method that composes decorators. Built-in decorator: Retry<R>. Custom decorators (metrics, caching, circuit breaker) plug in the same way.
import xyz.juandiii.ark.core.http.decorator.Retry;
import xyz.juandiii.ark.core.http.decorator.SyncRetryOps;
Transport<RawResponse> resilient = new ArkJdkSyncTransport(HttpClient.newBuilder().build())
.with(Retry.of(retryPolicy, new SyncRetryOps()))
// .with(MyMetrics.of(registry))
// .with(MyCache.of(store));
Ark client = ArkClient.builder()
.serializer(serializer)
.transport(resilient)
.baseUrl("https://api.example.com")
.build();The chain composes outside-in — the last .with(...) is the outermost wrapper. RetryOps<R> strategies exist per execution model: SyncRetryOps, AsyncRetryOps, ReactorRetryOps, MutinyRetryOps, VertxRetryOps. See Retry & Backoff for ordering rules (e.g., Metrics outside Retry measures total wall-clock; inside, per-attempt).
- Java 17+
- Fluent HTTP API
- Declarative HTTP clients with Spring
@HttpExchangeor JAX-RS@Path/@GET @RegisterArkClientfor zero-boilerplate auto-registration and injection (sync + async)- Generic
Transport<R>contract unified across all 5 execution models - Composable
.with(...)decorator chain (built-inRetry; bring your own) - Pluggable transports (JDK, Reactor Netty, Vert.x, Apache HttpClient 5)
- Pluggable serializers (Jackson, Jackson Classic, JSON-B)
- Dedicated sync, async, Reactor, Mutiny, and Vert.x APIs
- Type-safe per-client configuration (
ArkProperties/@ConfigMapping) - Per-client interceptors and default headers via config
- Retry with exponential backoff and jitter (
Retry<R>decorator) - Async stacktraces preserve the caller site (suppressed exception, no lost frames)
- TLS/SSL support (Spring SSL Bundles, Quarkus TLS Registry)
- Trust-all SSL for development (with runtime warning)
- Request/response logging with sensitive-header and credential-body redaction (
NONE,BASIC,HEADERS,BODY) - Typed exception hierarchy (400-504 mapped to specific exceptions)
- Permissive error handling — opt out of throw-on-4xx/5xx per request
(
.noThrow()) or at the client level (throwOnError(false)). Useful when 4xx is business semantics (e.g. 404 = not found, not an error). - Raw response access —
.raw()on every*ClientResponse, or declareRawResponseas a proxy method return type. Bypasses deserialization and auto-disables throw-on-error — useful for inspecting error bodies or non-JSON responses. - Per-request timeout support
- HTTP/2 by default
- Spring Boot (sync + async + WebFlux) and Quarkus integration
- GraalVM native image support
- Easy to test and mock
Ark provides dedicated entry points for different execution models while preserving a consistent client experience.
| Model | Client | Return Type |
|---|---|---|
| Sync | ArkClient |
T |
| Async | AsyncArkClient |
CompletableFuture<T> |
| Reactor | ReactorArkClient |
Mono<T> |
| Mutiny | MutinyArkClient |
Uni<T> |
| Vert.x | VertxArkClient |
Future<T> |
Same fluent API - only the return type changes:
User user = client
.get("/users/1")
.retrieve()
.body(User.class);
CompletableFuture<User> cf = asyncClient
.get("/users/1")
.retrieve()
.body(User.class);
Mono<User> mono = reactorClient
.get("/users/1")
.retrieve()
.body(User.class);
Uni<User> uni = mutinyClient
.get("/users/1")
.retrieve()
.body(User.class);
Future<User> future = vertxClient
.get("/users/1")
.retrieve()
.body(User.class);Import the BOM first:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-bom</artifactId>
<version>1.0.7</version> <!-- ark-bom -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Then choose the modules you need.
<dependencies>
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-core</artifactId>
</dependency>
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-jackson</artifactId>
</dependency>
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-transport-jdk</artifactId>
</dependency>
</dependencies>For async support:
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-async</artifactId>
</dependency>For Reactor support:
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-reactor</artifactId>
</dependency>
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-transport-reactor</artifactId>
</dependency>For Mutiny support:
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-mutiny</artifactId>
</dependency>
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-transport-vertx-mutiny</artifactId>
</dependency>For Vert.x Future support:
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-vertx</artifactId>
</dependency>
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-transport-vertx</artifactId>
</dependency>For Spring Boot:
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-spring-boot-starter</artifactId>
</dependency>For Spring WebFlux:
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-spring-boot-starter-webflux</artifactId>
</dependency>For Quarkus (Jackson):
<dependency>
<groupId>xyz.juandiii</groupId>
<artifactId>ark-quarkus-jackson</artifactId>
</dependency>Auto-configures JsonSerializer (Jackson 2.x), ArkClient.Builder (sync), and MutinyArkClient.Builder (reactive) as CDI beans.
Ark uses a bridge pattern.
The transport layer is a thin adapter around an already configured HTTP client. Ark does not own connection pools, low-level HTTP tuning, or TLS setup. Those concerns stay in the underlying transport.
All transports implement a single generic contract Transport<R> where R is the return-type wrapper for the execution model. Decorators compose via transport.with(...).
Built-in transports include:
- JDK
HttpClient— split intoArkJdkSyncTransport(sync) andArkJdkAsyncTransport(CompletableFuture); both can share the same underlyingHttpClientfor a shared connection pool - Reactor Netty (
ArkReactorNettyTransport) - Vert.x Web Client (
ArkVertxFutureTransport) - Vert.x Mutiny Web Client (
ArkVertxMutinyTransport) - Apache HttpClient 5 (
ArkApacheTransport)
You can also provide your own transport implementation or custom decorator — see Transport Model.
Ark logs requests and responses via LoggingInterceptor (sensitive headers and known credential body keys are redacted). Enable it per-client with ark.logging.level=BASIC|HEADERS|BODY (Spring / Quarkus) or programmatically via LoggingInterceptor.apply(builder, Level.HEADERS).
For raw wire-level transport debugging, enable the underlying client's own logger:
- JDK HttpClient:
-Djdk.httpclient.HttpClient.log=all - Apache HttpClient 5: set
org.apache.hc.client5.httpto DEBUG - Reactor Netty: set
reactor.netty.http.client.HttpClientto DEBUG - Vert.x WebClient: set
io.vertx.core.http.implto DEBUG
Found a vulnerability? Please follow the disclosure process in SECURITY.md — do not open a public issue.
Ark validates TLS certificates by default. To use a custom truststore (self-signed CA, mutual TLS), configure your SSL bundle (Spring) or TLS configuration (Quarkus) and reference it via ark.client.<name>.tls-configuration-name.
⚠️ trust-all: truedisables ALL certificate validation. Use only in local development against ephemeral environments. Settingark.client.<name>.trust-all=truein production exposes your application to man-in-the-middle attacks. Ark logs a runtime WARNING when trust-all is active so accidental production use is visible.
- CHANGELOG - release notes and migration guidance
- Compatibility Matrix - supported Spring Boot, Quarkus, and Java versions
- Getting Started
- Sync Client
- Async Client
- Reactor Client
- Mutiny Client
- Vert.x Client
- Transport Model -
Transport<R>contract, built-in transports, custom transports,.with(...)decorator chain - Retry & Backoff -
Retry<R>+ per-modelRetryOps<R>strategies; native operator alternatives - Serialization - Jackson, JSON-B, custom
- Logging -
LoggingInterceptorwith redaction, wire-level escape hatches - Multipart Upload - file upload with binary fidelity
- Error Handling - typed exception hierarchy
- Declarative Spring Clients
- Declarative JAX-RS Clients
- Spring Boot Integration - sync + async + WebFlux, config, TLS, IDE autowiring hint
- Quarkus Integration
- Quarkus Jackson Extension
- Testing
- Design Principles
- Keep transport explicit
- Keep serialization replaceable
- Support fluent and declarative styles
- Keep execution models separate at the API surface, unified at the transport contract
- Stay framework-friendly
- Prefer composition over lock-in (
transport.with(...))
mvn clean install
mvn clean install -DskipTests
mvn testContributions are welcome!
Please read CONTRIBUTING.md for guidelines on commit conventions, PR labels, and the release process.
Apache 2.0