Config Service — Application Logic
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 02 DDD
1. Use Cases — Commands
| Command | Use Case Class | Description |
|---|---|---|
CreateFeatureDefinitionCommand | CreateFeatureDefinitionUseCase | Create a feature within a module |
UpdateFeatureDefinitionCommand | UpdateFeatureDefinitionUseCase | Update feature actions or data scope |
CreateRoleDefinitionCommand | CreateRoleDefinitionUseCase | Create a new role (system or tenant) |
UpdateRoleDefinitionCommand | UpdateRoleDefinitionUseCase | Update role display name or flags |
AddRoleInheritanceCommand | AddRoleInheritanceUseCase | Add a parent-child role inheritance edge |
SetRoleFeatureGrantCommand | SetRoleFeatureGrantUseCase | Set granted/denied actions for role+feature |
CreateUserNodeOverrideCommand | CreateUserNodeOverrideUseCase | Create explicit allow/deny for user at node |
DeleteUserNodeOverrideCommand | DeleteUserNodeOverrideUseCase | Soft-delete a user node override |
CreateUIDefinitionCommand | CreateUIDefinitionUseCase | Register a UI element definition |
UpdateUIDefinitionCommand | UpdateUIDefinitionUseCase | Update a UI element's action binding |
SetUIVisibilityRuleCommand | SetUIVisibilityRuleUseCase | Set role/user visibility rule for an element |
SetDesignTokenCommand | SetDesignTokenUseCase | Set a design token at a scope |
2. Use Cases — Queries
| Query | Use Case Class | Description |
|---|---|---|
ResolveConfigQuery | ResolveConfigUseCase | Execute 9-step resolution pipeline |
ResolveUIConfigQuery | ResolveUIConfigUseCase | Return UI visibility tree for a feature |
ResolveDesignTokensQuery | ResolveDesignTokensUseCase | Return merged token map for module/locale |
GetUserOverridesQuery | GetUserOverridesUseCase | List all overrides for a user |
GetRoleFeatureGrantsQuery | GetRoleFeatureGrantsUseCase | List grants for a role |
3. Ports (Interfaces)
| Port | Interface | Adapters |
|---|---|---|
ConfigNodeRepository | ConfigNodeRepository port | PostgresConfigNodeAdapter |
FeatureDefinitionRepository | FeatureDefinitionRepository port | PostgresFeatureDefinitionAdapter |
RoleRepository | RoleRepository port | PostgresRoleAdapter |
UserNodeOverrideRepository | UserNodeOverrideRepository port | PostgresUserOverrideAdapter |
UIDefinitionRepository | UIDefinitionRepository port | PostgresUIDefinitionAdapter |
DesignTokenRepository | DesignTokenRepository port | PostgresDesignTokenAdapter |
HierarchyClient | HierarchyClient port | HttpHierarchyAdapter → facility-service |
LicenseClient | LicenseClient port | HttpLicenseAdapter → platform-admin-service |
FeatureFlagClient | FeatureFlagClient port | HttpFeatureFlagAdapter → platform-admin-service |
ABACClient | ABACClient port | HttpABACAdapter → access-policy |
IdentityClient | IdentityClient port | HttpIdentityAdapter → identity-service |
CachePort | CachePort port | RedisAdapter |
EventPublisher | EventPublisher port | NatsEventPublisher |
AuditClient | AuditClient port | NatsAuditPublisher → audit-service |
4. Resolution Pipeline — Sequence
5. Cache Invalidation Flow
When a mutation event is published (e.g. config.feature.updated.v1), the service subscribes via NATS and evicts relevant Redis keys:
| Mutation event | Cache keys evicted |
|---|---|
config.feature.* | cfg:{tenantId}:*:*:{moduleKey}:{featureKey} |
config.role.* | cfg:roles:{tenantId}:{roleKey}:expanded + all user resolution caches |
config.role_grant.* | cfg:roles:{tenantId}:{roleKey}:expanded |
config.ui_definition.* | cfg:ui:{tenantId}:*:{featureKey} |
config.user_override.* | cfg:{tenantId}:{userId}:* + cfg:ui:{tenantId}:{userId}:* |
config.design_token.* | cfg:tokens:{tenantId}:*:{moduleKey} |
After eviction, config.config.cache_busted.v1 is published for observability.
6. Outbox Pattern
All mutation use cases persist the domain event to the outbox table in the same database transaction before committing. A background relay worker reads the outbox and publishes to NATS JetStream. This guarantees at-least-once delivery and prevents event loss on crash.
7. Inbox / Deduplication
For events the config-service consumes from NATS (e.g. tenant mutation events from tenant-service), an inbox table deduplicates by (source, eventId). Duplicate events are acknowledged but not processed.
8. Error Handling
| Scenario | Behaviour |
|---|---|
| facility-service unavailable | Return { effect: "deny", reason: "DEPENDENCY_UNAVAILABLE" } with 503; do NOT fallback to an allow |
| access-policy unavailable | Return { effect: "deny", reason: "DEPENDENCY_UNAVAILABLE" } with 503; fail closed |
| Redis unavailable | Bypass cache; run full pipeline; log warning; alert if > 30 s |
| Resolution timeout > 500 ms | Return 504 RESOLUTION_TIMEOUT; circuit breaker trips after 5 consecutive timeouts |
| Cycle detected at role definition | Reject with 409 CIRCULAR_ROLE_INHERITANCE |
| Cycle detected at config node creation | Reject with 409 CONFIG_CIRCULAR_REFERENCE |
Cross-tenant nodeId | Return { effect: "deny", reason: "CROSS_TENANT" } in 200 body |
9. Business Rules Enforced in Application Layer
| Rule | Enforcement |
|---|---|
tenantId from JWT only — never from body/query in user path | JWT extraction middleware; DTO strips tenant field |
ExplicitDeny is final | Resolution use case short-circuits after Step 7 deny; no further evaluation |
isAbstract role cannot be assigned | SetRoleAssignmentUseCase validates isAbstract = false |
isSystem role only by SUPER_ADMIN | Guard in CreateRoleDefinitionUseCase |
justification required for user overrides | DTO validation; empty string → 422 |
| BFS depth max 10 | BFS loop tracks depth counter; terminates + throws at 10 |