HTTP client library built on OpenFeign for declarative REST API consumption. Features automatic response/error decoding, bucket-based rate limiting, request/response interceptors, route discovery, Cloudflare cache status parsing, retry-after handling, and timed connection socket factories.
Important
This library is under active development. APIs may change between releases until a stable 1.0.0 is published.
- Declarative HTTP clients - Define REST endpoints as annotated Java interfaces using OpenFeign
- Automatic decoding - Response and error bodies are decoded via Gson with configurable decoders
- Rate limiting - Bucket-based throttling with configurable rate limit policies per endpoint
- Retry-after handling - Automatic parsing and respect of
Retry-Afterheaders - Request/response interceptors - Pluggable interceptor pipeline for headers, logging, and transforms
- Route discovery - Dynamic route resolution with provider-based discovery
- Cloudflare integration - Parses
CF-Cache-Statusheaders for cache hit/miss tracking - Timed socket factories - Connection-level timing for plain and TLS sockets
- Reactive endpoints - Support for reactive-style endpoint definitions
| Requirement | Version | Notes |
|---|---|---|
| Java | 21+ | Required (LTS recommended) |
| Gradle | 9.4+ | Or use the included gradlew wrapper |
| Git | 2.x+ | For cloning the repository |
Published via JitPack. Add the JitPack repository and dependency to your build file.
repositories {
mavenCentral()
maven(url = "https://jitpack.io")
}
dependencies {
implementation("com.github.simplified-dev:client:master-SNAPSHOT")
}repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
dependencies {
implementation 'com.github.simplified-dev:client:master-SNAPSHOT'
}Tip
Replace master-SNAPSHOT with a specific commit hash or tag for reproducible builds.
Declare your API operations as a Feign-annotated interface extending Endpoint. Annotate the interface with @Route to set the target host and optional rate limit:
import dev.simplified.client.request.Endpoint;
import dev.simplified.client.ratelimit.RateLimitConfig;
import dev.simplified.client.response.Response;
import dev.simplified.client.route.Route;
import feign.Param;
import feign.RequestLine;
@Route(value = "api.example.com/v1", rateLimit = @RateLimitConfig(limit = 100, window = 60))
public interface UserApi extends Endpoint {
@RequestLine("GET /users/{id}")
Response<User> getUser(@Param("id") String id);
@RequestLine("GET /users")
Response<List<User>> listUsers();
}Client is an abstract class parameterized by your endpoint interface. Extend it and provide a Gson instance:
import com.google.gson.Gson;
import dev.simplified.client.Client;
@Getter
@RequiredArgsConstructor
public class UserClient extends Client<UserApi> {
private final @NotNull Gson gson;
}UserClient client = new UserClient(new Gson());
// Synchronous call via the endpoint proxy
Response<User> response = client.getEndpoint().getUser("123");
User user = response.getBody();
// Asynchronous call via ReactiveEndpoint
CompletableFuture<Response<User>> future = client.fromBlocking(api -> api.getUser("123"));
// Check rate limit status
long remaining = client.getRemainingRequests("api.example.com/v1");
boolean limited = client.isRateLimited("api.example.com/v1");
// Inspect the most recent response
client.getLastResponse().ifPresent(resp -> {
HttpStatus status = resp.getStatus();
long latency = client.getLatency();
});Override template methods on your Client subclass to customize behavior:
public class UserClient extends Client<UserApi> {
// ...
@Override
protected ConcurrentMap<String, String> configureHeaders() {
ConcurrentMap<String, String> headers = Concurrent.newMap();
headers.put("Content-Type", "application/json");
return headers;
}
@Override
protected ConcurrentMap<String, Supplier<Optional<String>>> configureDynamicHeaders() {
ConcurrentMap<String, Supplier<Optional<String>>> headers = Concurrent.newMap();
headers.put("Authorization", () -> Optional.ofNullable(getApiKey()));
return headers;
}
@Override
protected Timings configureTimings() {
return new Timings(
120_000, 45_000, 30_000, // connection TTL, idle timeout, keep-alive
5_000, 10_000, // connect timeout, socket timeout
200, 50, // max connections, max per route
3_600_000, 100 // cache duration, max cache size
);
}
@Override
protected ClientErrorDecoder configureErrorDecoder() {
return (methodKey, response) -> new MyApiException(methodKey, response);
}
}| Package | Description |
|---|---|
dev.simplified.client |
Core Client builder and configuration |
dev.simplified.client.decoder |
Response and error decoders (ClientErrorDecoder, InternalErrorDecoder, InternalResponseDecoder) |
dev.simplified.client.exception |
Exception hierarchy (ApiException, ApiDecodeException, RateLimitException, RetryableApiException) |
dev.simplified.client.factory |
Timed connection socket factories for plain and TLS connections |
dev.simplified.client.interceptor |
Request and response interceptor interfaces |
dev.simplified.client.ratelimit |
Rate limit management with bucket-based throttling (RateLimitManager, RateLimit, RateLimitBucket, RateLimitConfig) |
dev.simplified.client.request |
Endpoint definitions, HTTP methods, request timings, parameter expanders |
dev.simplified.client.response |
Response wrappers, HTTP status/state enums, Cloudflare cache status, retry-after parsing |
dev.simplified.client.route |
Route discovery and dynamic route resolution |
client/
├── src/main/java/dev/simplified/client/
│ ├── Client.java
│ ├── decoder/
│ │ ├── ClientErrorDecoder.java
│ │ ├── InternalErrorDecoder.java
│ │ └── InternalResponseDecoder.java
│ ├── exception/
│ │ ├── ApiDecodeException.java
│ │ ├── ApiException.java
│ │ ├── RateLimitException.java
│ │ └── RetryableApiException.java
│ ├── factory/
│ │ ├── TimedPlainConnectionSocketFactory.java
│ │ └── TimedSecureConnectionSocketFactory.java
│ ├── interceptor/
│ │ ├── InternalRequestInterceptor.java
│ │ └── InternalResponseInterceptor.java
│ ├── ratelimit/
│ │ ├── RateLimit.java
│ │ ├── RateLimitBucket.java
│ │ ├── RateLimitConfig.java
│ │ └── RateLimitManager.java
│ ├── request/
│ │ ├── Endpoint.java
│ │ ├── HttpMethod.java
│ │ ├── ReactiveEndpoint.java
│ │ ├── Request.java
│ │ ├── Timings.java
│ │ └── expander/
│ ├── response/
│ │ ├── CloudflareCacheStatus.java
│ │ ├── HttpState.java
│ │ ├── HttpStatus.java
│ │ ├── NetworkDetails.java
│ │ ├── Response.java
│ │ └── RetryAfterParser.java
│ └── route/
│ ├── DynamicRoute.java
│ ├── DynamicRouteProvider.java
│ ├── Route.java
│ └── RouteDiscovery.java
├── build.gradle.kts
├── settings.gradle.kts
├── gradle/
│ └── libs.versions.toml
├── LICENSE.md
└── lombok.config
| Dependency | Version | Scope |
|---|---|---|
| OpenFeign (feign-gson) | 13.11 | API |
| OpenFeign (feign-httpclient) | 13.11 | API |
| Gson | 2.11.0 | API |
| Log4j2 | 2.25.3 | API |
| JetBrains Annotations | 26.0.2 | API |
| Lombok | 1.18.36 | Compile-only |
| collections | master-SNAPSHOT | API (Simplified-Dev) |
| utils | master-SNAPSHOT | API (Simplified-Dev) |
| reflection | master-SNAPSHOT | API (Simplified-Dev) |
See CONTRIBUTING.md for development setup, code style guidelines, and how to submit a pull request.
This project is licensed under the Apache License 2.0 - see LICENSE.md for the full text.