Skip to main content
Version: 2.0.1

☕ Space Java Client

Java client library for Space, a pricing-driven self-adaptation platform for SaaS applications.

Maven Central License: MIT

Table of Contents

🎁 What You Get

  • Simple API to connect to Space.
  • Contract lifecycle operations.
  • Feature evaluation with optional expected consumption.
  • Revert operation for optimistic usage updates.
  • Pricing token generation.
  • Built-in in-memory cache and Redis cache support.
  • WebSocket events for real-time pricing updates.

✅ Requirements

  • Java 21+
  • Maven 3.6+

📦 Installation

Add this dependency to your pom.xml:

<dependency>
<groupId>io.github.isa-group</groupId>
<artifactId>space-java-client</artifactId>
<version>{version}</version>
</dependency>

⚙️ SetUp

Quick Start in 2 Minutes (Spring)

1. Configure properties

Go to application.properties:

space.client.url=http://localhost:3000
space.client.api-key=your-api-key
space.client.timeout=10000

where:

  • space.client.url: URL of your SPACE instance (e.g., http://localhost:3000).
  • space.client.api-key: API key for authentication (obtainable from SPACE dashboard). It must be an organization-level API key.
  • space.client.timeout (default 10000): HTTP request timeout in milliseconds.

2. Scan and inject space-java-client's spring module

In your main application class, add the package scan for io.github.isagroup.spaceclient.spring:

package org.springframework.samples.myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication()
@ComponentScan(basePackages = {
"org.springframework.samples.myapp",
"io.github.isagroup.spaceclient.spring" // ← Add this
})
public class MyApp {

public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}

}

3. Inject and use SpaceClient in a service

import io.github.isagroup.spaceclient.SpaceClient;
import io.github.isagroup.spaceclient.types.FeatureEvaluationResult;
import org.springframework.stereotype.Service;

@Service
public class PricingService {

private final SpaceClient spaceClient;

public PricingService(SpaceClient spaceClient) {
this.spaceClient = spaceClient;
}

public boolean canUseExport(String userId) {
FeatureEvaluationResult result = spaceClient.features.evaluate(userId, "myapp-billingExport");
return result.getError() == null && result.getEval();
}
}
warning

The example above is provided solely as a minimal illustration of how to bootstrap the usage of space-java-client within a SPACE-based application.

It assumes that a service named myapp is already provisioned in SPACE, along with an active contract associated with a user identified by userId. This user is subscribed to a specific service version in which the billingExport feature is explicitly governed by the pricing configuration.

As such, this example omits prerequisite steps such as service provisioning, contract creation, and feature configuration, all of which are required for correct evaluation and execution.

📚 API Overview

SpaceClientFactory

Factory utility for safe construction and input validation.

MethodDescription
connect(SpaceConnectionOptions options)Creates client with full options and validates required inputs.
connect(String url, String apiKey)Convenience overload, default timeout (5000).
connect(String url, String apiKey, int timeout)Convenience overload with custom timeout.

SpaceClient

Main entry point. Exposes modules as public fields:

  • contracts (ContractModule)
  • features (FeatureModule)
  • cache (CacheModule)

Core methods:

MethodReturnsDetails
isConnectedToSpace()booleanHTTP health check against /healthcheck.
on(String event, Consumer<Object> callback)voidRegisters event callback for supported event names.
removeListener(String event)voidRemoves one callback by event name.
removeAllListeners()voidRemoves all event callbacks.
connect()voidConnects/reconnects WebSocket namespace if disconnected.
disconnect()voidDisconnects WebSocket namespace and clears socket handlers.
close()voidCloses sockets, cache provider, and HTTP resources.

Getters:

  • getHttpUrl()
  • getApiKey()
  • getTimeout()
  • getObjectMapper()

ContractModule

Contract operations available at spaceClient.contracts.

MethodReturnsDetails
getContract(String userId)ContractReads from cache first when enabled; returns null on error.
addContract(ContractToCreate contractToCreate)ContractCreates contract and invalidates/refreshes user cache; null on error.
updateContractSubscription(String userId, Subscription newSubscription)ContractUpdates one user subscription; updates cache when enabled.
updateContractSubscriptionByGroupId(String groupId, Subscription newSubscription)List<Contract>Batch update by group ID; invalidates cache for each updated user.
updateContractUsageLevels(String userId, String serviceName, Map<String, Number> usageLevelsNovations)ContractUpdates usage levels for a user and service.
removeContract(String userId)voidDeletes contract and invalidates user cache if enabled.

Usage example:

import io.github.isagroup.spaceclient.types.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

UserContact userContact = new UserContact("user-123", "john_doe");
userContact.setEmail("john@example.com");

ContractToCreate.BillingPeriodToCreate billing =
new ContractToCreate.BillingPeriodToCreate(true, 30);

Map<String, String> contractedServices = Map.of("serviceA", "pricing/v1");
Map<String, String> subscriptionPlans = Map.of("serviceA", "basic");
Map<String, Map<String, Integer>> addOns = new HashMap<>();

ContractToCreate toCreate = new ContractToCreate(
userContact,
billing,
contractedServices,
"group-1",
subscriptionPlans,
addOns
);

Contract created = client.contracts.addContract(toCreate);
Contract current = client.contracts.getContract("user-123");

Subscription subscription = new Subscription(
contractedServices,
Map.of("serviceA", "premium"),
addOns
);

Contract updatedSingle = client.contracts.updateContractSubscription("user-123", subscription);
List<Contract> updatedGroup = client.contracts.updateContractSubscriptionByGroupId("group-1", subscription);

Map<String, Number> usagePatch = Map.of("requests", 120, "storage", 2048);
Contract usageUpdated = client.contracts.updateContractUsageLevels("user-123", "serviceA", usagePatch);

client.contracts.removeContract("user-123");

FeatureModule

Feature operations available at spaceClient.features.

MethodReturnsDetails
evaluate(String userId, String featureId)FeatureEvaluationResultRead-only evaluation. Cache can be used.
evaluate(String userId, String featureId, Map<String, Number> expectedConsumption)FeatureEvaluationResultEvaluation with consumption payload.
evaluate(String userId, String featureId, Map<String, Number> expectedConsumption, boolean details, boolean server)FeatureEvaluationResultFull control over query flags.
revertEvaluation(String userId, String featureId)booleanReverts evaluation update (latest behavior).
revertEvaluation(String userId, String featureId, boolean revertToLatest)booleanRevert strategy control (latest=true/false).
generateUserPricingToken(String userId)StringReturns token or null on failure.

Feature example:

import io.github.isagroup.spaceclient.types.FeatureEvaluationResult;
import java.util.HashMap;
import java.util.Map;

FeatureEvaluationResult readOnly = client.features.evaluate("user-123", "serviceA-featureX");
if (readOnly.getError() == null && readOnly.getEval()) {
System.out.println("Read-only evaluation enabled");
}

Map<String, Number> expectedConsumption = new HashMap<>();
expectedConsumption.put("requests", 10);
expectedConsumption.put("storage", 1024);

FeatureEvaluationResult withConsumption = client.features.evaluate(
"user-123",
"serviceA-featureX",
expectedConsumption,
true,
false
);

boolean reverted = client.features.revertEvaluation("user-123", "serviceA-featureX", true);
String pricingToken = client.features.generateUserPricingToken("user-123");

CacheModule

Cache module is exposed as spaceClient.cache. It is initialized automatically if enabled in SpaceConnectionOptions.

Common methods:

MethodDescription
isEnabled()Whether cache is active and provider initialized.
get(String key, Class<T> type)Read value by key.
set(String key, T value)Set value using default TTL from cache config.
set(String key, T value, Integer ttl)Set value with explicit TTL (seconds).
delete(String key)Delete one key.
has(String key)Check key presence.
clear()Clear all provider entries.
keys(String pattern)List keys by glob pattern.
invalidateUser(String userId)Clear common key groups for user.
close()Close provider resources.

Key helper methods:

  • getContractKey(userId)
  • getFeatureKey(userId, featureName)
  • getSubscriptionKey(userId)
  • getPricingTokenKey(userId)

📊 Data Models

SpaceConnectionOptions

Fields:

  • url (String)
  • apiKey (String)
  • timeout (Integer, default 5000)
  • cache (CacheOptions)

CacheOptions

Fields:

  • enabled (boolean)
  • type (CacheType): BUILTIN or REDIS
  • ttl (Integer, seconds, default 300)
  • external (ExternalCacheConfig)

Redis fields (CacheOptions.RedisConfig):

  • host (required for Redis mode)
  • port (default 6379)
  • password (optional)
  • db (default 0, valid range 0..15)
  • connectTimeout (default 5000)
  • keyPrefix (default space-client:)

FeatureEvaluationResult

Fields:

  • eval (boolean)
  • used (Map<String, Object>)
  • limit (Map<String, Object>)
  • error (EvaluationError) with:
    • code
    • message
  • ContractToCreate
    • userContact
    • billingPeriod (autoRenew, renewalDays)
    • groupId
    • contractedServices
    • subscriptionPlans
    • subscriptionAddOns
  • Subscription
    • contractedServices
    • subscriptionPlans
    • subscriptionAddOns
  • Contract
    • userContact, billingPeriod, groupId, organizationId
    • usageLevels
    • contractedServices, subscriptionPlans, subscriptionAddOns
    • history
  • UserContact
    • userId, username, firstName, lastName, email, phone

💾 Caching

The client supports two strategies.

1) Built-in in-memory cache

import io.github.isagroup.spaceclient.types.CacheOptions;
import io.github.isagroup.spaceclient.types.CacheOptions.CacheType;
import io.github.isagroup.spaceclient.types.SpaceConnectionOptions;

CacheOptions cacheOptions = new CacheOptions(true, CacheType.BUILTIN, 300);

SpaceConnectionOptions options = new SpaceConnectionOptions(
"http://localhost:3000",
"your-api-key",
5000,
cacheOptions
);

SpaceClient client = SpaceClientFactory.connect(options);

2) Redis cache

import io.github.isagroup.spaceclient.types.CacheOptions;
import io.github.isagroup.spaceclient.types.CacheOptions.CacheType;
import io.github.isagroup.spaceclient.types.CacheOptions.ExternalCacheConfig;
import io.github.isagroup.spaceclient.types.CacheOptions.RedisConfig;
import io.github.isagroup.spaceclient.types.SpaceConnectionOptions;

RedisConfig redis = new RedisConfig("localhost", 6379);
redis.setPassword("your-password"); // optional
redis.setDb(0);
redis.setConnectTimeout(5000);
redis.setKeyPrefix("space-client:");

ExternalCacheConfig external = new ExternalCacheConfig();
external.setRedis(redis);

CacheOptions cacheOptions = new CacheOptions(true, CacheType.REDIS, 300);
cacheOptions.setExternal(external);

SpaceConnectionOptions options = new SpaceConnectionOptions(
"http://localhost:3000",
"your-api-key",
5000,
cacheOptions
);

SpaceClient client = SpaceClientFactory.connect(options);

Cache notes:

  • Contract reads use cache when enabled.
  • Read-only feature evaluations may be cached for 60 seconds.
  • Pricing tokens may be cached for 900 seconds.
  • Mutations invalidate related user keys.

🔌 WebSocket Events

Supported events:

  • synchronized
  • pricing_created
  • pricing_archived
  • pricing_actived
  • service_disabled
  • error

Example:

client.on("synchronized", data -> System.out.println("Socket connected"));
client.on("pricing_created", data -> System.out.println("Pricing created: " + data));
client.on("error", err -> System.err.println("Socket error: " + err));

client.connect();

// ...

client.removeListener("pricing_created");
client.removeAllListeners();
client.disconnect();

⚠️ Error Handling

Current behavior by module:

  • SpaceClientFactory.connect(...) throws IllegalArgumentException for invalid input.
  • SpaceClient.isConnectedToSpace() returns false when health check fails.
  • ContractModule methods return null on HTTP/IO failure (except removeContract, which logs internally).
  • FeatureModule.evaluate(...) returns a FeatureEvaluationResult with error populated on IO failures.
  • FeatureModule.revertEvaluation(...) returns false on failure.
  • FeatureModule.generateUserPricingToken(...) returns null on failure.

Suggested defensive pattern:

FeatureEvaluationResult result = client.features.evaluate(userId, featureId);
if (result.getError() != null) {
// handle recoverable error
return;
}
if (!result.getEval()) {
// feature disabled
return;
}
// proceed

✨ Best Practices

  • Keep one SpaceClient per process/service where possible.
  • Always call client.close() on shutdown to release resources.
  • Use Redis cache for multi-instance deployments.
  • For deterministic behavior under network issues, always check null / false / error outputs.
  • Reuse connection options and avoid creating short-lived clients per request.

🛠️ Tech Stack

The project uses the following main tools and technologies:

Java Maven OkHttp Jackson Socket.IO Redis SLF4J JUnit 5 Mockito Spring

🤝 Contributing

Contributions are welcome. Open an issue or submit a pull request.

📄 Disclaimer & License

This project is licensed under the MIT License. See LICENSE.

warning

This SDK is part of ongoing research in pricing-driven devops. It is still in an early stage and not intended for production use.