☕ Space Java Client
Java client library for Space, a pricing-driven self-adaptation platform for SaaS applications.
Table of Contents
- 🎁 What You Get
- ✅ Requirements
- 📦 Installation
- ⚙️ SetUp
- 📚 API Overview
- 📊 Data Models
- 💾 Caching
- 🔌 WebSocket Events
- ⚠️ Error Handling
- ✨ Best Practices
- 🤝 Contributing
- 📄 Disclaimer & License
🎁 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
- 🌱 Spring
- ☕ Java Vanilla
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(default10000): 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();
}
}
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.
This setup can also be used to integrate SPACE within a Spring-based application. However, for production-grade scenarios, the dedicated Spring module is recommended, as it provides a higher level of abstraction, streamlined configuration, and tighter integration with the Spring ecosystem (e.g., auto-configuration and dependency injection support).
Quick Start in 2 Minutes (Java Vanilla)
import io.github.isagroup.spaceclient.SpaceClient;
import io.github.isagroup.spaceclient.SpaceClientFactory;
import io.github.isagroup.spaceclient.types.FeatureEvaluationResult;
public class Main {
public static void main(String[] args) {
SpaceClient client = SpaceClientFactory.connect(
"http://localhost:3000",
"your-api-key",
10_000
);
boolean healthy = client.isConnectedToSpace();
System.out.println("Connected: " + healthy);
String userId = "user-123";
FeatureEvaluationResult result = client.features.evaluate(userId, "serviceA-featureX");
if (result.getError() != null) {
System.err.println("Evaluation error: " + result.getError().getMessage());
} else {
System.out.println("Feature enabled: " + result.getEval());
}
String pricingToken = client.features.generateUserPricingToken(userId);
System.out.println("Pricing token: " + pricingToken);
client.close();
}
}
Configuration (Java Vanilla)
You can connect using either convenience factory methods or SpaceConnectionOptions.
Option A: simple connection
SpaceClient client = SpaceClientFactory.connect("http://localhost:3000", "your-api-key");
Option B: custom timeout
SpaceClient client = SpaceClientFactory.connect("http://localhost:3000", "your-api-key", 15000);
Option C: full options object
import io.github.isagroup.spaceclient.types.SpaceConnectionOptions;
SpaceConnectionOptions options = new SpaceConnectionOptions(
"http://localhost:3000",
"your-api-key",
15000
);
SpaceClient client = SpaceClientFactory.connect(options);
SpaceClientFactory.connect(...) validation rules:
- URL is required and must start with
http://orhttps://. - API key is required.
- Timeout is optional, but must be a positive number.
📚 API Overview
SpaceClientFactory
Factory utility for safe construction and input validation.
| Method | Description |
|---|---|
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:
| Method | Returns | Details |
|---|---|---|
isConnectedToSpace() | boolean | HTTP health check against /healthcheck. |
on(String event, Consumer<Object> callback) | void | Registers event callback for supported event names. |
removeListener(String event) | void | Removes one callback by event name. |
removeAllListeners() | void | Removes all event callbacks. |
connect() | void | Connects/reconnects WebSocket namespace if disconnected. |
disconnect() | void | Disconnects WebSocket namespace and clears socket handlers. |
close() | void | Closes sockets, cache provider, and HTTP resources. |
Getters:
getHttpUrl()getApiKey()getTimeout()getObjectMapper()
ContractModule
Contract operations available at spaceClient.contracts.
| Method | Returns | Details |
|---|---|---|
getContract(String userId) | Contract | Reads from cache first when enabled; returns null on error. |
addContract(ContractToCreate contractToCreate) | Contract | Creates contract and invalidates/refreshes user cache; null on error. |
updateContractSubscription(String userId, Subscription newSubscription) | Contract | Updates 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) | Contract | Updates usage levels for a user and service. |
removeContract(String userId) | void | Deletes 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.
| Method | Returns | Details |
|---|---|---|
evaluate(String userId, String featureId) | FeatureEvaluationResult | Read-only evaluation. Cache can be used. |
evaluate(String userId, String featureId, Map<String, Number> expectedConsumption) | FeatureEvaluationResult | Evaluation with consumption payload. |
evaluate(String userId, String featureId, Map<String, Number> expectedConsumption, boolean details, boolean server) | FeatureEvaluationResult | Full control over query flags. |
revertEvaluation(String userId, String featureId) | boolean | Reverts evaluation update (latest behavior). |
revertEvaluation(String userId, String featureId, boolean revertToLatest) | boolean | Revert strategy control (latest=true/false). |
generateUserPricingToken(String userId) | String | Returns 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:
| Method | Description |
|---|---|
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
Configuration options for establishing a connection to the Space server.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
url | String | - | Yes | The base URL of the Space server |
apiKey | String | - | Yes | API key for authentication |
timeout | Integer | 5000 | No | Connection timeout in milliseconds |
cache | CacheOptions | null | No | Cache configuration options |
Example:
SpaceConnectionOptions options = new SpaceConnectionOptions();
options.setUrl("https://space.example.com");
options.setApiKey("sk-1234567890abcdef");
options.setTimeout(10000);
Or using the constructor:
SpaceConnectionOptions options = new SpaceConnectionOptions(
"https://space.example.com",
"sk-1234567890abcdef",
10000
);
CacheOptions
Cache configuration for storing feature evaluation results.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
enabled | boolean | false | Yes | Enable/disable caching |
type | CacheType | BUILTIN | No | Type of cache to use |
ttl | Integer | 300 | No | Time-to-live in seconds (default 5 minutes) |
external | ExternalCacheConfig | null | No | External cache configuration (required when type is REDIS) |
CacheType Enum
| Value | Description |
|---|---|
BUILTIN | In-memory cache (default) |
REDIS | External Redis cache |
ExternalCacheConfig
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
redis | RedisConfig | - | Yes | Redis server configuration |
RedisConfig
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
host | String | - | Yes | Redis server hostname |
port | Integer | 6379 | No | Redis server port |
password | String | null | No | Redis authentication password |
db | Integer | 0 | No | Redis database number (valid range: 0-15) |
connectTimeout | Integer | 5000 | No | Connection timeout in milliseconds |
keyPrefix | String | "space-client:" | No | Prefix for cache keys |
Examples:
Basic cache with built-in memory:
CacheOptions cacheOptions = new CacheOptions();
cacheOptions.setEnabled(true);
cacheOptions.setType(CacheOptions.CacheType.BUILTIN);
cacheOptions.setTtl(600); // 10 minutes
Redis cache configuration:
CacheOptions.RedisConfig redisConfig = new CacheOptions.RedisConfig("redis.example.com");
redisConfig.setPort(6379);
redisConfig.setPassword("redis-password");
redisConfig.setDb(2);
redisConfig.setConnectTimeout(3000);
redisConfig.setKeyPrefix("petclinic:");
CacheOptions.ExternalCacheConfig externalConfig = new CacheOptions.ExternalCacheConfig();
externalConfig.setRedis(redisConfig);
CacheOptions cacheOptions = new CacheOptions();
cacheOptions.setEnabled(true);
cacheOptions.setType(CacheOptions.CacheType.REDIS);
cacheOptions.setExternal(externalConfig);
FeatureEvaluationResult
Result of evaluating a feature flag against a user context.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
eval | boolean | - | Yes | Whether the feature is enabled |
used | Map<String, Object> | - | No | Variables used in the evaluation |
limit | Map<String, Object> | - | No | Limit values applied |
error | EvaluationError | null | No | Error information if evaluation failed |
EvaluationError
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
code | String | - | Yes | Error code identifier |
message | String | - | Yes | Human-readable error message |
Examples:
Successful evaluation:
FeatureEvaluationResult result = new FeatureEvaluationResult();
result.setEval(true);
result.setUsed(Map.of("userId", "user-123", "plan", "premium"));
result.setLimit(Map.of("maxRequests", 1000));
result.setError(null);
Evaluation with error:
FeatureEvaluationResult.EvaluationError error = new FeatureEvaluationResult.EvaluationError(
"FEATURE_NOT_FOUND",
"Feature 'dark-mode' does not exist"
);
FeatureEvaluationResult result = new FeatureEvaluationResult();
result.setEval(false);
result.setError(error);
Reading the result:
if (result.getError() != null) {
System.err.println("Error: " + result.getError().getCode() + " - " + result.getError().getMessage());
} else if (result.getEval()) {
System.out.println("Feature is enabled!");
// Access used variables
Object userId = result.getUsed().get("userId");
} else {
System.out.println("Feature is disabled");
}
ContractToCreate
Request object for creating a new contract.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
userContact | UserContact | - | Yes | Contact information for the contract owner |
billingPeriod | BillingPeriodToCreate | - | Yes | Billing period configuration |
groupId | String | null | No | Group identifier |
contractedServices | Map<String, String> | - | Yes | Services contracted (service name → service ID) |
subscriptionPlans | Map<String, String> | - | Yes | Subscription plans (plan name → plan ID) |
subscriptionAddOns | Map<String, Map<String, Integer>> | - | Yes | Add-ons with quantities (service → add-on → quantity) |
BillingPeriodToCreate (nested class)
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
autoRenew | Boolean | null | Yes | Whether the contract auto-renews |
renewalDays | Integer | null | Yes | Days before renewal to send notification |
Example:
UserContact contact = new UserContact("user-001", "jdoe", "John", "Doe", "john@example.com", "+1-555-0123");
ContractToCreate.BillingPeriodToCreate billingPeriod = new ContractToCreate.BillingPeriodToCreate(
true, // autoRenew
30 // renewalDays
);
Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-premium");
Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 5, "storage-gb", 10));
ContractToCreate contract = new ContractToCreate();
contract.setUserContact(contact);
contract.setBillingPeriod(billingPeriod);
contract.setGroupId("group-abc");
contract.setContractedServices(services);
contract.setSubscriptionPlans(plans);
contract.setSubscriptionAddOns(addons);
Contract
Full contract information retrieved from the Space server.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
id | String | - | No | Contract identifier |
_id | String | - | No | Internal MongoDB identifier |
userContact | UserContact | - | Yes | Contact information for the contract owner |
billingPeriod | BillingPeriod | - | Yes | Current billing period details |
organizationId | String | - | No | Organization identifier |
groupId | String | - | No | Group identifier |
usageLevels | Map<String, Map<String, UsageLevel>> | - | Yes | Usage tracking per service and feature |
contractedServices | Map<String, String> | - | Yes | Services contracted (service name → service ID) |
subscriptionPlans | Map<String, String> | - | Yes | Subscription plans (plan name → plan ID) |
subscriptionAddOns | Map<String, Map<String, Integer>> | - | Yes | Add-ons with quantities |
history | List<ContractHistoryEntry> | - | Yes | History of contract changes |
Example:
Contract contract = new Contract();
contract.setId("contract-001");
contract.setOrganizationId("org-petclinic");
contract.setGroupId("group-veterinary");
UserContact contact = new UserContact("user-001", "jdoe", "John", "Doe", "john@example.com", "+1-555-0123");
contract.setUserContact(contact);
BillingPeriod billingPeriod = new BillingPeriod();
billingPeriod.setStartDate(new Date());
billingPeriod.setAutoRenew(true);
billingPeriod.setRenewalDays(30);
contract.setBillingPeriod(billingPeriod);
Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
contract.setContractedServices(services);
Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-premium");
contract.setSubscriptionPlans(plans);
Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 5));
contract.setSubscriptionAddOns(addons);
Map<String, Map<String, UsageLevel>> usageLevels = new HashMap<>();
UsageLevel level = new UsageLevel();
level.setConsumed(150.0);
level.setResetTimeStamp(new Date());
usageLevels.put("petclinic", Map.of("api-calls", level));
contract.setUsageLevels(usageLevels);
List<ContractHistoryEntry> history = new ArrayList<>();
// ... add history entries
contract.setHistory(history);
Subscription
Request object for updating an existing subscription.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
contractedServices | Map<String, String> | new HashMap<>() | Yes | Services contracted (service name → service ID) |
subscriptionPlans | Map<String, String> | new HashMap<>() | Yes | Subscription plans (plan name → plan ID) |
subscriptionAddOns | Map<String, Map<String, Integer>> | new HashMap<>() | Yes | Add-ons with quantities |
Example:
Subscription subscription = new Subscription();
Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
subscription.setContractedServices(services);
Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-enterprise");
subscription.setSubscriptionPlans(plans);
Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 10, "storage-gb", 50));
subscription.setSubscriptionAddOns(addons);
Or using the constructor:
Subscription subscription = new Subscription(
Map.of("petclinic", "svc-petclinic-001"),
Map.of("petclinic", "plan-enterprise"),
Map.of("petclinic", Map.of("extra-users", 10))
);
BillingPeriod
Billing period information for a contract.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
startDate | Date | - | Yes | Start date of the billing period |
endDate | Date | - | Yes | End date of the billing period |
autoRenew | boolean | false | Yes | Whether the contract auto-renews |
renewalDays | int | 0 | Yes | Days before renewal to send notification |
Example:
import java.util.Calendar;
Calendar startCal = Calendar.getInstance();
Calendar endCal = Calendar.getInstance();
endCal.add(Calendar.MONTH, 1);
BillingPeriod billingPeriod = new BillingPeriod();
billingPeriod.setStartDate(startCal.getTime());
billingPeriod.setEndDate(endCal.getTime());
billingPeriod.setAutoRenew(true);
billingPeriod.setRenewalDays(15);
Or using the constructor:
BillingPeriod billingPeriod = new BillingPeriod(
new Date(), // startDate
endDate, // endDate
true, // autoRenew
15 // renewalDays
);
UserContact
User contact information. Uses @JsonInclude(JsonInclude.Include.NON_NULL) to omit null fields during serialization.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
userId | String | - | Yes | User identifier |
username | String | - | Yes | Username |
firstName | String | "" | No | First name (defaults to empty string to satisfy SPACE validation) |
lastName | String | "" | No | Last name (defaults to empty string to satisfy SPACE validation) |
email | String | null | No | Email address |
phone | String | null | No | Phone number |
Note: When using the constructor
UserContact(userId, username),firstNameandlastNameare automatically set to empty strings to avoid SPACE validation errors.
Examples:
Minimal contact (recommended):
UserContact contact = new UserContact("user-001", "jdoe");
// firstName and lastName are automatically set to ""
Full contact:
UserContact contact = new UserContact(
"user-001",
"jdoe",
"John",
"Doe",
"john.doe@petclinic.com",
"+1-555-0123"
);
Using setters:
UserContact contact = new UserContact();
contact.setUserId("user-001");
contact.setUsername("jdoe");
contact.setFirstName("John");
contact.setLastName("Doe");
contact.setEmail("john.doe@petclinic.com");
contact.setPhone("+1-555-0123");
UsageLevel
Usage tracking information for a specific feature within a service.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
resetTimeStamp | Date | - | Yes | When the usage counter will reset |
consumed | double | 0.0 | Yes | Amount of the limit consumed |
Example:
UsageLevel usageLevel = new UsageLevel();
usageLevel.setConsumed(750.0);
usageLevel.setResetTimeStamp(new Date());
Or using the constructor:
UsageLevel usageLevel = new UsageLevel(resetDate, 750.0);
Checking usage against limits:
double consumed = usageLevel.getConsumed();
double limit = 1000.0;
double percentage = (consumed / limit) * 100;
System.out.println("Usage: " + percentage + "%");
ContractHistoryEntry
Historical record of a contract state.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
startDate | Date | - | Yes | Start date of this contract version |
endDate | Date | - | Yes | End date of this contract version |
contractedServices | Map<String, String> | - | Yes | Services contracted during this period |
subscriptionPlans | Map<String, String> | - | Yes | Subscription plans during this period |
subscriptionAddOns | Map<String, Map<String, Integer>> | - | Yes | Add-ons with quantities during this period |
Example:
ContractHistoryEntry entry = new ContractHistoryEntry();
entry.setStartDate(startDate);
entry.setEndDate(endDate);
Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
entry.setContractedServices(services);
Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-basic");
entry.setSubscriptionPlans(plans);
Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 2));
entry.setSubscriptionAddOns(addons);
SpaceEvent
Enum representing events that can be listened to for real-time updates.
| Value | Event Name | Description |
|---|---|---|
SYNCHRONIZED | "synchronized" | Data synchronization completed |
PRICING_CREATED | "pricing_created" | New pricing was created |
PRICING_ARCHIVED | "pricing_archived" | Pricing was archived |
PRICING_ACTIVED | "pricing_actived" | Pricing was activated |
SERVICE_DISABLED | "service_disabled" | A service was disabled |
ERROR | "error" | An error occurred |
Examples:
Using the enum directly:
SpaceEvent event = SpaceEvent.PRICING_CREATED;
String eventName = event.getEventName(); // "pricing_created"
Converting from string:
SpaceEvent event = SpaceEvent.fromString("synchronized");
if (event != null) {
System.out.println("Event: " + event.name());
}
Quick Reference: Map Key Conventions
Many types use Map<String, String> or Map<String, Map<String, Integer>>. Here are the typical key patterns:
contractedServices
Map<String, String> services = Map.of(
"petclinic", "svc-petclinic-001" // serviceName → serviceId
);
subscriptionPlans
Map<String, String> plans = Map.of(
"petclinic", "plan-premium" // serviceName → planId
);
subscriptionAddOns
Map<String, Map<String, Integer>> addons = Map.of(
"petclinic", Map.of( // serviceName → {addOnName → quantity}
"extra-users", 5,
"storage-gb", 10
)
);
usageLevels
Map<String, Map<String, UsageLevel>> usageLevels = Map.of(
"petclinic", Map.of( // serviceName → {featureName → UsageLevel}
"api-calls", new UsageLevel(resetDate, 150.0),
"storage", new UsageLevel(resetDate, 5.5)
)
);
CacheOptions
Cache configuration for storing feature evaluation results.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
enabled | boolean | false | Yes | Enable/disable caching |
type | CacheType | BUILTIN | No | Type of cache to use |
ttl | Integer | 300 | No | Time-to-live in seconds (default 5 minutes) |
external | ExternalCacheConfig | null | No | External cache configuration (required when type is REDIS) |
CacheType Enum
| Value | Description |
|---|---|
BUILTIN | In-memory cache (default) |
REDIS | External Redis cache |
ExternalCacheConfig
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
redis | RedisConfig | - | Yes | Redis server configuration |
RedisConfig
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
host | String | - | Yes | Redis server hostname |
port | Integer | 6379 | No | Redis server port |
password | String | null | No | Redis authentication password |
db | Integer | 0 | No | Redis database number (valid range: 0-15) |
connectTimeout | Integer | 5000 | No | Connection timeout in milliseconds |
keyPrefix | String | "space-client:" | No | Prefix for cache keys |
Examples
Basic cache with built-in memory:
CacheOptions cacheOptions = new CacheOptions();
cacheOptions.setEnabled(true);
cacheOptions.setType(CacheOptions.CacheType.BUILTIN);
cacheOptions.setTtl(600); // 10 minutes
Redis cache configuration:
CacheOptions.RedisConfig redisConfig = new CacheOptions.RedisConfig("redis.example.com");
redisConfig.setPort(6379);
redisConfig.setPassword("redis-password");
redisConfig.setDb(2);
redisConfig.setConnectTimeout(3000);
redisConfig.setKeyPrefix("petclinic:");
CacheOptions.ExternalCacheConfig externalConfig = new CacheOptions.ExternalCacheConfig();
externalConfig.setRedis(redisConfig);
CacheOptions cacheOptions = new CacheOptions();
cacheOptions.setEnabled(true);
cacheOptions.setType(CacheOptions.CacheType.REDIS);
cacheOptions.setExternal(externalConfig);
FeatureEvaluationResult
Result of evaluating a feature flag against a user context.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
eval | boolean | - | Yes | Whether the feature is enabled |
used | Map<String, Object> | - | No | Variables used in the evaluation |
limit | Map<String, Object> | - | No | Limit values applied |
error | EvaluationError | null | No | Error information if evaluation failed |
EvaluationError
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
code | String | - | Yes | Error code identifier |
message | String | - | Yes | Human-readable error message |
Examples
Successful evaluation:
FeatureEvaluationResult result = new FeatureEvaluationResult();
result.setEval(true);
result.setUsed(Map.of("userId", "user-123", "plan", "premium"));
result.setLimit(Map.of("maxRequests", 1000));
result.setError(null);
Evaluation with error:
FeatureEvaluationResult.EvaluationError error = new FeatureEvaluationResult.EvaluationError(
"FEATURE_NOT_FOUND",
"Feature 'dark-mode' does not exist"
);
FeatureEvaluationResult result = new FeatureEvaluationResult();
result.setEval(false);
result.setError(error);
Reading the result:
if (result.getError() != null) {
System.err.println("Error: " + result.getError().getCode() + " - " + result.getError().getMessage());
} else if (result.getEval()) {
System.out.println("Feature is enabled!");
// Access used variables
Object userId = result.getUsed().get("userId");
} else {
System.out.println("Feature is disabled");
}
ContractToCreate
Request object for creating a new contract.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
userContact | UserContact | - | Yes | Contact information for the contract owner |
billingPeriod | BillingPeriodToCreate | - | Yes | Billing period configuration |
groupId | String | null | No | Group identifier |
contractedServices | Map<String, String> | - | Yes | Services contracted (service name → service ID) |
subscriptionPlans | Map<String, String> | - | Yes | Subscription plans (plan name → plan ID) |
subscriptionAddOns | Map<String, Map<String, Integer>> | - | Yes | Add-ons with quantities (service → add-on → quantity) |
BillingPeriodToCreate (nested class)
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
autoRenew | Boolean | null | Yes | Whether the contract auto-renews |
renewalDays | Integer | null | Yes | Days before renewal to send notification |
Example
UserContact contact = new UserContact("user-001", "jdoe", "John", "Doe", "john@example.com", "+1-555-0123");
ContractToCreate.BillingPeriodToCreate billingPeriod = new ContractToCreate.BillingPeriodToCreate(
true, // autoRenew
30 // renewalDays
);
Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-premium");
Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 5, "storage-gb", 10));
ContractToCreate contract = new ContractToCreate();
contract.setUserContact(contact);
contract.setBillingPeriod(billingPeriod);
contract.setGroupId("group-abc");
contract.setContractedServices(services);
contract.setSubscriptionPlans(plans);
contract.setSubscriptionAddOns(addons);
Contract
Full contract information retrieved from the Space server.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
id | String | - | No | Contract identifier |
_id | String | - | No | Internal MongoDB identifier |
userContact | UserContact | - | Yes | Contact information for the contract owner |
billingPeriod | BillingPeriod | - | Yes | Current billing period details |
organizationId | String | - | No | Organization identifier |
groupId | String | - | No | Group identifier |
usageLevels | Map<String, Map<String, UsageLevel>> | - | Yes | Usage tracking per service and feature |
contractedServices | Map<String, String> | - | Yes | Services contracted (service name → service ID) |
subscriptionPlans | Map<String, String> | - | Yes | Subscription plans (plan name → plan ID) |
subscriptionAddOns | Map<String, Map<String, Integer>> | - | Yes | Add-ons with quantities |
history | List<ContractHistoryEntry> | - | Yes | History of contract changes |
Example
Contract contract = new Contract();
contract.setId("contract-001");
contract.setOrganizationId("org-petclinic");
contract.setGroupId("group-veterinary");
UserContact contact = new UserContact("user-001", "jdoe", "John", "Doe", "john@example.com", "+1-555-0123");
contract.setUserContact(contact);
BillingPeriod billingPeriod = new BillingPeriod();
billingPeriod.setStartDate(new Date());
billingPeriod.setAutoRenew(true);
billingPeriod.setRenewalDays(30);
contract.setBillingPeriod(billingPeriod);
Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
contract.setContractedServices(services);
Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-premium");
contract.setSubscriptionPlans(plans);
Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 5));
contract.setSubscriptionAddOns(addons);
Map<String, Map<String, UsageLevel>> usageLevels = new HashMap<>();
UsageLevel level = new UsageLevel();
level.setConsumed(150.0);
level.setResetTimeStamp(new Date());
usageLevels.put("petclinic", Map.of("api-calls", level));
contract.setUsageLevels(usageLevels);
List<ContractHistoryEntry> history = new ArrayList<>();
// ... add history entries
contract.setHistory(history);
Subscription
Request object for updating an existing subscription.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
contractedServices | Map<String, String> | new HashMap<>() | Yes | Services contracted (service name → service ID) |
subscriptionPlans | Map<String, String> | new HashMap<>() | Yes | Subscription plans (plan name → plan ID) |
subscriptionAddOns | Map<String, Map<String, Integer>> | new HashMap<>() | Yes | Add-ons with quantities |
Example
Subscription subscription = new Subscription();
Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
subscription.setContractedServices(services);
Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-enterprise");
subscription.setSubscriptionPlans(plans);
Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 10, "storage-gb", 50));
subscription.setSubscriptionAddOns(addons);
Or using the constructor:
Subscription subscription = new Subscription(
Map.of("petclinic", "svc-petclinic-001"),
Map.of("petclinic", "plan-enterprise"),
Map.of("petclinic", Map.of("extra-users", 10))
);
BillingPeriod
Billing period information for a contract.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
startDate | Date | - | Yes | Start date of the billing period |
endDate | Date | - | Yes | End date of the billing period |
autoRenew | boolean | false | Yes | Whether the contract auto-renews |
renewalDays | int | 0 | Yes | Days before renewal to send notification |
Example
import java.util.Calendar;
Calendar startCal = Calendar.getInstance();
Calendar endCal = Calendar.getInstance();
endCal.add(Calendar.MONTH, 1);
BillingPeriod billingPeriod = new BillingPeriod();
billingPeriod.setStartDate(startCal.getTime());
billingPeriod.setEndDate(endCal.getTime());
billingPeriod.setAutoRenew(true);
billingPeriod.setRenewalDays(15);
Or using the constructor:
BillingPeriod billingPeriod = new BillingPeriod(
new Date(), // startDate
endDate, // endDate
true, // autoRenew
15 // renewalDays
);
UserContact
User contact information. Uses @JsonInclude(JsonInclude.Include.NON_NULL) to omit null fields during serialization.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
userId | String | - | Yes | User identifier |
username | String | - | Yes | Username |
firstName | String | "" | No | First name (defaults to empty string to satisfy SPACE validation) |
lastName | String | "" | No | Last name (defaults to empty string to satisfy SPACE validation) |
email | String | null | No | Email address |
phone | String | null | No | Phone number |
Note: When using the constructor
UserContact(userId, username),firstNameandlastNameare automatically set to empty strings to avoid SPACE validation errors.
Examples
Minimal contact (recommended):
UserContact contact = new UserContact("user-001", "jdoe");
// firstName and lastName are automatically set to ""
Full contact:
UserContact contact = new UserContact(
"user-001",
"jdoe",
"John",
"Doe",
"john.doe@petclinic.com",
"+1-555-0123"
);
Using setters:
UserContact contact = new UserContact();
contact.setUserId("user-001");
contact.setUsername("jdoe");
contact.setFirstName("John");
contact.setLastName("Doe");
contact.setEmail("john.doe@petclinic.com");
contact.setPhone("+1-555-0123");
UsageLevel
Usage tracking information for a specific feature within a service.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
resetTimeStamp | Date | - | Yes | When the usage counter will reset |
consumed | double | 0.0 | Yes | Amount of the limit consumed |
Example
UsageLevel usageLevel = new UsageLevel();
usageLevel.setConsumed(750.0);
usageLevel.setResetTimeStamp(new Date());
Or using the constructor:
UsageLevel usageLevel = new UsageLevel(resetDate, 750.0);
Checking usage against limits:
double consumed = usageLevel.getConsumed();
double limit = 1000.0;
double percentage = (consumed / limit) * 100;
System.out.println("Usage: " + percentage + "%");
ContractHistoryEntry
Historical record of a contract state.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
startDate | Date | - | Yes | Start date of this contract version |
endDate | Date | - | Yes | End date of this contract version |
contractedServices | Map<String, String> | - | Yes | Services contracted during this period |
subscriptionPlans | Map<String, String> | - | Yes | Subscription plans during this period |
subscriptionAddOns | Map<String, Map<String, Integer>> | - | Yes | Add-ons with quantities during this period |
Example
ContractHistoryEntry entry = new ContractHistoryEntry();
entry.setStartDate(startDate);
entry.setEndDate(endDate);
Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
entry.setContractedServices(services);
Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-basic");
entry.setSubscriptionPlans(plans);
Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 2));
entry.setSubscriptionAddOns(addons);
SpaceEvent
Enum representing events that can be listened to for real-time updates.
| Value | Event Name | Description |
|---|---|---|
SYNCHRONIZED | "synchronized" | Data synchronization completed |
PRICING_CREATED | "pricing_created" | New pricing was created |
PRICING_ARCHIVED | "pricing_archived" | Pricing was archived |
PRICING_ACTIVED | "pricing_actived" | Pricing was activated |
SERVICE_DISABLED | "service_disabled" | A service was disabled |
ERROR | "error" | An error occurred |
Examples
Using the enum directly:
SpaceEvent event = SpaceEvent.PRICING_CREATED;
String eventName = event.getEventName(); // "pricing_created"
Converting from string:
SpaceEvent event = SpaceEvent.fromString("synchronized");
if (event != null) {
System.out.println("Event: " + event.name());
}
Quick Reference: Map Key Conventions
Many types use Map<String, String> or Map<String, Map<String, Integer>>. Here are the typical key patterns:
contractedServices
Map<String, String> services = Map.of(
"petclinic", "svc-petclinic-001" // serviceName → serviceId
);
subscriptionPlans
Map<String, String> plans = Map.of(
"petclinic", "plan-premium" // serviceName → planId
);
subscriptionAddOns
Map<String, Map<String, Integer>> addons = Map.of(
"petclinic", Map.of( // serviceName → {addOnName → quantity}
"extra-users", 5,
"storage-gb", 10
)
);
usageLevels
Map<String, Map<String, UsageLevel>> usageLevels = Map.of(
"petclinic", Map.of( // serviceName → {featureName → UsageLevel}
"api-calls", new UsageLevel(resetDate, 150.0),
"storage", new UsageLevel(resetDate, 5.5)
)
);
💾 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:
synchronizedpricing_createdpricing_archivedpricing_activedservice_disablederror
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(...)throwsIllegalArgumentExceptionfor invalid input.SpaceClient.isConnectedToSpace()returnsfalsewhen health check fails.ContractModulemethods returnnullon HTTP/IO failure (exceptremoveContract, which logs internally).FeatureModule.evaluate(...)returns aFeatureEvaluationResultwitherrorpopulated on IO failures.FeatureModule.revertEvaluation(...)returnsfalseon failure.FeatureModule.generateUserPricingToken(...)returnsnullon 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
SpaceClientper 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/erroroutputs. - Reuse connection options and avoid creating short-lived clients per request.
🛠️ Tech Stack
The project uses the following main tools and technologies:
🤝 Contributing
Contributions are welcome. Open an issue or submit a pull request.
📄 Disclaimer & License
This project is licensed under the MIT License. See LICENSE.
This SDK is part of ongoing research in pricing-driven devops. It is still in an early stage and not intended for production use.