The CAST domain
cache-domain-cast is the first domain plugin and the reference for
writing one: a set of shared regions under dac:cast, a CastDacGateway that
is both the read API and the DacGateway for access control, and
@CastAuthContext for parameter injection. Applications plug in two small beans.
Concept
CAST resolves reads in two steps — identifier → jbUids, then jbUid → details — so the plugin caches both steps separately. Two lookups resolving to the same person share one cached user, and a user cached by indc-core during delivery is a warm hit for alert-be-app's settings screen.
extLoginId · partner · portfolio → jbUids
one entry per jbUid: user, profile, entitlements, bootstrap, preferences
BRs, portfolios, accounts as their own entries; permissions evaluated once
through the app's CastUserSource
All regions live under the plugin-pinned namespace dac:cast, regardless of the
application's own root, and share one TTL (dac.domains.cast.ttl-minutes, default 180)
so a user's data can never mix two loads. Preferences, which change in CAST, have their own shorter
TTL (preferences-ttl-minutes, default 15).
Regions
| region | key | value | why |
|---|---|---|---|
castJbUids | idType|value | CastJbUids | step 1 — identifier to jbUids |
castUsers | jbUid | UserDto | step 2 — the full user aggregate as CAST returns it |
castUserProfiles | jbUid | UserProfileDto | profile screen |
castUserEntitlements | jbUid (+ filter) | UserEntitlementsDto | entitlement lists with a filter |
castUserBootstrap | jbUid | UserBootstrapDto | bootstrap record |
castPreferences | jbUid (+ filter) | List<PreferenceDto> | mutable in CAST — separate TTL and eviction |
castUserBRscastUserPortfolioscastUserAccounts | jbUid|number | shard snapshots, indexed byJbUid | per-user lists and reverse lookups without traversing the aggregate |
castUserGrants | jbUid | CastUserGrants | the permission-evaluation loop run once at load; allows(scope, feature, permission) is a set lookup |
castUserShards | jbUid | CastShardManifest | every shard key of a user, so evictUser removes the aggregate and all children exactly, cross-pod |
Plugging an application in
The plugin activates only when the application provides a CastUserSource bean. Two
methods are required; the richer GETs are optional:
public interface CastUserSource {
List<String> getJbUids(CastIdType idType, String value);
UserDto getUser(String jbUid);
default UserDto getUser(String jbUid, CastEntitlementFilter filter) { … }
default UserProfileDto getUserProfile(String jbUid) { return null; }
default UserEntitlementsDto getUserEntitlements(String jbUid, CastEntitlementFilter filter) { return null; }
default UserBootstrapDto getUserBootstrap(String jbUid) { return null; }
default List<PreferenceDto> getPreferences(String jbUid, CastPreferenceFilter filter) { return null; }
}
For @CastAuthContext and AbstractDacCache to know who is calling, the
application also provides a CastPrincipalSource — one method:
@FunctionalInterface
public interface CastPrincipalSource extends PrincipalSource {
Optional<String> currentJbUid(); // from the JWT, a header, a thread-local
}
In CNH the source is the zone-aware CastAppInitiatedRestClient: DMZ pods forward the
caller's WAF/GSSO token (from the servlet context or CastCallContext for reactive
callers); BaerNet pods use an ADFS service token. One client, both zones, only the token differs.
Reading through the gateway
List<UserDto> findUsersByPriority(String extLoginId, String partnerNumber, String portfolioNumber);
List<String> jbUids(CastIdType idType, String value);
Optional<UserDto> user(String jbUid);
Optional<UserProfileDto> userProfile(String jbUid);
Optional<UserEntitlementsDto> userEntitlements(String jbUid, CastEntitlementFilter filter);
Optional<UserBootstrapDto> userBootstrap(String jbUid);
List<PreferenceDto> preferences(String jbUid, CastPreferenceFilter filter);
List<CastBrSnapshot> businessRelationships(String jbUid);
List<CastPortfolioSnapshot> portfolios(String jbUid);
Optional<CastUserGrants> grants(String jbUid);
void evictUser(String jbUid);
void evictPreferences(String jbUid, CastPreferenceFilter filter);
A permission check is the whole story in one line:
gateway.grants(jbUid)
.map(g -> g.allows(CastUserGrants.portfolioScope("PF-100"), "ETRADING", "ETRADING_INITIATE_TRADE"))
.orElse(false);
Because CastDacGateway extends DacGateway with domain() = "cast", the same
bean is what an AbstractDacCache or @DacAuthorize(domain = "cast") uses to
decide access — see Access control.
Rollout flags, in flip order
| flag | what it turns on | safe default |
|---|---|---|
dac.cache.enabled | the whole engine — needs spring.data.redis.* | on in indc-core; off in the two apps until their charts wire Redis |
dac.domains.cast.enabled | CAST reads served from the shared regions; @CastAuthContext resolves | off (ENABLED_BY_DEFAULT = false) — on in indc-core, off in DMZ apps until connectivity is verified |
dac.domains.cast.ttl-minutesdac.domains.cast.preferences-ttl-minutes | shared TTL of the user aggregate; TTL of preferences | 180 / 15 |
With the domain off every gateway call goes straight to CastUserSource and every
@CastAuthContext parameter is null, so the call site's legacy path runs.
Turning it on changes no Redis key layout, so the flip is reversible.
Writing another domain
Copy the shape, not the code:
- A
<Name>DacGateway extends DacGatewayinterface withdomain()fixed and the domain's read methods. - A caching implementation that declares its regions under
dac:<name>— with facades (AbstractCache) or explicitCacheRegionConfigbeans — and reads the source through an SPI bean the application provides. - A
<Name>PrincipalSourcethe application implements, socurrentPrincipal()works. - A
Grantsrecord and a region that caches it per principal. - Optionally a parameter annotation meta-annotated with
@Stitchand its resolver. - An auto-configuration conditional on the SPI bean, so the plugin is inert in applications that do not provide it.