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.

On this page

    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.

    step 1castJbUids

    extLoginId · partner · portfolio → jbUids

    →
    step 2castUsers …

    one entry per jbUid: user, profile, entitlements, bootstrap, preferences

    →
    fan-outshards + grants

    BRs, portfolios, accounts as their own entries; permissions evaluated once

    →
    on missCAST

    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

    regionkeyvaluewhy
    castJbUidsidType|valueCastJbUidsstep 1 — identifier to jbUids
    castUsersjbUidUserDtostep 2 — the full user aggregate as CAST returns it
    castUserProfilesjbUidUserProfileDtoprofile screen
    castUserEntitlementsjbUid (+ filter)UserEntitlementsDtoentitlement lists with a filter
    castUserBootstrapjbUidUserBootstrapDtobootstrap record
    castPreferencesjbUid (+ filter)List<PreferenceDto>mutable in CAST — separate TTL and eviction
    castUserBRs
    castUserPortfolios
    castUserAccounts
    jbUid|numbershard snapshots, indexed byJbUidper-user lists and reverse lookups without traversing the aggregate
    castUserGrantsjbUidCastUserGrantsthe permission-evaluation loop run once at load; allows(scope, feature, permission) is a set lookup
    castUserShardsjbUidCastShardManifestevery 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

    flagwhat it turns onsafe default
    dac.cache.enabledthe whole engine — needs spring.data.redis.*on in indc-core; off in the two apps until their charts wire Redis
    dac.domains.cast.enabledCAST reads served from the shared regions; @CastAuthContext resolvesoff (ENABLED_BY_DEFAULT = false) — on in indc-core, off in DMZ apps until connectivity is verified
    dac.domains.cast.ttl-minutes
    dac.domains.cast.preferences-ttl-minutes
    shared TTL of the user aggregate; TTL of preferences180 / 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:

    1. A <Name>DacGateway extends DacGateway interface with domain() fixed and the domain's read methods.
    2. A caching implementation that declares its regions under dac:<name> — with facades (AbstractCache) or explicit CacheRegionConfig beans — and reads the source through an SPI bean the application provides.
    3. A <Name>PrincipalSource the application implements, so currentPrincipal() works.
    4. A Grants record and a region that caches it per principal.
    5. Optionally a parameter annotation meta-annotated with @Stitch and its resolver.
    6. An auto-configuration conditional on the SPI bean, so the plugin is inert in applications that do not provide it.