UMS-specific: No Evolith upstream. The dependency guard pattern as applied here (tenant → user → profile dependency chain) is specific to UMS’s domain model. The generic application-layer guard pattern may be considered for future Evolith promotion.
Status: Accepted
Date: 2026-06-03
Decision Owner: Architecture
Related:
Several state-change operations in UMS are unsafe when active dependencies exist. For example, suspending a tenant while active users are still logged in, or blocking a user while their profiles are actively used in authorization decisions.
Without a guard layer, these operations would either silently leave orphaned active data or require complex multi-step workflows to check constraints manually. We need a consistent, discoverable contract that:
Dependency guards live in application command handlers, not in the domain aggregate or the UI. The handler queries the relevant repositories before dispatching the state-change command and rejects the operation if blocking dependencies are found.
This allows:
BlockedOperationError with | separatorBlocking errors are encoded as a string using BlockedOperationError.Encode(errorCode, deps) where multiple dependency entries are separated by |. The decoder BlockedOperationError.TryDecode(error, out errorCode, out deps) reconstructs the list in the presentation layer.
BlockedOperationResponseWhen a guard fires, the endpoint returns HTTP 409 Conflict with the following JSON body:
{
"errorCode": "TENANT_HAS_ACTIVE_USERS",
"message": "Human-readable message in configured locale",
"brokenRule": "A tenant cannot be suspended while active users exist.",
"blockingDependencies": [
{ "entityType": "UserAccount", "status": "Active", "count": 3 }
]
}
The errorCode values are defined in DomainErrors and mapped to messages/rules in BlockedOperationMessages.
| Operation | Error code | Blocking dependency |
|---|---|---|
| Suspend Tenant | TENANT_HAS_ACTIVE_USERS |
Active UserAccount records |
| Suspend Tenant | TENANT_HAS_ACTIVE_BRANCHES |
Active Branch records |
| Suspend Tenant | TENANT_HAS_ACTIVE_IDP_CONFIG |
Active IdpConfiguration records |
| Block / Delete User | USER_HAS_ACTIVE_PROFILES |
Active Profile records |
| Deactivate Role | ROLE_HAS_ACTIVE_PROFILES |
Active Profile records |
| Deactivate Role | ROLE_HAS_ACTIVE_CHILD_ROLES |
Active child Role records |
| Deprecate Template | TEMPLATE_HAS_ACTIVE_PROFILES |
Active Profile records |
| Remove DomainResource | DOMAIN_RESOURCE_HAS_TEMPLATE_ITEMS |
Active PermissionTemplateItem records |
| Remove Module | MODULE_HAS_ACTIVE_MENUS |
Active Menu records |
DomainErrors constant, a new BlockedOperationMessages entry, and a new check in the relevant command handler.