---
title: Cached lookup
summary: Serve a connection string from the in-memory cache, or populate it via a retrieval.
status: draft
owner: Framework.NCore maintainers
last_reviewed: 2026-10-02
library: SAIF.NetCore.DBConnectionClient
reference_type: package
operation_id: OP-DBC-06
trigger: Called internally by every Get*Connection method
---

# Cached lookup

This private operation serializes access to the per-instance connection-string cache, returning a cached value when present and populating the cache from a retrieval when it is not. Failed retrievals are never cached.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-DBC-06 |
| **Trigger** | Called internally by `GetSqlServerConnection`, `GetPostgreSqlServerConnection`, and `GetOracleConnection` |
| **Inputs** | `connectionName` (cache key), `urlPath` |
| **Outputs** | Connection string (or an exception propagated from retrieval) |
| **Primary components** | `ConnectionClient.GetCachedConnectionString` (private) |
| **Entry point** | `src/SAIF.NetCore.DbConnectionClient/SAIF.NetCore.DBConnectionClient/SAIF.NetCore.DBConnectionClient/ConnectionClient.cs` |

## Steps

1. Acquire `lock(_connections)` ([BR-DBC-06](../business-rules.md#br-dbc-06-successful-retrievals-are-cached-for-instance-lifetime%3B-failures-are-not)). The lock is instance-wide — shared across every cache key, not just `connectionName` — so a slow or failing retrieval for one key blocks lookups for all other keys on the same instance until it completes*.
2. If the key is present in the cache, return the cached value immediately and release the lock — no HTTP call is made.
3. If the key is absent, retrieve it with bounded retry before releasing the lock:
   - Validate the backoff invariant (`RetrieveBackoffDelays.Length >= MaxRetrieveAttempts - 1`); if it does not hold, throw `InvalidOperationException` before any HTTP call ([BR-DBC-07](../business-rules.md#br-dbc-07-backoff-configuration-must-cover-all-retry-attempts)).
   - Loop up to `MaxRetrieveAttempts` (4, by default) ([BR-DBC-08](../business-rules.md#br-dbc-08-bounded-retry-with-fixed-backoff)): call `HttpClient.GetAsync(urlPath)` and read the per-attempt result.
   - On a successful, non-empty response: decode the legacy `ZSPECIALCHAR` token ([BR-DBC-12](../business-rules.md#br-dbc-12-legacy-special-character-token-is-decoded)), add the result to the cache, and return it.
   - On HTTP 401, 403, or 404: throw `DBClientException` immediately, with no retry ([BR-DBC-09](../business-rules.md#br-dbc-09-401%2F403%2F404-are-permanent-failures)).
   - On a 503 status or a non-null `ErrorException` (network-level failure): treat it as transient ([BR-DBC-10](../business-rules.md#br-dbc-10-503-or-network-level-failure-is-transient)) — if attempts remain, wait the fixed backoff delay (2s, then 3s, then 5s) and retry; otherwise throw `DBClientException`.
   - On any other non-success status, or a successful-but-empty body: throw `DBClientException` immediately ([BR-DBC-11](../business-rules.md#br-dbc-11-other-failures-and-empty-bodies-fail-immediately)).
   - If retrieval throws for any reason, nothing is added to the cache — failures are never cached ([BR-DBC-06](../business-rules.md#br-dbc-06-successful-retrievals-are-cached-for-instance-lifetime%3B-failures-are-not)).
   - > **Origin**: Steps 3.1–3.5 are implemented in [Retrieve with retry](retrieve-with-retry.md) and executed on behalf of this operation, including the exact exception messages for each branch.
4. Return the cached value and release the lock.

## Process flow

```mermaid
flowchart TD
	Start([GetCachedConnectionString called]) --> Lock[Acquire instance-wide lock on _connections]
	Lock --> Present{Key present?}
	Present -->|Yes| Return(["Return cached value, release lock"])
	Present -->|No| Validate{Backoff config valid?}
	Validate -->|No| ConfigErr(["Throw InvalidOperationException, release lock"])
	Validate -->|Yes| Get[GetAsync urlPath]
	Get --> Outcome{Outcome}
	Outcome -->|Success, non-empty body| Decode["Decode ZSPECIALCHAR, add to cache"]
	Decode --> Return
	Outcome -->|401/403/404| FailFast(["Throw DBClientException, no retry, release lock"])
	Outcome -->|503 or ErrorException| Remaining{Attempts remaining?}
	Remaining -->|Yes| Delay["Delay 2s/3s/5s"] --> Get
	Remaining -->|No| ThrowTransient(["Throw DBClientException, release lock"])
	Outcome -->|Other non-success or empty body| ImmediateFail(["Throw DBClientException, release lock"])
```

## Failure and edge-case behavior
- Because the lock is held across the entire retrieval attempt (including HTTP calls and backoff delays), a cold or failing lookup blocks every other caller of the same instance — including lookups for unrelated keys — for the duration of the retry loop*.
- A successfully retrieved value is cached for the life of the instance and never expires or is invalidated. Refreshing it requires replacing the instance — restarting the usual singleton-hosted process is the typical way to do this. Rotating the outbound NTLM service-account credential is distinct from this cache: it changes the identity presented on the next uncached request but does not clear any already-cached connection string*.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts.

## Sources
- `src/SAIF.NetCore.DbConnectionClient/SAIF.NetCore.DBConnectionClient/SAIF.NetCore.DBConnectionClient/ConnectionClient.cs`
