Java SDK
The GoodMem Java SDK offers an OpenAI-style API where any operation is accessed through client.<namespace>.<method>(...) on a Goodmem instance.
Installation
Available on Maven Central as ai.pairsys:goodmem-java.
// build.gradle.kts
repositories { mavenCentral() }
dependencies {
implementation("ai.pairsys:goodmem-java:0.1.2")
}<!-- pom.xml -->
<dependency>
<groupId>ai.pairsys</groupId>
<artifactId>goodmem-java</artifactId>
<version>0.1.2</version>
</dependency>To build from source instead (e.g., to depend on an unreleased commit):
cd goodmem/clients/v2/java
./gradlew publishToMavenLocal # installs to ~/.m2/repository/Then add mavenLocal() to your repositories block alongside mavenCentral().
Requirements: JDK 21 LTS. Brings in OkHttp 4 (HTTP) and Jackson 2.18 (JSON) transitively.
Clients
The Java SDK ships two clients with identical construction surfaces and parallel methods:
Goodmem— synchronous. Every method returns the response directly or throws.AsyncGoodmem— asynchronous. Every method returnsCompletableFuture<T>.
Most of the usage details in this page apply equally to both; code samples default to the sync client. For the async surface see Async client below.
Construction
Goodmem is constructed via its Builder in two mutually exclusive patterns.
Pattern 1 — Simple mode (baseUrl + apiKey):
import ai.pairsys.goodmem.client.Goodmem;
Goodmem client = Goodmem.builder()
.baseUrl("http://localhost:8080")
.apiKey("gm_...")
.timeout(java.time.Duration.ofSeconds(60)) // optional; defaults to 30s
.build();baseUrl— Goodmem server URL, e.g.,"http://localhost:8080"(nov1suffix)apiKey— Goodmem API key, e.g.,"gm_..."timeout— Call/connect/read/write timeout. Defaults to 30 seconds. Increase for long operations like RAG retrieval with LLM generation.
Pattern 2 — http_client mode (custom OkHttpClient):
import okhttp3.OkHttpClient;
Goodmem client = Goodmem.builder()
.baseUrl("http://localhost:8080")
.apiKey("gm_...")
.httpClient(myOkHttpClient) // fully configured by you
.build();Use this when you need interceptors (logging, tracing, retry), a proxy, a custom TLS trust store, or connection pool tuning. You own the client's lifecycle — goodmem.close() is a no-op under this mode.
try-with-resources
Goodmem implements AutoCloseable. Use it as a resource so the connection pool is released deterministically:
try (Goodmem client = Goodmem.builder()
.baseUrl("http://localhost:8080")
.apiKey("gm_...")
.build()) {
// use client
}Quickstart
import ai.pairsys.goodmem.client.Goodmem;
import ai.pairsys.goodmem.client.models.*;
try (Goodmem client = Goodmem.builder()
.baseUrl("http://localhost:8080")
.apiKey("gm_...")
.build()) {
// Create an embedder. Registry auto-fills provider, endpoint,
// dimensionality, max_sequence_length, distribution_type from the
// model_identifier; apiKey is converted to structured credentials.
EmbedderResponse embedder = client.embedders.create(
EmbedderCreationRequest.builder()
.displayName("My OpenAI")
.modelIdentifier("text-embedding-3-large")
.build(),
"sk-..."
);
// Create a space. DEFAULT_CHUNKING_CONFIG is auto-injected when unset.
Space space = client.spaces.create(
SpaceCreationRequest.builder()
.name("My Space")
.label("env", "dev")
.spaceEmbedders(java.util.List.of(new SpaceEmbedderConfig(embedder.embedderId(), null)))
.publicRead(false)
.build());
// Pagination is automatic — iterate across all pages.
for (Memory m : client.memories.list(space.spaceId())) {
System.out.println(m.memoryId());
}
}TLS configuration
Four situations cover the typical needs:
1. Default — trusts system CAs
No configuration needed. Works for any server with a cert from a public CA.
2. Skip verification (localhost / dev)
Build an OkHttpClient that trusts all certificates, then pass it in http_client mode. Never use in production — an attacker in the middle can impersonate the server:
import javax.net.ssl.*;
import java.security.cert.X509Certificate;
TrustManager[] trustAll = new TrustManager[] {
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] c, String a) {}
public void checkServerTrusted(X509Certificate[] c, String a) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}
};
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustAll, new java.security.SecureRandom());
OkHttpClient http = new OkHttpClient.Builder()
.sslSocketFactory(ctx.getSocketFactory(), (X509TrustManager) trustAll[0])
.hostnameVerifier((host, session) -> true)
.build();
Goodmem client = Goodmem.builder()
.baseUrl("https://localhost:8081")
.apiKey("gm_...")
.httpClient(http)
.build();3. Custom CA — trusts only that CA
Load your CA into a KeyStore and build a TrustManagerFactory:
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import javax.net.ssl.*;
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
try (var in = new java.io.FileInputStream("/path/to/rootCA.pem")) {
ks.setCertificateEntry("ca", CertificateFactory.getInstance("X.509").generateCertificate(in));
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
OkHttpClient http = new OkHttpClient.Builder()
.sslSocketFactory(ctx.getSocketFactory(), (X509TrustManager) tmf.getTrustManagers()[0])
.build();4. Custom CA + system CAs
If you need to trust both your CA and the default system trust store, load both into a KeyStore:
KeyStore defaultKs = KeyStore.getInstance(KeyStore.getDefaultType());
defaultKs.load(null);
TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
defaultTmf.init((KeyStore) null); // loads system CAs
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
// add default CAs
for (TrustManager tm : defaultTmf.getTrustManagers()) {
if (tm instanceof X509TrustManager xtm) {
int i = 0;
for (X509Certificate c : xtm.getAcceptedIssuers()) ks.setCertificateEntry("sys-" + i++, c);
}
}
// add your CA
try (var in = new java.io.FileInputStream("/path/to/rootCA.pem")) {
ks.setCertificateEntry("mine", CertificateFactory.getInstance("X.509").generateCertificate(in));
}
// then build TrustManagerFactory + OkHttpClient as in #3Proxy
OkHttpClient http = new OkHttpClient.Builder()
.proxy(new java.net.Proxy(Proxy.Type.HTTP, new java.net.InetSocketAddress("proxy.example", 8080)))
.build();Request logging
import okhttp3.logging.HttpLoggingInterceptor;
HttpLoggingInterceptor log = new HttpLoggingInterceptor();
log.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient http = new OkHttpClient.Builder().addInterceptor(log).build();Requires the extra dep: com.squareup.okhttp3:logging-interceptor.
Methods
Each namespace below links to its full class Javadoc on javadoc.io — that's where the per-method @param contract, @throws block, Example code, and REST equivalent all live. The MDX page below this section is the narrative reference; the Javadoc is the per-method reference.
admin
Server lifecycle ops — drain, license reload, background-job purge.
Javadoc: ai.pairsys.goodmem.client.api.AdminAPI
| Method | Description |
|---|---|
backgroundJobsPurge | Purge completed background jobs |
drain | Request the server to enter drain mode |
licenseReload | Reload the active license from disk |
retrieveMemoryLogPoliciesCreate | Create a RetrieveMemory log policy |
retrieveMemoryLogPoliciesDelete | Delete a RetrieveMemory log policy |
retrieveMemoryLogPoliciesGet | Get a RetrieveMemory log policy |
retrieveMemoryLogPoliciesList | List RetrieveMemory log policies |
apikeys
API key lifecycle — create, list, update, soft-delete.
Javadoc: ai.pairsys.goodmem.client.api.ApikeysAPI
| Method | Description |
|---|---|
create | Create a new API key |
delete | Delete an API key Permanently deletes an API key |
list | List API keys |
update | Update an API key |
embedders
Embedder management — provider configuration + lifecycle.
Javadoc: ai.pairsys.goodmem.client.api.EmbeddersAPI
| Method | Description |
|---|---|
create | Create a new embedder |
delete | Delete an embedder |
get | Get an embedder by ID |
list | List embedders |
update | Update an embedder |
llms
LLM management — generation-time model registration.
Javadoc: ai.pairsys.goodmem.client.api.LlmsAPI
| Method | Description |
|---|---|
create | Create a new LLM |
delete | Delete an LLM |
get | Get an LLM by ID |
list | List LLMs |
update | Update an LLM |
memories
Memory CRUD + retrieval (streaming) + file upload + batch ops.
Javadoc: ai.pairsys.goodmem.client.api.MemoriesAPI
| Method | Description |
|---|---|
batchCreate | Create multiple memories in a single batch |
batchDelete | Delete memories in batch |
batchGet | Get multiple memories by ID |
content | Download memory content |
create | Create a memory from text content or base64-encoded binary content |
delete | Delete a memory |
get | Get a memory by ID |
list | Lists memories within a given space |
pages | List memory page images |
pagesImage | Download memory page image content |
retrieve | Performs a streaming semantic search across one or more memory spaces and returns matching chunks ranked by relevance as well as… |
ocr
Document OCR — text extraction from PDFs / images.
Javadoc: ai.pairsys.goodmem.client.api.OcrAPI
| Method | Description |
|---|---|
document | Run OCR on a document or image |
ping
Endpoint health probes — single-shot and streaming.
Javadoc: ai.pairsys.goodmem.client.api.PingAPI
rerankers
Reranker management — re-scoring of retrieval hits.
Javadoc: ai.pairsys.goodmem.client.api.RerankersAPI
| Method | Description |
|---|---|
create | Create a new reranker |
delete | Delete a reranker |
get | Get a reranker by ID |
list | List rerankers |
update | Update a reranker |
spaces
Memory space management — the top-level container for memories.
Javadoc: ai.pairsys.goodmem.client.api.SpacesAPI
| Method | Description |
|---|---|
create | Create a new Space |
delete | Delete a space |
get | Get a space by ID |
list | List spaces accessible to the caller, with optional filtering by owner, labels, and name |
update | Update a space |
system
Server info + system initialization.
Javadoc: ai.pairsys.goodmem.client.api.SystemAPI
users
User lookup by id, email, or me.
Javadoc: ai.pairsys.goodmem.client.api.UsersAPI
Request builders
Every request record (EmbedderCreationRequest, SpaceCreationRequest, JsonMemoryCreationRequest, RetrieveMemoryRequest, RerankerCreationRequest, LLMCreationRequest, etc.) exposes a nested Builder so you can set only the fields you care about and skip the long positional null, null, null, … constructor:
EmbedderCreationRequest req = EmbedderCreationRequest.builder()
.displayName("My OpenAI")
.modelIdentifier("text-embedding-3-large")
.build();Map<String, String> fields named labels, hints, or metadata get a singular-form convenience setter that appends to the map:
SpaceCreationRequest req = SpaceCreationRequest.builder()
.name("My Space")
.label("env", "dev") // appends to labels
.label("team", "search")
.spaceEmbedders(List.of(new SpaceEmbedderConfig(embedderId, null)))
.build();Under the hood the Builder just calls the record's positional constructor. Null unset fields are omitted from the wire payload thanks to @JsonInclude(NON_NULL). You can still use the positional constructor directly if you prefer — the Builder is purely additive.
Typed list/get options
List and get methods expose a typed XxxListOptions / XxxGetOptions record so you don't have to remember wire-format keys (maxResults, sortOrder, …) and can let the IDE autocomplete them:
Page<Space> page = client.spaces.list(
SpaceListOptions.builder()
.maxResults(50)
.nameFilter("research*")
.sortOrder(SortOrder.ASCENDING)
.label("env", "prod") // flattened to label.env=prod on the wire
.build());Null-valued fields are dropped from the query string. Map<String, String> fields named label are flattened into label.<key>=<value> entries to match the server's label-filter syntax. Passing null for the options argument is equivalent to an empty filter.
Eight options classes are generated:
| Options type | Method |
|---|---|
SpaceListOptions | spaces.list(options) |
EmbedderListOptions | embedders.list(options) |
LLMListOptions | llms.list(options) |
RerankerListOptions | rerankers.list(options) |
MemoryListOptions | memories.list(spaceId, options) |
MemoryGetOptions | memories.get(id, options) |
MemoryPageListOptions | memories.pages(id, options) |
MemoryPageImageOptions | memories.pagesImage(id, pageIndex, options) |
The original Map<String, Object> overload is still available as an escape hatch — it's renamed to listRaw(...) / getRaw(...) / pagesRaw(...) / pagesImageRaw(...) so the primary method name belongs to the typed path:
// Typed (preferred):
client.spaces.list(SpaceListOptions.builder().maxResults(50).build());
// Raw escape hatch:
client.spaces.listRaw(Map.of("maxResults", 50));UUID-typed path parameters
Every get / delete / update method that takes a resource id offers a java.util.UUID overload alongside the String form. The generator converts via .toString() internally:
UUID spaceUuid = UUID.fromString("...");
Space typed = client.spaces.get(spaceUuid); // UUID-typed
Space raw = client.spaces.get("sp_..."); // String-typed (escape hatch)memories.list(spaceId, options) and memories.pagesImage(id, pageIndex, ...) also get UUID + long pageIndex overloads, plus a combined (UUID, long, ...) variant so the strongly-typed signatures compose without forcing you to mix.
Factory methods for oneOf models
Records with an "exactly one of" contract (ChunkingConfiguration, ContextItem, PingEvent, RetrievedItem) expose static factory methods per variant, so you can't accidentally hand-null sibling fields:
ChunkingConfiguration cfg = ChunkingConfiguration.recursive(
RecursiveChunkingConfiguration.builder()
.chunkSize(512)
.chunkOverlap(64)
.build());
RetrievedItem item = RetrievedItem.memory(memory);Encoded-primitive convenience setters
The Builder for each request record overloads a handful of high-churn fields with stronger Java types:
| Where | String/Long base | Typed overload | What it does |
|---|---|---|---|
endpointUrl / apiPath / monitoringEndpoint | String | java.net.URI | .toString() |
*Ms timeout fields | Long | java.time.Duration | .toMillis() |
*Sec timeout fields | Long | java.time.Duration | .toSeconds() |
*At timestamp fields (e.g. expiresAt) | Long | java.time.Instant | .toEpochMilli() |
JsonMemoryCreationRequest.originalContent | String | byte[] | base64-encodes into originalContentB64 |
RetrieveMemoryRequest.postProcessor | PostProcessor | ChatPostProcessorConfig | .toPostProcessor() |
CreateApiKeyRequest req = CreateApiKeyRequest.builder()
.label("scope", "ci")
.expiresAt(Instant.now().plus(Duration.ofDays(30)))
.build();Use ai.pairsys.goodmem.client.MediaTypes for common contentType values: TEXT_PLAIN, APPLICATION_PDF, IMAGE_PNG, etc. — avoids string-literal typos.
Typed configs for the built-in RAG chat processor
ai.pairsys.goodmem.client.ChatPostProcessorConfig is a fluent, validated builder for the ChatPostProcessor factory — the common RAG pipeline. Numeric fields enforce documented bounds where applicable (llmTemp [0, 2], positive token budgets, positive maxResults):
RetrieveMemoryRequest req = RetrieveMemoryRequest.builder()
.message("What do you know about AI?")
.spaceKeys(List.of(new SpaceKey(spaceId, null, null)))
.postProcessor(ChatPostProcessorConfig.builder()
.llmId(llmId)
.genTokenBudget(2048L)
.llmTemp(0.2)
.build())
.build();For non-built-in processors, keep using new PostProcessor(factoryName, configMap) directly.
Typed status enums
Four IR-declared status fields now deserialize as typed Java enums instead of raw strings — so pattern matching and switch on known states works:
Memory.processingStatus→MemoryProcessingStatusApiKeyResponse.status/UpdateApiKeyRequest.status→ApiKeyStatusBackgroundJobSummary.status→BackgroundJobStatusAdminDrainResponse.state→AdminDrainState
Unknown server values fail Jackson deserialization loudly; if the server adds a new state, regenerate from an updated IR.
Range validation on Builder setters
Setters on documented numeric bounds throw IllegalArgumentException at call time rather than waiting for the server to reject. Null still clears the field. Covered today:
| Field | Bound |
|---|---|
dimensionality, maxSequenceLength, chunkSize, maxResults, limit, count, efSearch, maxScan, maxInFlight | ≥ 1 |
chunkOverlap | ≥ 0 |
temperature | [0.0, 2.0] |
topP | [0.0, 1.0] |
frequencyPenalty, presencePenalty | [-2.0, 2.0] |
Common Data Models
Types shared across multiple API namespaces. Models are Java records with Jackson annotations; JSON wire names are camelCase (matching the record component names).
Page<T>
ai.pairsys.goodmem.client.Page<T> — a page of results from a list endpoint. Returned by every method that auto-paginates (spaces.list, memories.list). Iterates across all pages lazily; subsequent pages are fetched on demand. Page<T> does not implement AutoCloseable — the underlying HTTP connection is released per call, so there's nothing to close.
Page<Space> page = client.spaces.list(SpaceListOptions.builder().maxResults(50).build());
// First-page items only
List<Space> first = page.items();
// Auto-paginate through everything (lazy)
for (Space s : page) { process(s); }
// Manual page advance
while (page.hasMore()) {
page = page.next();
for (Space s : page.items()) { process(s); }
}items()→List<T>— items in this pagenextToken()→String— continuation token, ornullif exhaustedhasMore()→boolean— true iffnextToken != nullnext()→Page<T>— fetch next page; throwsNoSuchElementExceptionwhen exhaustediterator()→Iterator<T>— lazy cross-page iteration; can only be called once
RetrieveMemoryStream
ai.pairsys.goodmem.client.RetrieveMemoryStream — iterable, closeable NDJSON stream of RetrieveMemoryEvent returned by memories.retrieve. Each newline-delimited JSON document is parsed into one event. Always use try-with-resources to release the underlying HTTP connection:
RetrieveMemoryRequest req = RetrieveMemoryRequest.builder()
.message("question")
.spaceId(spaceId)
.build();
try (RetrieveMemoryStream events = client.memories.retrieve(req)) {
for (RetrieveMemoryEvent evt : events) {
process(evt);
}
}The iterator can only be consumed once — NDJSON cannot be rewound. To restart, call memories.retrieve again.
Defaults
ai.pairsys.goodmem.client.Defaults — hand-written SDK default constants. Currently exposes:
-
DEFAULT_CHUNKING_CONFIG(ChunkingConfiguration) — recursive chunking at 512 characters with 64-character overlap,KEEP_ENDboundary strategy, measured by characters. Auto-injected byspaces.createwhendefaultChunkingConfigis null. Reference it directly if you want to start from the defaults and tweak:import ai.pairsys.goodmem.client.Defaults; ChunkingConfiguration base = Defaults.DEFAULT_CHUNKING_CONFIG;
ApiKeyAuth
Configuration for classic API-key authentication.
inlineSecret(String, optional) — secret stored directly in GoodMem (mutually exclusive withsecretRef)secretRef(SecretReference, optional) — reference to an external secret manager entryheaderName(String, optional) — HTTP header to carry the credential (defaults toAuthorization)prefix(String, optional) — prepended to the secret (e.g.,"Bearer ")
ChunkingConfiguration
Chunking strategy used when processing content. Exactly one of none, recursive, or sentence must be set.
none(NoChunkingConfiguration, optional) — preserve original content as a single unitrecursive(RecursiveChunkingConfiguration, optional) — hierarchical splitting with configurable separatorssentence(SentenceChunkingConfiguration, optional) — sentence-based splitting with language detection
CredentialKind
Java enum with @JsonProperty wire values: CREDENTIAL_KIND_UNSPECIFIED, CREDENTIAL_KIND_API_KEY, CREDENTIAL_KIND_GCP_ADC.
DistributionType
Java enum: DENSE, SPARSE.
EndpointAuthentication
Structured credential payload describing how GoodMem authenticates with an upstream provider.
kind(CredentialKind) — selected credential strategyapiKey(ApiKeyAuth, optional) — set whenkind == CREDENTIAL_KIND_API_KEYgcpAdc(GcpAdcAuth, optional) — set whenkind == CREDENTIAL_KIND_GCP_ADClabels(Map<String, String>, optional) — operator-facing annotations
GcpAdcAuth
Configuration for Google Application Default Credentials.
scopes(List<String>, optional) — additional OAuth scopesquotaProjectId(String, optional) — quota project for billing
GoodMemStatus
Non-fatal warning or status (operation continues).
code(String) — machine-readable codemessage(String) — human-readable messagedetails(Map<String, String>, optional) — additional context
LengthMeasurement
Java enum: CHARACTER_COUNT, TOKEN_COUNT, CUSTOM.
LLMProviderType
Java enum for LLM providers: OPENAI, LITELLM_PROXY, OPEN_ROUTER, VLLM, OLLAMA, LLAMA_CPP, CUSTOM_OPENAI_COMPATIBLE.
Modality
Java enum: TEXT, IMAGE, AUDIO, VIDEO.
NoChunkingConfiguration
Empty record — used as the none variant of ChunkingConfiguration to preserve content as a single unit.
ProviderType
Java enum for embedder/reranker providers: OPENAI, VLLM, TEI, LLAMA_CPP, VOYAGE, COHERE, JINA.
RecursiveChunkingConfiguration
Recursive hierarchical chunking with configurable separators and overlap.
chunkSize(Long) — maximum size of a chunkchunkOverlap(Long) — sliding overlap between adjacent chunksseparators(List<String>, optional) — hierarchy of splitting separatorskeepStrategy(SeparatorKeepStrategy) — how to handle separator characters after splittingseparatorIsRegex(Boolean, optional) — treat separators as regexlengthMeasurement(LengthMeasurement) — unit forchunkSize/chunkOverlap
SecretReference
Reference to an external secret manager entry.
uri(String) — identifier for the resolver (vault://,env://, …)hints(Map<String, String>, optional) — metadata for the resolver (e.g.,{"encoding":"base64"})
SentenceChunkingConfiguration
Sentence-based chunking with language detection.
maxChunkSize(Long) — maximum chunk sizeminChunkSize(Long) — minimum before creating a new chunkenableLanguageDetection(Boolean, optional) — detect language for better segmentationlengthMeasurement(LengthMeasurement) — unit for size fields
SeparatorKeepStrategy
Java enum: KEEP_NONE, KEEP_START, KEEP_END.
SortOrder
Java enum: ASCENDING, DESCENDING, SORT_ORDER_UNSPECIFIED.
Errors
All SDK methods throw typed unchecked exceptions on HTTP errors. The hierarchy is rooted at GoodmemException, with ApiException for any HTTP 4xx/5xx and per-status subclasses for the common ones. Error class names align with the Python SDK's naming.
| Exception | HTTP | Description |
|---|---|---|
GoodmemException | — | Base exception for all SDK errors (also wraps I/O failures) |
ApiException | any 4xx/5xx | Generic HTTP error (has getStatusCode(), getBody()) |
BadRequestException | 400 | Malformed or invalid request |
AuthenticationException | 401 | Invalid or missing API key |
PermissionDeniedException | 403 | Insufficient permissions |
NotFoundException | 404 | Resource not found |
ConflictException | 409 | Conflict (e.g., duplicate id) |
UnprocessableEntityException | 422 | Invalid request parameters |
RateLimitException | 429 | Too many requests |
InternalServerException | 5xx | Server-side error |
import ai.pairsys.goodmem.client.errors.*;
try {
Memory memory = client.memories.get("nonexistent-id");
} catch (NotFoundException e) {
System.out.println("Memory not found");
} catch (ApiException e) {
System.out.println("API error " + e.getStatusCode() + ": " + e.getBody());
}All exceptions are RuntimeExceptions — no checked-exception surface.
File upload convenience
memories.create(String spaceId, Path filePath) reads a file from disk, base64-encodes it, infers a contentType via Files.probeContentType, and posts a JSON memory:
import java.nio.file.Path;
Memory memory = client.memories.create(
space.spaceId(),
Path.of("/tmp/report.pdf"));For large files consider streaming the bytes yourself or using an async upload in your own code — the built-in overload reads the file fully into memory before base64-encoding.
Javadoc
Every class, method, field, and parameter is documented in full Javadoc. Locally:
cd goodmem/clients/v2/java
./gradlew javadoc
open build/docs/javadoc/index.htmljavadoc.io hosts the per-class API reference automatically once releases land on Maven Central:
https://javadoc.io/doc/ai.pairsys/goodmem-java/latest/.
Async client
AsyncGoodmem is a parallel client where every method returns
CompletableFuture<T> instead of blocking for the response. Construction
mirrors Goodmem exactly — same Builder, same two modes (simple /
custom OkHttpClient), same timeout / close semantics.
import ai.pairsys.goodmem.client.AsyncGoodmem;
import ai.pairsys.goodmem.client.models.SystemInfoResponse;
try (AsyncGoodmem client = AsyncGoodmem.builder()
.baseUrl("http://localhost:8080")
.apiKey("gm_...")
.build()) {
// Single call — `.get()` or `.join()` if you want to block here
SystemInfoResponse info = client.system.info().get();
// Composition — ideal for pipelines
EmbedderCreationRequest req = EmbedderCreationRequest.builder()
.displayName("My OpenAI")
.modelIdentifier("text-embedding-3-large")
.build();
client.embedders
.create(req, "sk-...")
.thenCompose(emb -> client.spaces.create(
SpaceCreationRequest.builder()
.name("My Space")
.spaceEmbedders(java.util.List.of(
new SpaceEmbedderConfig(emb.embedderId(), null)))
.build()))
.thenAccept(space -> System.out.println("Space ready: " + space.spaceId()))
.exceptionally(ex -> { ex.printStackTrace(); return null; })
.join();
}Under the hood, the async client uses OkHttp's native async callbacks —
no thread pool is required beyond OkHttp's own dispatcher, and the same
connection pool is shared across concurrent requests. If you need more
concurrency control (custom dispatcher executor, rate limiting, etc.),
build an OkHttpClient yourself and pass it via .httpClient(...).
Async pagination
List endpoints return CompletableFuture<AsyncPage<T>> instead of Page<T>.
AsyncPage<T> exposes the current page's items synchronously (they're
already loaded); .next() returns CompletableFuture<AsyncPage<T>> to
fetch the next page.
static CompletableFuture<List<Space>> collectAll(AsyncPage<Space> page, List<Space> acc) {
acc.addAll(page.items());
return page.hasMore()
? page.next().thenCompose(p -> collectAll(p, acc))
: CompletableFuture.completedFuture(acc);
}
client.spaces.list()
.thenCompose(first -> collectAll(first, new ArrayList<>()))
.thenAccept(all -> System.out.println("Got " + all.size() + " spaces"));AsyncPage<T> intentionally does not implement Iterable<T> — mixing
blocking iteration with async I/O is a footgun. Chain via .next() and
.thenCompose() instead.
Async streaming
memories.retrieve returns CompletableFuture<RetrieveMemoryStream> on
the async client. The future completes once the stream headers arrive;
the returned stream is consumed the same way as in the sync client (lazy
Iterable<RetrieveMemoryEvent> inside a try-with-resources block).
RetrieveMemoryRequest req = RetrieveMemoryRequest.builder()
.message("question")
.spaceId(spaceId)
.build();
client.memories.retrieve(req).thenAccept(events -> {
try (events) {
for (RetrieveMemoryEvent e : events) process(e);
}
}).join();Error handling
HTTP errors (4xx/5xx) and I/O failures complete the future exceptionally
with the same exception types as the sync client, wrapped by
CompletionException at the call site:
client.memories.get("missing")
.thenAccept(System.out::println)
.exceptionally(ex -> {
Throwable cause = ex.getCause(); // unwrap CompletionException
if (cause instanceof NotFoundException) {
System.out.println("Memory not found");
}
return null;
});Request validation errors — e.g., credential_check throwing when a SaaS
endpoint is targeted without credentials — are thrown synchronously,
before the HTTP call is scheduled. The rationale: that's a programmer
error, not an I/O event, and should fail fast.
Support
Report issues at github.com/PAIR-Systems-Inc/goodmem/issues, or reach out to forrest@pairsys.ai.