Back to Blog

Hierarchical Multi-Tenancy: Organization-Aware Resource Isolation

Simple multi-tenant systems have only one "tenant" layer. Enterprise scenarios are far more complex:

CoomiaPublished on December 29, 20256 min read
Share this articleTwitter / X

Hierarchical Multi-Tenancy: Organization-Aware Resource Isolation

Series: S10 Design Patterns · Article 14 | Level: Advanced | Reading Time: 18 min

#TL;DR

  • Hierarchical Multi-Tenancy supports Group, Subsidiary, Region, Branch, and Team organizational structures, with each level having independent resource quotas, data isolation, and permission boundaries.
  • The coomia-dip tenant model is not a flat "tenant_id" but an organization tree. Child tenants inherit parent configurations while being able to override specific settings.
  • Through the combination of World Context and Nessie branching, coomia-dip achieves multi-level data isolation while supporting cross-level aggregation queries.

#Introduction: Enterprise Multi-Tenancy Complexity

Simple multi-tenant systems have only one "tenant" layer. Enterprise scenarios are far more complex:

Code
Wanda Group
├── Wanda Commercial (Subsidiary A)
│   ├── East Region
│   │   ├── Shanghai Branch
│   │   └── Hangzhou Branch
│   └── North Region
│       └── Beijing Branch
├── Wanda Tourism (Subsidiary B)
└── Wanda Finance (Subsidiary C)

Requirements:
- Group HQ needs to see aggregated data across all subsidiaries
- Subsidiaries can only see their own regional data
- Branches can only see their own data
- Certain rules defined at group level, inherited by subsidiaries
- Subsidiaries can override some group rules but not mandatory ones

#Part 1: Tenant Hierarchy Model

#1.1 Organization Tree Definition

Python
from dataclasses import dataclass, field
from typing import Any

@dataclass
class TenantNode:
    tenant_id: str
    name: str
    display_name: str
    level: str                          # group | subsidiary | region | branch | team
    parent_id: str | None = None
    children: list[str] = field(default_factory=list)
    config: dict[str, Any] = field(default_factory=dict)
    quotas: dict[str, Any] = field(default_factory=dict)
    overrides: dict[str, Any] = field(default_factory=dict)
    status: str = "active"

class TenantHierarchy:
    def __init__(self):
        self._nodes: dict[str, TenantNode] = {}

    async def create_tenant(self, tenant: TenantNode) -> TenantNode:
        if tenant.parent_id and tenant.parent_id not in self._nodes:
            raise ValueError(f"Parent tenant '{tenant.parent_id}' not found")
        self._nodes[tenant.tenant_id] = tenant
        if tenant.parent_id:
            self._nodes[tenant.parent_id].children.append(tenant.tenant_id)
        return tenant

    def get_ancestors(self, tenant_id: str) -> list[TenantNode]:
        ancestors = []
        current = self._nodes.get(tenant_id)
        while current and current.parent_id:
            parent = self._nodes.get(current.parent_id)
            if parent:
                ancestors.append(parent)
            current = parent
        return ancestors

    def get_descendants(self, tenant_id: str) -> list[TenantNode]:
        descendants = []
        node = self._nodes.get(tenant_id)
        if not node:
            return descendants
        queue = list(node.children)
        while queue:
            child_id = queue.pop(0)
            child = self._nodes.get(child_id)
            if child:
                descendants.append(child)
                queue.extend(child.children)
        return descendants

    def get_effective_config(self, tenant_id: str) -> dict:
        config: dict[str, Any] = {}
        for ancestor in reversed(self.get_ancestors(tenant_id)):
            config = self._deep_merge(config, ancestor.config)
        current = self._nodes[tenant_id]
        config = self._deep_merge(config, current.config)
        config = self._deep_merge(config, current.overrides)
        return config

    def _deep_merge(self, base: dict, override: dict) -> dict:
        result = dict(base)
        for key, value in override.items():
            if key in result and isinstance(result[key], dict) and isinstance(value, dict):
                result[key] = self._deep_merge(result[key], value)
            else:
                result[key] = value
        return result

#1.2 Quota Inheritance and Allocation

Python
class QuotaManager:
    async def allocate_quota(
        self, parent_id: str, child_id: str, resource_type: str, amount: int
    ) -> bool:
        parent_quota = await self._get_quota(parent_id, resource_type)
        available = parent_quota.total_limit - parent_quota.allocated
        if amount > available:
            return False
        parent_quota.allocated += amount
        child_quota = QuotaDefinition(resource_type=resource_type, total_limit=amount)
        await self._save_quota(parent_id, parent_quota)
        await self._save_quota(child_id, child_quota)
        return True

    async def get_quota_usage(self, tenant_id: str) -> dict:
        descendants = self._hierarchy.get_descendants(tenant_id)
        usage = {}
        for resource_type in ["storage_gb", "compute_cores", "objects", "rules"]:
            own_quota = await self._get_quota(tenant_id, resource_type)
            child_allocated = sum(
                (await self._get_quota(d.tenant_id, resource_type)).total_limit
                for d in descendants
            )
            usage[resource_type] = {
                "total_limit": own_quota.total_limit,
                "allocated_to_children": child_allocated,
                "own_usage": own_quota.allocated - child_allocated,
                "available": own_quota.total_limit - own_quota.allocated,
            }
        return usage

#Part 2: Data Isolation Strategies

#2.1 Multi-Level Isolation via World and Nessie

Python
class HierarchicalDataIsolation:
    async def setup_isolation(self, tenant: TenantNode) -> dict:
        world = await self._world_service.create_world(
            name=f"tenant-{tenant.tenant_id}",
            parent_world_id=self._get_parent_world(tenant),
            isolation_level=self._get_isolation_level(tenant.level),
        )
        namespace = await self._nessie.create_namespace(
            f"tenant/{tenant.tenant_id}",
            properties={"tenant_id": tenant.tenant_id, "level": tenant.level},
        )
        return {
            "world_id": world.world_id,
            "namespace": namespace.name,
            "isolation_level": self._get_isolation_level(tenant.level),
        }

    def _get_isolation_level(self, level: str) -> str:
        return {
            "group": "full",
            "subsidiary": "namespace",
            "region": "schema",
            "branch": "row",
            "team": "row",
        }.get(level, "row")

#2.2 Cross-Level Aggregation Queries

Python
class CrossTenantQueryEngine:
    async def aggregate_query(
        self, query: str, tenant_id: str,
        include_descendants: bool = True, depth: int | None = None
    ) -> list[dict]:
        tenants = [tenant_id]
        if include_descendants:
            descendants = self._hierarchy.get_descendants(tenant_id)
            if depth:
                descendants = [d for d in descendants if self._get_depth(d, tenant_id) <= depth]
            tenants.extend([d.tenant_id for d in descendants])

        for tid in list(tenants):
            if not await self._check_cross_tenant_permission(tenant_id, tid):
                tenants.remove(tid)

        results = await asyncio.gather(
            *[self._query_tenant(query, tid) for tid in tenants]
        )
        merged = []
        for tenant_results in results:
            merged.extend(tenant_results)
        return merged

#Part 3: Rule Inheritance and Override

#3.1 Rule Hierarchy

Python
class HierarchicalRuleEngine:
    async def get_effective_rules(self, tenant_id: str, rule_type: str) -> list[dict]:
        ancestors = self._hierarchy.get_ancestors(tenant_id)
        all_tenants = list(reversed(ancestors)) + [self._hierarchy.get_node(tenant_id)]

        effective_rules = []
        overridden_ids = set()

        for tenant in reversed(all_tenants):
            tenant_rules = await self._rule_store.get_rules(tenant.tenant_id, rule_type)
            for rule in tenant_rules:
                if rule.get("mandatory") and rule["id"] in overridden_ids:
                    raise PolicyViolation(
                        f"Mandatory rule '{rule['id']}' cannot be overridden"
                    )
                if rule["id"] not in overridden_ids:
                    effective_rules.append(rule)
                    if rule.get("overrides"):
                        overridden_ids.add(rule["overrides"])
        return effective_rules

#3.2 Mandatory vs Optional Rules

Python
class RuleInheritancePolicy:
    RULE_CATEGORIES = {
        "mandatory": {"description": "Cannot be overridden by child tenants"},
        "default": {"description": "Applied by default, can be overridden"},
        "optional": {"description": "Not applied unless explicitly enabled"},
    }

    async def can_override(self, tenant_id: str, rule_id: str) -> bool:
        rule = await self._rule_store.get(rule_id)
        return rule.get("category") != "mandatory"

#Part 4: Tenant Management API

#4.1 Tenant CRUD

Python
class TenantManagementService:
    async def create_subsidiary(
        self, group_id: str, name: str, config: dict, quotas: dict
    ) -> TenantNode:
        tenant = TenantNode(
            tenant_id=generate_id(), name=name,
            display_name=config.get("display_name", name),
            level="subsidiary", parent_id=group_id,
            config=config, quotas=quotas,
        )
        for resource_type, limit in quotas.items():
            await self._quota_manager.allocate_quota(
                group_id, tenant.tenant_id, resource_type, limit
            )
        await self._data_isolation.setup_isolation(tenant)
        parent_config = self._hierarchy.get_effective_config(group_id)
        tenant.config = self._hierarchy._deep_merge(parent_config, config)
        await self._hierarchy.create_tenant(tenant)
        return tenant

    async def get_organization_tree(self, root_tenant_id: str) -> dict:
        root = self._hierarchy.get_node(root_tenant_id)
        return self._build_tree(root)

    def _build_tree(self, node: TenantNode) -> dict:
        return {
            "id": node.tenant_id, "name": node.name,
            "level": node.level, "status": node.status,
            "children": [
                self._build_tree(self._hierarchy.get_node(cid))
                for cid in node.children
                if self._hierarchy.get_node(cid)
            ],
        }

#Part 5: Security Boundaries

#5.1 Cross-Tenant Access Control

Python
class CrossTenantAccessControl:
    async def check_access(
        self, actor_tenant_id: str, target_tenant_id: str, operation: str
    ) -> bool:
        if actor_tenant_id == target_tenant_id:
            return True
        if self._is_ancestor(actor_tenant_id, target_tenant_id):
            return operation in ["read", "aggregate"]
        return False

    def _is_ancestor(self, potential_ancestor: str, tenant_id: str) -> bool:
        ancestors = self._hierarchy.get_ancestors(tenant_id)
        return any(a.tenant_id == potential_ancestor for a in ancestors)

#Key Takeaways

  1. Organization Tree Model: Tenants are not flat IDs but a multi-level tree reflecting real organizational structure
  2. Configuration Inheritance: Child tenants inherit parent configurations and can override non-mandatory settings
  3. Quota Management: Quotas are allocated from parent to child tenants with recursive usage tracking
  4. Multi-Level Isolation: Different isolation levels selected by organizational tier (full/namespace/row-level)
  5. Downward Visibility: Parent tenants can aggregate query all child tenant data; children see only their own
  6. Mandatory Rules: Group-level mandatory rules cannot be overridden by subsidiaries, ensuring compliance consistency

#Next Article

S10-15: AI Pair Programming: LLM-Assisted Ontology Modeling and Rule Writing

#Tags

#DesignPatterns #MultiTenancy #Hierarchical #OrganizationTree #QuotaManagement #DataIsolation #RuleInheritance