Milesoft Clients Library (clients)

1. Overview & Purpose

The clients library is a robust, highly-observable HTTP network and authentication client layer built on top of Apache HTTP Client 5. It serves as the unified execution engine responsible for routing, securing, and executing tenant-authenticated platform requests across the Milesoft microservices ecosystem.

All domain-driven Software Development Kits (SDKs) in the platform—such as accounts-sdk, contacts-sdk, games-sdk, and liquid-sdk—extend the core structures of this library to achieve consistent, secure, and self-healing platform integration.

1.1 Key Capabilities

  • Unified Low-Level Client (PlatformClient): Encapsulates core platform security mechanisms, including token generation (login), OAuth token refreshes, JWKS key distribution, and programmatic API key management.
  • Base Execution Framework (AbstractClient): Provides robust request execution patterns accepting pure Options request specifications, handling connection pooling, and standardizing JSON deserialization.
  • Self-Healing Token Caching & Rotation: Automatically manages token acquisition, in-memory caching via Guava Cache, and proactive token rotation upon encountering 401 Unauthorized or 403 Forbidden statuses.
  • Observability & Decoupled Exception Mapping: Translates HTTP non-2xx failures into structured ClientResponseException instances containing standard ApiError payloads, ensuring error transparency without swallowing backend details.

2. Installation & Status

The clients library is hosted in our secure Google Cloud Artifact Registry repository.

To consume clients in your project, declare our repository and add the dependency to your build.gradle:

plugins {
    id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.5"
}

repositories {
    mavenCentral()
    maven { url "artifactregistry://us-west2-maven.pkg.dev/milesoft-repo/repo-maven" }
}

dependencies {
    // Declaring the standard clients library dependency
    implementation "io.milesoft:clients:2.0.1"
}

2.1 Status

The clients library is Production-Ready and forms the backbone of all microservice-to-microservice and client-to-platform communication.


3. Core Architectural Components

3.1 The Low-Level Security Engine (PlatformClient)

PlatformClient interacts directly with the platform's core authorization and key-management endpoints (/api/v2/auth and /api/v2/key). It requires a target baseUrl and a secure platform apiKey.

The client provides the following core capabilities:

Method Signature Return Type Description
jwks() Jwks Retrieves the public JSON Web Key Sets used for signature verification.
login(Actor actor) Tokens Authenticates an execution context (Actor) to obtain dynamic access and refresh tokens.
refresh(String sid, String jti, String token) Tokens Rotates expired access/refresh token pairs utilizing active session identifiers.
introspect(String token) AuthUser Introspects an active JWT and yields details on the validated user identity.
initializeKey(String name, String nonce) FreshKey Registers a new security credential key associated with an authentication session.
createKey(String name, List<String> roles) FreshKey Provisions a new platform API key associated with specific security roles.
listKeys(String cursor, Integer limit) Keys Returns a paginated index of all API keys registered under the caller's credentials.
deleteKey(String id) void Permanently revokes and removes a specified API key.
import io.milesoft.clients.PlatformClient;
import io.milesoft.clients.dto.Tokens;
import io.milesoft.primitives.actor.Actor;

// 1. Instantiate the security client
PlatformClient platformClient = new PlatformClient("https://accounts.milesoft.api", "your-secure-api-key");

// 2. Perform actor login exchange (generates temporary tokens)
Actor actor = Actor.of("app-abc", "account-xyz").withUser("user-123");
Tokens tokens = platformClient.login(actor);

System.out.println("Access Token: " + tokens.getAccessToken());
System.out.println("Refresh Token: " + tokens.getRefreshToken());

3.2 The Standard Command Framework (AbstractClient)

All specialized domain clients subclass AbstractClient. This abstract base encapsulates base routing suffixes, exposes delegated HTTP execution verbs (doGet, doPost, doPut, doDelete, doFilePut), and enforces secure request dispatch through callAs or runAs blocks.

public abstract class AbstractClient {
    protected final String baseUrl;
    protected final TokenProvider tokenProvider;

    protected AbstractClient(String baseUrl, @Nullable String suffix, TokenProvider tokenProvider) {
        this.baseUrl = suffix != null ? baseUrl + suffix : baseUrl;
        this.tokenProvider = tokenProvider;
    }

    // Executes a query block and expects a return value, wrapping auth & retry
    protected final <R> R callAs(Actor actor, Function<String, R> callable) { ... }

    // Executes an action block (mutating payload) with no return value
    protected final void runAs(Actor actor, Consumer<String> runnable) { ... }
}

3.3 Dynamic Token Providers & Token Caching

To prevent overwhelming the core authorization service with redundant login requests, PlatformTokenProvider implements the standard TokenProvider interface. Under the hood, it delegates to an ActorAuthProvider that maintains a centralized, thread-safe cache backed by Google Guava:

  • Cache Timeout (CACHE_DURATION_AUTH): In-memory token credentials expire automatically 15 minutes after write operations.
  • Prefix Standard: All generated access tokens are formatted under the Bearer prefix standard before transmission.
import io.milesoft.clients.strategy.PlatformTokenProvider;
import io.milesoft.primitives.security.TokenProvider;

// PlatformTokenProvider acts as the shared credential supplier across all domain clients
PlatformTokenProvider tokenProvider = new PlatformTokenProvider(platformClient);

4. Self-Healing Authentication & Retry Mechanics

Executing network requests across distributed microservices introduces transient authentication hurdles (e.g., token expiration). The clients library implements an elegant, self-healing retry policy within its ClientUtils layer:

  1. Optimistic Attempt: The client attempts the target HTTP request using the cached token (provider.getAuth(false)).
  2. Failure Check: If the call succeeds, results are returned. If the endpoint responds with an HTTP status of 401 Unauthorized or 403 Forbidden, the client traps the ClientResponseException.
  3. Automatic Rotation: The client bypasses the cache, issues a fresh login exchange (provider.getAuth(true)), updates the cache with the new token, and retries the HTTP request.
  4. Finality: If the retried call fails, or if any other non-2xx status code is returned initially, the exception is propagated to the caller.
+--------------------+
|  Call API Method   |
+---------+----------+
          |
          v
+-----------------------------+
| Get Cached Auth (fresh=false) |
+---------+-------------------+
          |
          v
+-----------------------------+
|    Execute HTTP Request     |
+---------+-------------------+
          |
          +----------------------+
          | (Success, 2xx)       | (Failure, 401/403)
          v                      v
    [Return Result]      +------------------------------+
                         |  Get Fresh Auth (fresh=true)  |
                         +---------------+--------------+
                                         |
                                         v
                         +------------------------------+
                         |     Retry HTTP Request       |
                         +---------------+--------------+
                                         |
                                         +------------------+
                                         | (Success, 2xx)   | (Failure)
                                         v                  v
                                   [Return Result]    [Propagate Error]

5. Request Specification Builder (Options<I, O>)

HTTP operations are specified cleanly using the fluent, generic Options builder. This decouples the low-level HTTP client implementation from the high-level API signatures.

5.1 Configuration Parameters

  • url (Required): The fully-qualified destination endpoint URL.
  • auth: The Authorization header value (e.g., "Bearer ..."), managed automatically by callAs/runAs.
  • timeZone: A Java ZoneId value that dynamically appends the timezone indicator standard x-tz header.
  • input: The input request DTO to serialize into JSON as the request body.
  • output: The output class token (e.g. User.class) indicating the target mapping format for Jackson JSON deserialization.
  • inputHeader(key, value): Appends a custom request header.
  • outputHeader(key): Specifies response headers to capture and return in a BodyAndHeaders container (e.g., extracting X-Access-Token headers).
  • file / fileName / data: Used to perform multipart/binary file uploads natively.
import io.milesoft.clients.domain.Options;
import java.time.ZoneId;

Options<CreateUser, User> options = Options.<CreateUser, User>builder()
    .url("https://accounts.milesoft.app/api/v2/user/")
    .auth(bearerToken)
    .timeZone(ZoneId.of("America/New_York"))
    .input(new CreateUser("john.doe@milesoft.io", "John", "Doe"))
    .output(User.class)
    .build();

6. Implementation Code Examples

6.1 Creating a Custom Domain Service Client

To integrate a new platform API microservice, simply subclass AbstractClient and define your domain models:

import io.milesoft.clients.AbstractClient;
import io.milesoft.clients.domain.Options;
import io.milesoft.primitives.actor.Actor;
import io.milesoft.primitives.security.TokenProvider;
import io.milesoft.liquid.client.v2.dto.Template;
import io.milesoft.liquid.client.v2.dto.SaveTemplate;

public class TemplateClient extends AbstractClient {

    public TemplateClient(String baseUrl, TokenProvider tokenProvider) {
        // Appends service root suffix automatically
        super(baseUrl, "/api/v2/template", tokenProvider);
    }

    /**
     * Loads a specific Liquid template within the scope of the calling Actor.
     */
    public Template getTemplate(String id, Actor actor) {
        final String url = String.format("%s/%s/", baseUrl, id);

        // callAs ensures self-healing auth and thread safety
        return callAs(actor, auth -> doGet(Options.<Void, Template>builder()
            .url(url)
            .output(Template.class)
            .auth(auth)
            .build()));
    }

    /**
     * Saves or modifies a Liquid template.
     */
    public Template saveTemplate(String id, SaveTemplate command, Actor actor) {
        final String url = String.format("%s/%s/", baseUrl, id);

        return callAs(actor, auth -> doPut(Options.<SaveTemplate, Template>builder()
            .url(url)
            .input(command)
            .output(Template.class)
            .auth(auth)
            .build()));
    }
}

6.2 Spring Boot Dependency Injection Configuration

In backend application modules, instantiate your clients within a unified Spring @Configuration class. This setup typically loads base URLs and API keys using a secrets manager.

import io.milesoft.clients.PlatformClient;
import io.milesoft.clients.strategy.PlatformTokenProvider;
import io.milesoft.games.client.v2.GameClient;
import io.milesoft.games.client.v2.LeaderBoardClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GamesConfig {

    private final PlatformClient platformClient;
    private final PlatformTokenProvider tokenProvider;
    private final GameClient gameClient;
    private final LeaderBoardClient leaderBoardClient;

    public GamesConfig(
            @Value("${milesoft.platform.base-url}") String baseUrl,
            @Value("${milesoft.platform.api-key}") String apiKey) {

        // 1. Create the base low-level Client
        this.platformClient = new PlatformClient(baseUrl, apiKey);
        
        // 2. Wrap it with the caching TokenProvider
        this.tokenProvider = new PlatformTokenProvider(platformClient);

        // 3. Instantiate individual domain service clients using the TokenProvider
        this.gameClient = new GameClient(baseUrl, tokenProvider);
        this.leaderBoardClient = new LeaderBoardClient(baseUrl, tokenProvider);
    }

    @Bean
    public PlatformClient platformClient() {
        return platformClient;
    }

    @Bean
    public GameClient gameClient() {
        return gameClient;
    }

    @Bean
    public LeaderBoardClient leaderBoardClient() {
        return leaderBoardClient;
    }
}

6.3 Programmatic Error Parsing & Exception Handling

When platform endpoints return non-2xx statuses, clients translates them into a runtime ClientResponseException. It does not swallow error payloads; it reads them as standard ApiError responses, enabling rich programmatic error handling:

import io.milesoft.primitives.exception.ClientResponseException;
import io.milesoft.primitives.exception.ApiError;

try {
    Template template = templateClient.getTemplate("non-existent-id", actor);
} catch (ClientResponseException e) {
    int httpStatus = e.getStatusCode(); // e.g., 404
    ApiError errorDetails = e.getApiError();

    System.err.printf("Request failed with status: %d%n", httpStatus);
    
    if (errorDetails != null) {
        System.err.printf("Error Code: %s%n", errorDetails.getErrorCode());
        System.err.printf("Message: %s%n", errorDetails.getMessage());
        System.err.printf("Timestamp: %s%n", errorDetails.getTimestamp());
        
        if (errorDetails.getFieldErrors() != null) {
            errorDetails.getFieldErrors().forEach((field, issue) -> {
                System.err.printf(" -> Field '%s' failed validation: %s%n", field, issue);
            });
        }
    } else {
        // Fallback if the body couldn't be parsed as a standard ApiError
        System.err.printf("Raw server response: %s%n", e.getMessage());
    }
}