Skip to main content
The SDK maintains an in-memory snapshot of all config entries, secrets, and feature flags. This snapshot is the source of truth for all getter calls. The SDK never makes a blocking network call inside a getter.

TTL-based background sync

Every getter (getString, getBoolean, getInt, getLong, getDouble, getJson, getFlag, findFlag, getSecret, getSecretFilePath) runs a freshness check before reading the snapshot:
  1. If the client is closed, return - nothing to refresh.
  2. If a billing back-off window is in force, return without syncing (see Billing).
  3. If a quarantine is in force, return without syncing (see Quarantine).
  4. If now - snapshot.syncedAt < ttl, the snapshot is fresh - return.
  5. Otherwise, launch a coalesced background sync. The getter returns the current value without waiting.
This means:
  • Getters are always non-blocking, and never throw because of a network problem.
  • The first call after TTL expiry returns a slightly stale value while the refresh runs.
  • A later call, once the refresh has landed, returns the fresh value.

Coalescing and ordering

One sync applies at a time, and background refreshes coalesce.
  • Coalescing. A burst of stale reads, or a burst of stream events, produces one sync plus at most one follow-up run for whatever arrived while the first was in flight. Without this, every read past the TTL started its own request - the snapshot’s timestamp only advances once a sync finishes, so each read saw the same stale stamp.
  • Ordering. Applying the snapshot is serialised, so two responses can never land out of order and leave the older one in the cache.
A manual sync() participates in the same serialisation, so it waits behind an in-flight refresh rather than racing it.

ETag / 304 support

Every sync sends the last received ETag in an If-None-Match header. When nothing has changed the server returns 304 Not Modified with an empty body: the SDK skips deserialisation, keeps the existing snapshot, and resets its freshness stamp. That last part is what makes 304 cheap. A snapshot whose stamp did not advance would be permanently overdue, so every read past the TTL would issue another request - for a service whose data never changes, which is precisely the case the conditional request exists to make cheap.

Stream path bypasses the TTL

When connectStream() is active and the server pushes an event, a sync runs immediately - the TTL is ignored. The snapshot is updated within milliseconds of a change on the platform, regardless of ttl. For production services with the stream active, a large ttl (120 seconds or more) is fine. The stream handles freshness; the TTL is the safety net for when the stream is in fallback.

Quarantine (429 with a quarantine body)

When a sync receives HTTP 429 with a quarantine body, the SDK:
  1. Clamps the expires_at deadline to at most 24 hours from now, and collapses a deadline in the past to now.
  2. Records the clamped deadline.
  3. Calls observer.onQuarantined(reason, expiresAt) with the clamped value.
  4. Throws NexusQuarantinedException carrying the clamped value.
Until the deadline passes, the freshness check skips the sync and getters keep returning cached values. No sync requests are sent. The clamp matters: an unbounded deadline - a malformed date, clock skew, a misconfiguration - would park the background refresh for the process’s lifetime, turning a temporary measure into a permanent one with no way back. The stream has its own quarantine handling (see SSE live updates) and waits out the same clamped deadline before reconnecting, without consuming its failure budget.

Billing (402)

When a sync receives HTTP 402, the SDK:
  1. Backs the background refresh off for five minutes.
  2. Calls observer.onBillingOverdue().
  3. Throws NexusBillingException.
During the window, getters return the last successfully cached values. When it elapses, the next stale read tries again on its own. Any successful sync clears the window immediately. This is a throttle rather than a halt. A “suspend until resolved” flag needs something to clear it, and the only thing that can is the very sync the flag prevents - so the client would serve an ageing cache until the process was restarted.

Manual sync

Useful after a deployment or a known config change where you need fresh values before the next TTL expiry. Raises NexusClosedException on a closed client.

Snapshot atomicity

The snapshot is held in an AtomicReference. Reads are lock-free. A new snapshot is constructed from the full sync response and swapped in atomically, so there is no window in which a getter could read a mix of old and new values. syncedAt() returns the instant the snapshot was last confirmed current - by a 200 OK or a 304.

Bounded responses

Response bodies are bounded while reading, so an over-large response is never buffered in full and never reaches the parser as a truncated document: Content-Length is checked first when the server declares one. Exceeding a cap raises NexusResponseTooLargeException.

File secrets

Secrets of type file are materialised to disk at sync time; getSecretFilePath(key) returns the path.
  • The file is 0600 from the moment it exists - the permissions are a creation attribute, not a chmod afterwards, because a chmod-after leaves a window at the process umask.
  • Writes are staged under an unguessable CREATE_NEW name and atomically renamed over the target, so a reader holding the path sees either the old file or the new one, and the rename replaces any pre-existing file rather than writing through it.
  • The containing directory is per client, unguessably named, 0700, and created on first use - a client with no file-type secrets leaves nothing on disk.
  • The file name is a SHA-256 hash of both the key and the value, because the path reaches application logs, environment variables and process listings.
  • A secret that disappears from the snapshot has its file removed on that sync.
  • close() removes the files and the directory; a JVM shutdown hook does the same if the application exits without calling close().

Summary