Back to Blog

S3-16 Entity 360° View: InstanceDetailService Aggregated View

The Entity 360° View is one of the most critical data consumption capabilities in an Ontology platform. InstanceDetailService provides a unified aggregated view interface that consolidates an entity's base properties, relationships, time-series data, computed metrics, audit logs, and action history into a single structured response. This article fully dissects the architecture design, multi-source data aggregation strategy, caching and invalidation mechanisms, permission trimming, and performance optimizations behind the 360° View.

Coomia DIPPublished on July 26, 202520 min read
Share this articleTwitter / X

S3-16 Entity 360° View: InstanceDetailService Aggregated View

“

Series: S3 Data Foundation · Article 16 | Level: Advanced | Reading Time: 20 min

#TL;DR

The Entity 360° View is one of the most critical data consumption capabilities in an Ontology platform. InstanceDetailService provides a unified aggregated view interface that consolidates an entity's base properties, relationships, time-series data, computed metrics, audit logs, and action history into a single structured response. This article fully dissects the architecture design, multi-source data aggregation strategy, caching and invalidation mechanisms, permission trimming, and performance optimizations behind the 360° View.

#1. Why Entity 360° Views Matter

In traditional enterprise applications, viewing complete information about a single entity requires switching between multiple systems: ERP for base attributes, CRM for customer interactions, BI dashboards for metric reports, and logging systems for operational records. This fragmented experience severely degrades decision-making efficiency.

One of Palantir Foundry's core value propositions is the "Object View" — displaying all information related to an entity on a single page. This is not a trivial data concatenation exercise; it demands deep architectural design.

The coomia-dip InstanceDetailService mirrors Palantir's Object View with the following goals:

  • Unified entry point: A single gRPC call retrieves all dimensional information about an entity
  • On-demand loading: Clients specify which dimensions they need, avoiding unnecessary queries
  • Permission-aware: Automatically trims invisible properties and relationships based on the caller's ABAC policies
  • Real-time aggregation: Fetches the latest data from multiple sources on every request
  • Cache optimization: Enables caching for slowly changing dimensions to reduce backend pressure
Code
+------------------------------------------------------------------+
|  Entity 360° View — Data Consumption Pattern                      |
|                                                                   |
|  Traditional Pattern              360° View Pattern               |
|                                                                   |
|  App1 → DB1 (base props)          InstanceDetailService           |
|  App2 → DB2 (relationships)             |                         |
|  App3 → DB3 (metrics)            Aggregate all dimensions         |
|  App4 → DB4 (time-series)        into one structured response     |
|  App5 → DB5 (audit logs)                |                         |
|                                   One call → Full entity profile  |
+------------------------------------------------------------------+

#2. InstanceDetailService Core Architecture

InstanceDetailService is an aggregator service — it does not store data itself but orchestrates queries across multiple downstream data sources and assembles results into a unified response structure.

Code
+------------------------------------------------------------------+
|                    InstanceDetailService                          |
|                                                                   |
|  +--------------------+  +---------------------+                  |
|  | DimensionRouter    |  | ResponseAssembler   |                  |
|  | - route to source  |  | - merge results     |                  |
|  | - parallel fetch   |  | - apply permissions |                  |
|  +--------------------+  +---------------------+                  |
|           |                        |                              |
|  +--------------------+  +---------------------+                  |
|  | CacheManager       |  | PermissionFilter    |                  |
|  | - dimension-level  |  | - field-level ABAC  |                  |
|  | - TTL per source   |  | - relation-level    |                  |
|  | - invalidation     |  | - redaction rules   |                  |
|  +--------------------+  +---------------------+                  |
|                                                                   |
|  Data Sources:                                                    |
|  +----------+ +----------+ +----------+ +----------+ +----------+|
|  | Property | | Relation | | Metric   | | TimeSer. | | AuditLog ||
|  | Store    | | Store    | | Engine   | | Store    | | Store    ||
|  | (Doris)  | | (Doris)  | | (Calc)   | | (Doris)  | | (PG)     ||
|  +----------+ +----------+ +----------+ +----------+ +----------+|
+------------------------------------------------------------------+

#2.1 Core Component Responsibilities

DimensionRouter: Receives the list of dimensions specified in the client request and routes each dimension to its corresponding data source. Supports parallel querying via asyncio.gather to issue multiple data source queries simultaneously.

ResponseAssembler: Merges results returned from each data source into a unified EntityDetailResponse structure. Handles data source timeouts, partial failures, and other exceptions, ensuring that even if one dimension query fails, other dimensions still return normally.

CacheManager: A dimension-level cache manager. Different dimensions have different TTLs (Time-To-Live) — base properties change slowly and can be cached for 5 minutes, computed metrics can be cached for 1 minute, and audit logs are never cached.

PermissionFilter: Before the final response is returned, trims invisible fields based on the caller's ABAC policies. This is a critical component of the Ontology platform's security model.

#3. Dimension Model Design

The core of the 360° View is the "dimension" concept. Each dimension represents a category of information that can be independently queried, cached, and authorized.

Python
from enum import Enum
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field
from datetime import datetime


class ViewDimension(str, Enum):
    """Supported dimensions for Entity 360° View"""
    PROPERTIES = "properties"          # Base properties
    LINKS = "links"                    # Relationships (outgoing + incoming)
    METRICS = "metrics"               # Computed metric values
    TIME_SERIES = "time_series"        # Time-series data
    AUDIT_LOG = "audit_log"            # Audit log entries
    ACTION_HISTORY = "action_history"  # Action execution history
    ATTACHMENTS = "attachments"        # Attachments / media
    DERIVED = "derived"               # Derived properties
    LINEAGE = "lineage"               # Data lineage


class DimensionConfig(BaseModel):
    """Dimension configuration: controls query behavior"""
    dimension: ViewDimension
    enabled: bool = True
    cache_ttl_seconds: int = 300
    max_items: int = 100               # Max return count for list dimensions
    include_fields: Optional[List[str]] = None  # Field whitelist
    exclude_fields: Optional[List[str]] = None  # Field blacklist


class EntityDetailRequest(BaseModel):
    """Entity 360° View request"""
    object_type_rid: str
    instance_rid: str
    dimensions: List[ViewDimension] = Field(
        default_factory=lambda: [ViewDimension.PROPERTIES, ViewDimension.LINKS]
    )
    dimension_configs: Optional[Dict[ViewDimension, DimensionConfig]] = None
    locale: str = "en-US"
    world_rid: Optional[str] = None    # Which World to view from


class DimensionResult(BaseModel):
    """Query result for a single dimension"""
    dimension: ViewDimension
    status: str                         # "ok" | "error" | "timeout" | "forbidden"
    data: Optional[Dict[str, Any]] = None
    error_message: Optional[str] = None
    fetched_at: datetime = Field(default_factory=datetime.utcnow)
    from_cache: bool = False
    cache_expires_at: Optional[datetime] = None


class EntityDetailResponse(BaseModel):
    """Entity 360° View response"""
    object_type_rid: str
    instance_rid: str
    display_name: str
    object_type_display_name: str
    dimensions: Dict[ViewDimension, DimensionResult]
    total_fetch_ms: int                 # Total elapsed time
    partial_failure: bool = False       # Whether any dimensions failed

#3.1 Dimension Registry

The system manages each dimension's data source and query strategy through a Dimension Registry:

Python
class DimensionRegistry:
    """Dimension Registry: dimension -> data source + query strategy"""

    def __init__(self):
        self._registry: Dict[ViewDimension, DimensionHandler] = {}

    def register(self, dimension: ViewDimension, handler: DimensionHandler):
        self._registry[dimension] = handler

    def get_handler(self, dimension: ViewDimension) -> Optional[DimensionHandler]:
        return self._registry.get(dimension)

    def supported_dimensions(self) -> List[ViewDimension]:
        return list(self._registry.keys())


class DimensionHandler(ABC):
    """Base class for dimension handlers"""

    @abstractmethod
    async def fetch(
        self,
        object_type_rid: str,
        instance_rid: str,
        config: DimensionConfig,
        context: QueryContext
    ) -> DimensionResult:
        ...

    @abstractmethod
    def cache_key(self, object_type_rid: str, instance_rid: str) -> str:
        ...

    @abstractmethod
    def default_ttl(self) -> int:
        """Default cache TTL in seconds"""
        ...

#4. Multi-Source Parallel Aggregation

The performance key to the 360° View lies in parallel aggregation. If 9 dimensions are queried sequentially, each taking 50ms, the total latency would be 450ms. Through parallel queries, total latency equals that of the slowest dimension.

Python
import asyncio
import time
from typing import Dict, List

class InstanceDetailService:
    """Entity 360° View Service"""

    def __init__(
        self,
        dimension_registry: DimensionRegistry,
        cache_manager: CacheManager,
        permission_filter: PermissionFilter,
        timeout_ms: int = 3000
    ):
        self._registry = dimension_registry
        self._cache = cache_manager
        self._permissions = permission_filter
        self._timeout_ms = timeout_ms

    async def get_entity_detail(
        self,
        request: EntityDetailRequest,
        caller_context: CallerContext
    ) -> EntityDetailResponse:
        start_time = time.monotonic()

        # Step 1: Verify entity existence
        instance = await self._verify_instance(
            request.object_type_rid, request.instance_rid
        )

        # Step 2: Query all requested dimensions in parallel
        dimension_tasks = {}
        for dim in request.dimensions:
            handler = self._registry.get_handler(dim)
            if handler is None:
                continue
            config = self._get_dimension_config(dim, request.dimension_configs)
            dimension_tasks[dim] = self._fetch_with_cache_and_timeout(
                handler, request, config, caller_context
            )

        # Execute all dimension queries in parallel
        results: Dict[ViewDimension, DimensionResult] = {}
        if dimension_tasks:
            completed = await asyncio.gather(
                *[self._wrap_task(dim, task) for dim, task in dimension_tasks.items()],
                return_exceptions=True
            )
            for dim, result in completed:
                results[dim] = result

        # Step 3: Permission trimming
        filtered_results = await self._permissions.filter_dimensions(
            results, caller_context
        )

        elapsed_ms = int((time.monotonic() - start_time) * 1000)
        partial_failure = any(
            r.status != "ok" for r in filtered_results.values()
        )

        return EntityDetailResponse(
            object_type_rid=request.object_type_rid,
            instance_rid=request.instance_rid,
            display_name=instance.display_name,
            object_type_display_name=instance.object_type_display_name,
            dimensions=filtered_results,
            total_fetch_ms=elapsed_ms,
            partial_failure=partial_failure
        )

    async def _fetch_with_cache_and_timeout(
        self,
        handler: DimensionHandler,
        request: EntityDetailRequest,
        config: DimensionConfig,
        caller_context: CallerContext
    ) -> DimensionResult:
        # Check cache first
        cache_key = handler.cache_key(
            request.object_type_rid, request.instance_rid
        )
        cached = await self._cache.get(cache_key)
        if cached is not None:
            cached.from_cache = True
            return cached

        # Query data source with timeout
        try:
            result = await asyncio.wait_for(
                handler.fetch(
                    request.object_type_rid,
                    request.instance_rid,
                    config,
                    QueryContext(world_rid=request.world_rid, locale=request.locale)
                ),
                timeout=self._timeout_ms / 1000
            )
            # Write to cache
            ttl = config.cache_ttl_seconds or handler.default_ttl()
            if ttl > 0:
                await self._cache.set(cache_key, result, ttl)
            return result
        except asyncio.TimeoutError:
            return DimensionResult(
                dimension=config.dimension,
                status="timeout",
                error_message=f"Dimension {config.dimension} timed out after {self._timeout_ms}ms"
            )
        except Exception as e:
            return DimensionResult(
                dimension=config.dimension,
                status="error",
                error_message=str(e)
            )

#4.1 Timeout and Degradation Strategies

Parallel queries must have timeout controls. If a particular data source (e.g., the time-series database) is slow to respond, it must not block the entire 360° View.

The degradation strategy has three tiers:

  1. Timeout degradation: After a dimension query times out, return status: "timeout". The client can choose to retry that dimension separately later.
  2. Error degradation: After a data source error, return status: "error" with the error message. Other dimensions are unaffected.
  3. Cache degradation: If a data source is unavailable but expired cache data exists, return the stale cache and mark stale: true.
Python
class CacheManager:
    """Cache manager with degradation support"""

    def __init__(self, redis_client):
        self._redis = redis_client

    async def get(self, key: str) -> Optional[DimensionResult]:
        raw = await self._redis.get(f"entity360:{key}")
        if raw is None:
            return None
        return DimensionResult.model_validate_json(raw)

    async def get_stale(self, key: str) -> Optional[DimensionResult]:
        """Get possibly-expired cache (used for degradation)"""
        raw = await self._redis.get(f"entity360:stale:{key}")
        if raw is None:
            return None
        result = DimensionResult.model_validate_json(raw)
        result.from_cache = True
        return result

    async def set(self, key: str, result: DimensionResult, ttl: int):
        data = result.model_dump_json()
        # Normal cache
        await self._redis.setex(f"entity360:{key}", ttl, data)
        # Degradation cache (longer TTL)
        await self._redis.setex(f"entity360:stale:{key}", ttl * 10, data)

#5. Dimension Handler Implementations

#5.1 Properties Dimension

The properties dimension is the most fundamental dimension, querying all property values for an entity from Doris instance tables.

Python
class PropertiesDimensionHandler(DimensionHandler):
    """Base properties dimension handler"""

    def __init__(self, doris_client, schema_registry):
        self._doris = doris_client
        self._schema = schema_registry

    async def fetch(
        self,
        object_type_rid: str,
        instance_rid: str,
        config: DimensionConfig,
        context: QueryContext
    ) -> DimensionResult:
        # Get the ObjectType schema
        schema = await self._schema.get_object_type(object_type_rid)
        table_name = schema.storage_table

        # Build query
        fields = self._resolve_fields(schema, config)
        query = f"SELECT {', '.join(fields)} FROM {table_name} WHERE rid = ?"

        row = await self._doris.query_one(query, [instance_rid])
        if row is None:
            return DimensionResult(
                dimension=ViewDimension.PROPERTIES,
                status="error",
                error_message=f"Instance {instance_rid} not found"
            )

        # Convert database row to property dict with display names and type info
        properties = {}
        for prop_def in schema.properties:
            if prop_def.api_name in row:
                properties[prop_def.api_name] = {
                    "value": row[prop_def.api_name],
                    "display_name": prop_def.display_name.get(
                        context.locale, prop_def.api_name
                    ),
                    "type": prop_def.property_type.value,
                    "editable": prop_def.editable
                }

        return DimensionResult(
            dimension=ViewDimension.PROPERTIES,
            status="ok",
            data={"properties": properties, "primary_key": schema.primary_key}
        )

    def default_ttl(self) -> int:
        return 300  # 5 minutes

The links dimension queries all outgoing and incoming edges for an entity, including summary information about the linked target entities.

Python
class LinksDimensionHandler(DimensionHandler):
    """Relationships dimension handler"""

    def __init__(self, doris_client, schema_registry):
        self._doris = doris_client
        self._schema = schema_registry

    async def fetch(
        self,
        object_type_rid: str,
        instance_rid: str,
        config: DimensionConfig,
        context: QueryContext
    ) -> DimensionResult:
        schema = await self._schema.get_object_type(object_type_rid)

        # Query outgoing and incoming edges
        outgoing_links = await self._fetch_outgoing(schema, instance_rid, config)
        incoming_links = await self._fetch_incoming(schema, instance_rid, config)

        # Group by LinkType
        grouped = {}
        for link in outgoing_links + incoming_links:
            lt = link["link_type_rid"]
            if lt not in grouped:
                grouped[lt] = {
                    "link_type_rid": lt,
                    "display_name": link["link_type_display_name"],
                    "direction": link["direction"],
                    "items": []
                }
            grouped[lt]["items"].append({
                "target_rid": link["target_rid"],
                "target_display_name": link["target_display_name"],
                "target_object_type": link["target_object_type_rid"]
            })

        return DimensionResult(
            dimension=ViewDimension.LINKS,
            status="ok",
            data={
                "link_groups": list(grouped.values()),
                "total_outgoing": len(outgoing_links),
                "total_incoming": len(incoming_links)
            }
        )

    async def _fetch_outgoing(self, schema, instance_rid, config):
        query = """
            SELECT lt.rid as link_type_rid,
                   lt.display_name as link_type_display_name,
                   li.target_rid,
                   ti.display_name as target_display_name,
                   lt.target_object_type_rid
            FROM link_instances li
            JOIN link_types lt ON li.link_type_rid = lt.rid
            JOIN object_instances ti ON li.target_rid = ti.rid
            WHERE li.source_rid = ?
            LIMIT ?
        """
        return await self._doris.query_many(query, [instance_rid, config.max_items])

    def default_ttl(self) -> int:
        return 120  # 2 minutes

#5.3 Metrics Dimension

The metrics dimension calls the MetricEngine to compute all metric values associated with the current entity.

Python
class MetricsDimensionHandler(DimensionHandler):
    """Metrics dimension handler"""

    def __init__(self, metric_engine, metric_registry):
        self._engine = metric_engine
        self._registry = metric_registry

    async def fetch(
        self,
        object_type_rid: str,
        instance_rid: str,
        config: DimensionConfig,
        context: QueryContext
    ) -> DimensionResult:
        # Get all metric definitions associated with this ObjectType
        metric_defs = await self._registry.get_metrics_for_type(object_type_rid)

        # Compute all metrics in parallel
        metric_results = await asyncio.gather(*[
            self._engine.compute(
                metric_def=md,
                filter_instance_rid=instance_rid,
                world_rid=context.world_rid
            )
            for md in metric_defs
        ], return_exceptions=True)

        metrics = []
        for md, result in zip(metric_defs, metric_results):
            if isinstance(result, Exception):
                metrics.append({
                    "metric_rid": md.rid,
                    "display_name": md.display_name,
                    "status": "error",
                    "error": str(result)
                })
            else:
                metrics.append({
                    "metric_rid": md.rid,
                    "display_name": md.display_name,
                    "value": result.value,
                    "unit": md.unit,
                    "trend": result.trend,
                    "status": "ok"
                })

        return DimensionResult(
            dimension=ViewDimension.METRICS,
            status="ok",
            data={"metrics": metrics}
        )

    def default_ttl(self) -> int:
        return 60  # 1 minute

#5.4 Time Series Dimension

The time-series dimension queries an entity's time-series properties with support for time-range filtering and downsampling.

Python
class TimeSeriesDimensionHandler(DimensionHandler):
    """Time-series data dimension handler"""

    def __init__(self, doris_client, schema_registry):
        self._doris = doris_client
        self._schema = schema_registry

    async def fetch(
        self,
        object_type_rid: str,
        instance_rid: str,
        config: DimensionConfig,
        context: QueryContext
    ) -> DimensionResult:
        schema = await self._schema.get_object_type(object_type_rid)
        ts_properties = [
            p for p in schema.properties
            if p.property_type == PropertyType.TIME_SERIES
        ]

        if not ts_properties:
            return DimensionResult(
                dimension=ViewDimension.TIME_SERIES,
                status="ok",
                data={"series": [], "message": "No time series properties defined"}
            )

        series_data = []
        for ts_prop in ts_properties:
            points = await self._doris.query_many(
                f"""
                SELECT timestamp, value FROM {ts_prop.storage_table}
                WHERE instance_rid = ?
                ORDER BY timestamp DESC
                LIMIT ?
                """,
                [instance_rid, config.max_items]
            )
            series_data.append({
                "property_api_name": ts_prop.api_name,
                "display_name": ts_prop.display_name,
                "points": points,
                "point_count": len(points)
            })

        return DimensionResult(
            dimension=ViewDimension.TIME_SERIES,
            status="ok",
            data={"series": series_data}
        )

    def default_ttl(self) -> int:
        return 30  # 30 seconds

#5.5 Audit Log Dimension

The audit log dimension queries the entity's change history from PostgreSQL.

Python
class AuditLogDimensionHandler(DimensionHandler):
    """Audit log dimension handler"""

    def __init__(self, pg_client):
        self._pg = pg_client

    async def fetch(
        self,
        object_type_rid: str,
        instance_rid: str,
        config: DimensionConfig,
        context: QueryContext
    ) -> DimensionResult:
        logs = await self._pg.query_many(
            """
            SELECT id, action, actor_rid, actor_display_name,
                   changed_fields, old_values, new_values,
                   created_at, source_ip
            FROM audit_log
            WHERE entity_rid = ?
            ORDER BY created_at DESC
            LIMIT ?
            """,
            [instance_rid, config.max_items]
        )

        entries = []
        for log in logs:
            entries.append({
                "id": log["id"],
                "action": log["action"],
                "actor": {
                    "rid": log["actor_rid"],
                    "display_name": log["actor_display_name"]
                },
                "changes": {
                    "fields": log["changed_fields"],
                    "old_values": log["old_values"],
                    "new_values": log["new_values"]
                },
                "timestamp": log["created_at"].isoformat(),
                "source_ip": log["source_ip"]
            })

        return DimensionResult(
            dimension=ViewDimension.AUDIT_LOG,
            status="ok",
            data={"entries": entries, "total_count": len(entries)}
        )

    def default_ttl(self) -> int:
        return 0  # Never cache

#6. Permission Trimming: Field-Level ABAC

Data returned by the 360° View must pass through permission trimming. Different user roles may see entirely different dimensions, fields, and relationships.

Python
class PermissionFilter:
    """Permission trimming filter"""

    def __init__(self, abac_engine):
        self._abac = abac_engine

    async def filter_dimensions(
        self,
        dimensions: Dict[ViewDimension, DimensionResult],
        caller: CallerContext
    ) -> Dict[ViewDimension, DimensionResult]:
        filtered = {}

        for dim, result in dimensions.items():
            # Dimension-level permission check
            if not await self._abac.can_view_dimension(caller, dim):
                filtered[dim] = DimensionResult(
                    dimension=dim,
                    status="forbidden",
                    error_message="Insufficient permissions"
                )
                continue

            # Field-level permission trimming
            if dim == ViewDimension.PROPERTIES and result.data:
                result.data["properties"] = await self._filter_properties(
                    result.data["properties"], caller
                )

            # Relationship-level permission trimming
            if dim == ViewDimension.LINKS and result.data:
                result.data["link_groups"] = await self._filter_links(
                    result.data["link_groups"], caller
                )

            filtered[dim] = result

        return filtered

    async def _filter_properties(self, properties: dict, caller: CallerContext) -> dict:
        """Filter out invisible property fields"""
        visible = {}
        for prop_name, prop_data in properties.items():
            if await self._abac.can_view_property(caller, prop_name):
                if await self._abac.requires_masking(caller, prop_name):
                    prop_data = {**prop_data, "value": "***MASKED***"}
                visible[prop_name] = prop_data
        return visible

    async def _filter_links(self, link_groups: list, caller: CallerContext) -> list:
        """Filter out invisible relationships"""
        visible_groups = []
        for group in link_groups:
            if await self._abac.can_view_link_type(caller, group["link_type_rid"]):
                visible_items = []
                for item in group["items"]:
                    if await self._abac.can_view_instance(caller, item["target_rid"]):
                        visible_items.append(item)
                group["items"] = visible_items
                visible_groups.append(group)
        return visible_groups

#6.1 Masking Rules

Some properties may be visible to the caller but require data masking:

Code
+------------------------------------------------------------------+
|  Masking Rules Example                                            |
|                                                                   |
|  Property Type      Role: admin    Role: viewer    Role: external |
|  ─────────────     ───────────   ────────────   ──────────────    |
|  email             Full display   Full display   a***@domain.com  |
|  phone             Full display   138****5678    Not visible       |
|  salary            Full display   Not visible    Not visible       |
|  id_card           Full display   ****1234       Not visible       |
|  address           Full display   Full display   **Province**City |
+------------------------------------------------------------------+

#7. gRPC Interface Design

InstanceDetailService exposes its interface via gRPC. The Protobuf definition is as follows:

PROTOBUF
syntax = "proto3";

package onto.data.v1;

service InstanceDetailService {
  // Get entity 360° view
  rpc GetEntityDetail(EntityDetailRequest) returns (EntityDetailResponse);

  // Streaming retrieval (dimensions returned one by one)
  rpc StreamEntityDetail(EntityDetailRequest) returns (stream DimensionResultProto);

  // Batch retrieval for multiple entities
  rpc BatchGetEntityDetail(BatchEntityDetailRequest) returns (BatchEntityDetailResponse);
}

message EntityDetailRequest {
  string object_type_rid = 1;
  string instance_rid = 2;
  repeated string dimensions = 3;
  map<string, DimensionConfigProto> dimension_configs = 4;
  string locale = 5;
  optional string world_rid = 6;
}

message DimensionConfigProto {
  bool enabled = 1;
  int32 cache_ttl_seconds = 2;
  int32 max_items = 3;
  repeated string include_fields = 4;
  repeated string exclude_fields = 5;
}

message DimensionResultProto {
  string dimension = 1;
  string status = 2;                    // ok | error | timeout | forbidden
  bytes data_json = 3;                  // JSON-encoded dimension data
  optional string error_message = 4;
  int64 fetched_at_epoch_ms = 5;
  bool from_cache = 6;
}

message EntityDetailResponse {
  string object_type_rid = 1;
  string instance_rid = 2;
  string display_name = 3;
  string object_type_display_name = 4;
  map<string, DimensionResultProto> dimensions = 5;
  int32 total_fetch_ms = 6;
  bool partial_failure = 7;
}

message BatchEntityDetailRequest {
  string object_type_rid = 1;
  repeated string instance_rids = 2;
  repeated string dimensions = 3;
  int32 max_concurrency = 4;
}

message BatchEntityDetailResponse {
  repeated EntityDetailResponse results = 1;
  int32 total_fetch_ms = 2;
}

#7.1 Streaming Interface

For scenarios with many dimensions, the streaming interface allows clients to receive dimension results one at a time rather than waiting for all dimension queries to complete:

Python
class InstanceDetailServicer(InstanceDetailServiceServicer):
    """gRPC service implementation"""

    async def StreamEntityDetail(self, request, context):
        """Stream dimension results back to the client"""
        entity_request = self._convert_request(request)
        caller = self._extract_caller(context)

        for dim in entity_request.dimensions:
            handler = self._registry.get_handler(dim)
            if handler is None:
                continue

            config = self._get_config(dim, entity_request)
            result = await self._fetch_single_dimension(
                handler, entity_request, config, caller
            )

            yield self._to_proto(result)

#8. Cache Invalidation Strategies

Cache invalidation for the 360° View requires fine-grained control. Different dimensions have different data change frequencies and correspondingly different invalidation strategies.

Code
+------------------------------------------------------------------+
|  Cache Invalidation Strategy Matrix                               |
|                                                                   |
|  Dimension       TTL    Event-Driven Invalidation   Degradation   |
|  ──────────     ─────  ────────────────────────     ──────────    |
|  properties     5m     PropertyUpdateEvent          Stale cache   |
|  links          2m     LinkCreateEvent/DeleteEvent   Stale cache   |
|  metrics        1m     MetricRecalcEvent             Stale cache   |
|  time_series    30s    TimeSeriesIngestEvent          Empty        |
|  audit_log      0s     Never cached                   Empty        |
|  action_history 2m     ActionExecutedEvent            Stale cache   |
|  attachments    10m    AttachmentUploadEvent          Stale cache   |
|  derived        1m     DerivedPropertyRecalcEvent     Stale cache   |
|  lineage        30m    LineageUpdateEvent             Stale cache   |
+------------------------------------------------------------------+

Event-driven invalidation is implemented by subscribing to the message queue:

Python
class CacheInvalidator:
    """Cache invalidator: subscribe to events -> delete cache"""

    def __init__(self, cache_manager: CacheManager, event_bus):
        self._cache = cache_manager
        self._event_bus = event_bus

    async def start(self):
        await self._event_bus.subscribe("property.updated", self._on_property_updated)
        await self._event_bus.subscribe("link.created", self._on_link_changed)
        await self._event_bus.subscribe("link.deleted", self._on_link_changed)
        await self._event_bus.subscribe("metric.recalculated", self._on_metric_recalc)

    async def _on_property_updated(self, event: PropertyUpdateEvent):
        cache_key = f"properties:{event.object_type_rid}:{event.instance_rid}"
        await self._cache.invalidate(cache_key)

    async def _on_link_changed(self, event):
        for rid in [event.source_rid, event.target_rid]:
            cache_key = f"links:{event.object_type_rid}:{rid}"
            await self._cache.invalidate(cache_key)

    async def _on_metric_recalc(self, event):
        cache_key = f"metrics:{event.object_type_rid}:{event.instance_rid}"
        await self._cache.invalidate(cache_key)

#9. Performance Optimization: From 450ms to 80ms

#9.1 Query Coalescing

When multiple dimensions query the same data source, coalesce them into a single query:

Python
class QueryCoalescer:
    """Query coalescer: merge multiple queries to the same data source"""

    async def coalesce(self, queries: List[DimensionQuery]) -> Dict[str, Any]:
        # Group by data source
        by_source = defaultdict(list)
        for q in queries:
            by_source[q.data_source].append(q)

        # Execute only one query per data source
        results = {}
        for source, grouped_queries in by_source.items():
            merged_query = self._merge_queries(grouped_queries)
            raw_result = await source.execute(merged_query)
            for q in grouped_queries:
                results[q.dimension] = self._extract_for_dimension(raw_result, q)

        return results

#9.2 Preloading Strategy

For frequently accessed entities, the system can preload 360° View caches in the background:

Python
class PreloadScheduler:
    """Preload scheduler: warm caches for hot entities"""

    async def preload_hot_entities(self):
        """Preload Top 100 frequently accessed entities every 5 minutes"""
        hot_entities = await self._analytics.get_top_accessed_entities(limit=100)

        for entity in hot_entities:
            request = EntityDetailRequest(
                object_type_rid=entity.object_type_rid,
                instance_rid=entity.instance_rid,
                dimensions=list(ViewDimension)
            )
            await self._detail_service.get_entity_detail(
                request, system_context
            )

#9.3 Performance Benchmarks

Before-and-after optimization comparison:

Code
+------------------------------------------------------------------+
|  Performance Benchmarks (9 dimensions, P99 latency)               |
|                                                                   |
|  Optimization Stage    Latency    Improvement                     |
|  ───────────────      ──────    ───────────                       |
|  Sequential queries    450ms     Baseline                         |
|  Parallel queries      120ms     -73%                             |
|  + Cache hits          85ms      -81%                             |
|  + Query coalescing    72ms      -84%                             |
|  + Preloading          45ms      -90% (hot entities)              |
+------------------------------------------------------------------+

#10. SDK Integration

The Python SDK provides a clean 360° View access interface:

Python
from ontology_sdk import OntoPlatform

platform = OntoPlatform(endpoint="grpc://localhost:9090")

# Get entity 360° view
detail = await platform.objects.get_detail(
    object_type="Equipment",
    instance_rid="ri.equipment.main.001",
    dimensions=["properties", "links", "metrics", "time_series"]
)

# Access base properties
print(detail.properties["status"].value)       # "running"
print(detail.properties["temperature"].value)   # 72.5

# Access relationships
for link_group in detail.links.link_groups:
    print(f"{link_group.display_name}: {len(link_group.items)} items")

# Access metrics
for metric in detail.metrics:
    print(f"{metric.display_name}: {metric.value} {metric.unit}")

# Access time-series data
for series in detail.time_series:
    print(f"{series.display_name}: {len(series.points)} points")

# Batch retrieval
details = await platform.objects.batch_get_detail(
    object_type="Equipment",
    instance_rids=["ri.equipment.main.001", "ri.equipment.main.002"],
    dimensions=["properties", "metrics"]
)

#10.1 TypeScript OSDK Integration

TypeScript
import { OntoPlatform } from '@coomia-dip/osdk';

const platform = new OntoPlatform({ endpoint: 'grpc://localhost:9090' });

const detail = await platform.objects.getDetail({
  objectType: 'Equipment',
  instanceRid: 'ri.equipment.main.001',
  dimensions: ['properties', 'links', 'metrics'],
});

// Type-safe property access
console.log(detail.properties.status);      // inferred as string
console.log(detail.properties.temperature); // inferred as number

// Metric access
detail.metrics.forEach(m => {
  console.log(`${m.displayName}: ${m.value} ${m.unit}`);
});

#11. Comparison with Palantir Object View

Code
+------------------------------------------------------------------+
|  Capability Comparison                                            |
|                                                                   |
|  Capability            Palantir Object View   coomia-dip 360° View |
|  ──────────────       ──────────────────     ──────────────       |
|  Base properties       Yes                    Yes                  |
|  Relationships         Yes                    Yes                  |
|  Metric cards          Yes                    Yes                  |
|  Time-series charts    Yes                    Yes                  |
|  Audit log             Yes                    Yes                  |
|  Action execution      Yes                    Yes                  |
|  Attachment mgmt       Yes                    Yes                  |
|  Derived properties    Yes                    Yes                  |
|  Data lineage          Yes                    Yes                  |
|  Field-level perms     Yes                    Yes (ABAC)           |
|  Streaming response    No                     Yes                  |
|  Batch queries         Limited                Yes (concurrency)    |
|  Cache degradation     Unknown                Yes (3-tier)         |
+------------------------------------------------------------------+

#12. Testing Strategy

Python
import pytest
from unittest.mock import AsyncMock, MagicMock

class TestInstanceDetailService:

    @pytest.fixture
    def service(self):
        registry = DimensionRegistry()
        registry.register(ViewDimension.PROPERTIES, MockPropertiesHandler())
        registry.register(ViewDimension.LINKS, MockLinksHandler())
        registry.register(ViewDimension.METRICS, MockMetricsHandler())

        return InstanceDetailService(
            dimension_registry=registry,
            cache_manager=MockCacheManager(),
            permission_filter=MockPermissionFilter(),
            timeout_ms=3000
        )

    @pytest.mark.asyncio
    async def test_parallel_dimension_fetch(self, service):
        """Verify multi-dimension parallel query"""
        request = EntityDetailRequest(
            object_type_rid="ri.type.Equipment",
            instance_rid="ri.inst.001",
            dimensions=[
                ViewDimension.PROPERTIES,
                ViewDimension.LINKS,
                ViewDimension.METRICS
            ]
        )
        response = await service.get_entity_detail(request, mock_caller)
        assert len(response.dimensions) == 3
        assert all(r.status == "ok" for r in response.dimensions.values())

    @pytest.mark.asyncio
    async def test_partial_failure_handling(self, service):
        """Verify partial failure does not affect other dimensions"""
        service._registry._registry[ViewDimension.METRICS] = FailingHandler()

        request = EntityDetailRequest(
            object_type_rid="ri.type.Equipment",
            instance_rid="ri.inst.001",
            dimensions=[ViewDimension.PROPERTIES, ViewDimension.METRICS]
        )
        response = await service.get_entity_detail(request, mock_caller)
        assert response.partial_failure is True
        assert response.dimensions[ViewDimension.PROPERTIES].status == "ok"
        assert response.dimensions[ViewDimension.METRICS].status == "error"

    @pytest.mark.asyncio
    async def test_permission_filtering(self, service):
        """Verify permission trimming is correctly applied"""
        limited_caller = CallerContext(roles=["viewer"], department="sales")

        request = EntityDetailRequest(
            object_type_rid="ri.type.Employee",
            instance_rid="ri.inst.emp.001",
            dimensions=[ViewDimension.PROPERTIES]
        )
        response = await service.get_entity_detail(request, limited_caller)

        props = response.dimensions[ViewDimension.PROPERTIES].data["properties"]
        assert "salary" not in props

    @pytest.mark.asyncio
    async def test_cache_hit(self, service):
        """Verify cache hit behavior"""
        request = EntityDetailRequest(
            object_type_rid="ri.type.Equipment",
            instance_rid="ri.inst.001",
            dimensions=[ViewDimension.PROPERTIES]
        )
        await service.get_entity_detail(request, mock_caller)
        response = await service.get_entity_detail(request, mock_caller)
        assert response.dimensions[ViewDimension.PROPERTIES].from_cache is True

#Key Takeaways

  1. The Entity 360° View is the core data consumption capability of an Ontology platform, consolidating information scattered across multiple data sources into a single unified response via InstanceDetailService
  2. The dimension model is the architecture's core abstraction — each dimension is independently queryable, cacheable, and authorizable, made extensible through the DimensionRegistry pattern
  3. Parallel aggregation + timeout degradation ensures that even when a data source is unavailable, the overall service remains functional, with P99 latency optimized from 450ms down to 80ms
  4. Field-level ABAC permission trimming automatically filters invisible fields and relationships before response, with support for data masking rules
  5. Three-tier cache degradation (normal cache, stale cache, empty response) guarantees high service availability
  6. The gRPC streaming interface allows clients to receive dimension results incrementally, making it well-suited for scenarios with many dimensions

#Next Article

The next article, S3-17 Real-Time Ingestion: Flink CDC Pipeline, will dive deep into how coomia-dip captures database changes in real time through Flink CDC, covering the full pipeline from Debezium Connector to Iceberg table writes.

“

Tags: entity-360-view instance-detail aggregation multi-source caching abac permission-filtering grpc-streaming coomia-dip ontology-platform