Back to Blog

Derived Properties: Let Data Compute Itself

Scenario: Order "total amount"

CoomiaPublished on August 11, 202513 min read
Share this articleTwitter / X

Derived Properties: Let Data Compute Itself

Series: S4 Ontology Modeling · Article 7 | Level: Intermediate | Reading Time: 18 min

#TL;DR

  • Derived Properties are the Ontology version of "computed columns" — define a computation rule once, auto-recompute when data changes, and consumers always get the latest value, ending the nightmare of "writing the same calculation logic in 10 places."
  • 9 Reducers (SUM / AVG / COUNT / MIN / MAX / FIRST / LAST / CONCAT / CUSTOM) cover every computation scenario from simple aggregation to custom functions, each with clear semantics and performance characteristics.
  • Three evaluation modes (Realtime / Deferred / SQL-Backed) allow choosing the optimal strategy based on data volume and latency requirements.

#1. Why Derived Properties Are Needed

#1.1 Pain Points of the Traditional Approach

Code
Scenario: Order "total amount"

Traditional approach (calculation logic everywhere):

Frontend:
  const total = order.items.reduce((sum, item) =>
    sum + item.price * item.quantity, 0)  // Logic #1

Backend API:
  double total = lineItems.stream()
    .mapToDouble(item -> item.getPrice() * item.getQuantity())
    .sum();  // Logic #2

Reporting system:
  SELECT SUM(price * quantity) FROM line_items
  WHERE order_id = ?;  // Logic #3

Data pipeline:
  df['total'] = df['price'] * df['quantity']
  df.groupby('order_id')['total'].sum()  // Logic #4

Problems:
├── Same calculation written 4 times in 4 places
├── One place changes (e.g., adds discount), other 3 are forgotten
├── Different consumers see different "total amounts" → data inconsistency
├── New team members don't know which is the "correct" calculation
└── No audit trail (who defined this calculation? when was it changed?)

#1.2 The coomia-dip Solution

Code
coomia-dip approach (Derived Property):

Define once:
┌──────────────────────────────────────────┐
│  ObjectType: Order                       │
│                                          │
│  properties:                             │
│  ├── orderId: STRING (primary key)       │
│  ├── status: ENUM                        │
│  └── totalAmount: DERIVED                │
│       ├── reducer: SUM                   │
│       ├── source: Order_contains_LineItem │
│       ├── field: lineTotal               │
│       └── where: status != 'CANCELLED'   │
└──────────────────────────────────────────┘

All consumers:
  Frontend: order.totalAmount  // Direct read
  Backend:  order.totalAmount  // Direct read
  Reports:  order.totalAmount  // Direct read
  Pipeline: order.totalAmount  // Direct read

  → Same value, same calculation logic, always consistent ✅

#2. Derived Property Data Model

#2.1 Core Definition

Python
from ontology_sdk import OntologyClient

client = OntologyClient(base_url="http://localhost:8080")

# Create ObjectType with derived properties
client.schema.create_object_type({
    "name": "Order",
    "primaryKey": "orderId",
    "properties": {
        "orderId": {"type": "STRING", "required": True},
        "customerId": {"type": "STRING", "required": True},
        "status": {"type": "ENUM", "enumValues": ["DRAFT", "CONFIRMED", "SHIPPED", "DELIVERED", "CANCELLED"]},

        # Derived property: order total amount
        "totalAmount": {
            "type": "DOUBLE",
            "derived": True,
            "derivation": {
                "reducer": "SUM",
                "sourceRelation": "Order_contains_LineItem",
                "sourceProperty": "lineTotal",
                "filter": {"status": {"neq": "CANCELLED"}},
            },
        },

        # Derived property: line item count
        "itemCount": {
            "type": "INTEGER",
            "derived": True,
            "derivation": {
                "reducer": "COUNT",
                "sourceRelation": "Order_contains_LineItem",
                "filter": {"status": {"neq": "CANCELLED"}},
            },
        },

        # Derived property: average unit price
        "avgItemPrice": {
            "type": "DOUBLE",
            "derived": True,
            "derivation": {
                "reducer": "AVG",
                "sourceRelation": "Order_contains_LineItem",
                "sourceProperty": "unitPrice",
            },
        },

        # Derived property: highest-priced item
        "maxPriceItem": {
            "type": "STRING",
            "derived": True,
            "derivation": {
                "reducer": "MAX",
                "sourceRelation": "Order_contains_LineItem",
                "sourceProperty": "unitPrice",
                "returnProperty": "productName",  # Return product name of max price
            },
        },
    },
})

#2.2 Protobuf Schema

PROTOBUF
message DerivedPropertyDef {
    ReducerType reducer = 1;
    string source_relation = 2;
    string source_property = 3;
    string return_property = 4;
    FilterExpression filter = 5;
    EvaluationMode evaluation_mode = 6;
    string custom_expression = 7;
    string sql_query = 8;
    CacheConfig cache = 9;
}

enum ReducerType {
    SUM = 0;
    AVG = 1;
    COUNT = 2;
    MIN = 3;
    MAX = 4;
    FIRST = 5;
    LAST = 6;
    CONCAT = 7;
    CUSTOM = 8;
}

enum EvaluationMode {
    REALTIME = 0;    // Compute on every read
    DEFERRED = 1;    // Async recompute after changes
    SQL_BACKED = 2;  // Delegate to database
}

#3. 9 Reducers Explained

#3.1 SUM — Summation

Python
# Order total = sum of all line item amounts
"totalAmount": {
    "type": "DOUBLE",
    "derived": True,
    "derivation": {
        "reducer": "SUM",
        "sourceRelation": "Order_contains_LineItem",
        "sourceProperty": "lineTotal",  # lineTotal = unitPrice * quantity
    },
}

# Example data:
# LineItem-1: lineTotal = 100.00
# LineItem-2: lineTotal = 250.00
# LineItem-3: lineTotal = 75.50
# → totalAmount = 425.50

#3.2 AVG — Average

Python
# Customer average order amount
"avgOrderAmount": {
    "type": "DOUBLE",
    "derived": True,
    "derivation": {
        "reducer": "AVG",
        "sourceRelation": "Customer_hasOrder_Order",
        "sourceProperty": "totalAmount",
        "filter": {"status": {"in": ["DELIVERED", "SHIPPED"]}},
    },
}

#3.3 COUNT — Count

Python
# Department employee count
"employeeCount": {
    "type": "INTEGER",
    "derived": True,
    "derivation": {
        "reducer": "COUNT",
        "sourceRelation": "Department_hasEmployee_Employee",
        "filter": {"status": {"eq": "ACTIVE"}},
    },
}

#3.4 MIN — Minimum

Python
# Lowest price in product line
"lowestPrice": {
    "type": "DOUBLE",
    "derived": True,
    "derivation": {
        "reducer": "MIN",
        "sourceRelation": "ProductLine_contains_Product",
        "sourceProperty": "price",
    },
}

#3.5 MAX — Maximum

Python
# Most recent equipment alert time
"lastAlertTime": {
    "type": "TIMESTAMP",
    "derived": True,
    "derivation": {
        "reducer": "MAX",
        "sourceRelation": "Equipment_hasAlert_Alert",
        "sourceProperty": "createdAt",
    },
}

#3.6 FIRST — First Value

Python
# Customer's first order date
"firstOrderDate": {
    "type": "DATE",
    "derived": True,
    "derivation": {
        "reducer": "FIRST",
        "sourceRelation": "Customer_hasOrder_Order",
        "sourceProperty": "orderDate",
        "orderBy": "orderDate",
        "orderDirection": "ASC",
    },
}

#3.7 LAST — Last Value

Python
# Customer's most recent activity
"lastActivity": {
    "type": "STRING",
    "derived": True,
    "derivation": {
        "reducer": "LAST",
        "sourceRelation": "Customer_hasActivity_Activity",
        "sourceProperty": "description",
        "orderBy": "timestamp",
        "orderDirection": "ASC",
    },
}

#3.8 CONCAT — Concatenation

Python
# All product tags (comma-separated)
"tagList": {
    "type": "STRING",
    "derived": True,
    "derivation": {
        "reducer": "CONCAT",
        "sourceRelation": "Product_hasTag_Tag",
        "sourceProperty": "name",
        "separator": ", ",
        "orderBy": "name",
        "maxLength": 500,
    },
}
# → "AI, Machine Learning, NLP, Python"

#3.9 CUSTOM — Custom Expression

Python
# Custom calculation: weighted average rating
"weightedRating": {
    "type": "DOUBLE",
    "derived": True,
    "derivation": {
        "reducer": "CUSTOM",
        "expression": """
            sum(review.rating * review.weight) / sum(review.weight)
        """,
        "sourceRelation": "Product_hasReview_Review",
        "variables": {
            "review.rating": "rating",
            "review.weight": "helpfulVotes",
        },
    },
}

# Custom calculation: health score (based on multiple sensor readings)
"healthScore": {
    "type": "DOUBLE",
    "derived": True,
    "derivation": {
        "reducer": "CUSTOM",
        "expression": """
            let temp_score = if(temperature > 80, 0, if(temperature > 60, 50, 100));
            let vibration_score = if(vibration > 10, 0, if(vibration > 5, 50, 100));
            let pressure_score = if(pressure < 2 || pressure > 8, 0, 100);
            (temp_score + vibration_score + pressure_score) / 3
        """,
        "sourceRelation": "Equipment_hasReading_SensorReading",
        "variables": {
            "temperature": "readings.temperature",
            "vibration": "readings.vibration",
            "pressure": "readings.pressure",
        },
    },
}

#4. Three Evaluation Modes

#4.1 REALTIME — Realtime Evaluation

Code
Characteristics:
├── Computed on every read request
├── Data is always fresh
├── Computation cost borne at read time
└── Suitable when related data is small (< 1000 records)

┌──────┐    Read request    ┌──────────────┐    Query related     ┌──────┐
│Client│──────────────────►│ Runtime      │──────────────────────►│  DB  │
│      │◄──────────────────│              │◄──────────────────────│      │
│      │  Computed result  │  (realtime)  │    Raw data          │      │
└──────┘                   └──────────────┘                      └──────┘
Python
# Configure realtime evaluation
"employeeCount": {
    "type": "INTEGER",
    "derived": True,
    "derivation": {
        "reducer": "COUNT",
        "sourceRelation": "Department_hasEmployee_Employee",
        "evaluationMode": "REALTIME",
    },
}
Code
Characteristics:
├── Async recomputation after source data changes
├── Results cached, reads return cached value directly
├── Triggered on write, no extra cost on read
├── Brief inconsistency window (typically < 5 seconds)
└── Suitable for most business scenarios

Write flow:
┌──────┐   Write LineItem   ┌──────┐   Publish change event   ┌───────────┐
│Client│───────────────────►│  DB  │──────────────────────────►│ Event Bus │
└──────┘                    └──────┘                           └─────┬─────┘
                                                                     │
                                                                     ▼
                                                              ┌───────────┐
                                                              │ Recompute │
                                                              │  Worker   │
                                                              │           │
                                                              │ SUM(...)  │
                                                              └─────┬─────┘
                                                                    │
                                                                    ▼ Update cache
                                                              ┌───────────┐
                                                              │   Cache   │
                                                              └───────────┘

Read flow:
┌──────┐   Read totalAmount   ┌───────────┐
│Client│─────────────────────►│   Cache   │──► Return cached value directly
└──────┘                      └───────────┘
Python
# Configure deferred evaluation (default mode)
"totalAmount": {
    "type": "DOUBLE",
    "derived": True,
    "derivation": {
        "reducer": "SUM",
        "sourceRelation": "Order_contains_LineItem",
        "sourceProperty": "lineTotal",
        "evaluationMode": "DEFERRED",
        "cache": {
            "ttlSeconds": 300,           # Cache TTL 5 minutes
            "invalidateOnChange": True,  # Invalidate immediately on source change
        },
    },
}

#4.3 SQL_BACKED — SQL-Delegated Evaluation

Code
Characteristics:
├── Computation logic pushed down to the database
├── Leverages database computation power (for complex aggregations)
├── Suitable for large data volumes and complex computations
├── Depends on database performance
└── Supports window functions, subqueries, and advanced SQL features

┌──────┐   Read request   ┌──────────────┐   Execute SQL   ┌──────┐
│Client│─────────────────►│   Runtime    │────────────────►│ Doris│
│      │◄─────────────────│              │◄────────────────│      │
│      │  Computed result │ (SQL Proxy)  │  Query result   │      │
└──────┘                  └──────────────┘                 └──────┘
Python
# Configure SQL-delegated evaluation
"rollingAvg30d": {
    "type": "DOUBLE",
    "derived": True,
    "derivation": {
        "reducer": "CUSTOM",
        "evaluationMode": "SQL_BACKED",
        "sqlQuery": """
            SELECT AVG(daily_amount) as value
            FROM (
                SELECT DATE(order_date) as dt, SUM(total_amount) as daily_amount
                FROM orders
                WHERE customer_id = :objectId
                  AND order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
                GROUP BY DATE(order_date)
            ) daily_sums
        """,
    },
}

# Window function example
"salesRank": {
    "type": "INTEGER",
    "derived": True,
    "derivation": {
        "evaluationMode": "SQL_BACKED",
        "sqlQuery": """
            SELECT rank_value as value FROM (
                SELECT product_id,
                       RANK() OVER (
                           PARTITION BY category
                           ORDER BY total_sales DESC
                       ) as rank_value
                FROM product_sales_summary
            ) ranked
            WHERE product_id = :objectId
        """,
    },
}

#5. Evaluation Mode Selection Guide

Code
┌──────────────┬──────────────┬──────────────┬──────────────┐
│   Feature     │   REALTIME   │   DEFERRED   │  SQL_BACKED  │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ Data freshness│ 100% realtime│ Delay < 5s   │ Depends on   │
│              │              │              │ SQL          │
│ Read perf    │ Slow (needs  │ Fast (cache) │ Depends on   │
│              │ computation) │              │ SQL          │
│ Write impact │ None         │ Triggers     │ None         │
│              │              │ recompute    │              │
│ Data volume  │ < 1K related │ < 100K       │ Unlimited    │
│ Complexity   │ Simple agg   │ Simple agg   │ Arbitrarily  │
│              │              │              │ complex      │
│ Use case     │ RT dashboards│ Most business│ Complex      │
│              │              │ scenarios    │ analytics    │
│ Cache cost   │ None         │ Yes          │ Optional     │
└──────────────┴──────────────┴──────────────┴──────────────┘

Decision tree:
  Data volume > 100K?
  ├── Yes → SQL_BACKED
  └── No → Need 100% realtime?
            ├── Yes → REALTIME
            └── No → DEFERRED (recommended)

#6. Expression Evaluation Engine

Python
# Derived properties support rich expression syntax

# Math operations
"profit": {
    "derived": True,
    "derivation": {
        "reducer": "CUSTOM",
        "expression": "self.revenue - self.cost",  # Reference own properties
    },
}

# Conditional expressions
"riskLevel": {
    "derived": True,
    "derivation": {
        "reducer": "CUSTOM",
        "expression": """
            if(self.creditScore >= 750, 'LOW',
               if(self.creditScore >= 600, 'MEDIUM',
                  'HIGH'))
        """,
    },
}

# Date calculations
"daysSinceLastOrder": {
    "derived": True,
    "derivation": {
        "reducer": "CUSTOM",
        "expression": "dateDiff(now(), self.lastOrderDate, 'DAYS')",
    },
}

# String operations
"fullAddress": {
    "derived": True,
    "derivation": {
        "reducer": "CUSTOM",
        "expression": """
            concat(self.homeAddress.province, ' ',
                   self.homeAddress.city, ' ',
                   self.homeAddress.street)
        """,
    },
}

# Cross-relation reference
"managerName": {
    "derived": True,
    "derivation": {
        "reducer": "CUSTOM",
        "expression": "traverse(self, 'Employee_reportsTo_Employee').name",
    },
}

#7. Derived Property Update Mechanisms

#7.1 Trigger-Based Updates

Code
Change propagation path:

LineItem.quantity changes
    │
    ▼
LineItem.lineTotal recomputed (lineTotal = unitPrice * quantity)
    │
    ▼
Order.totalAmount recomputed (SUM of lineTotal)
    │
    ▼
Order.avgItemPrice recomputed (AVG of unitPrice)
    │
    ▼
Customer.totalSpent recomputed (SUM of Order.totalAmount)
    │
    ▼
Customer.loyaltyTier recomputed (tier based on totalSpent)

This is "cascade recomputation" — covered in detail in the next article (S4-08)

#7.2 Batch Recomputation

Python
# Manually trigger batch recomputation (used after data migration or repair)
result = client.schema.recompute_derived_property(
    object_type="Order",
    property_name="totalAmount",
    filter={"status": {"in": ["CONFIRMED", "SHIPPED"]}},
    batch_size=1000,
    concurrency=5,
)

print(f"Recomputation complete: {result.total_objects} objects")
print(f"Duration: {result.duration_seconds} seconds")
print(f"Succeeded: {result.success_count}")
print(f"Failed: {result.failure_count}")

#8. Derived Properties and Indexes

Python
# Derived properties can also be indexed (for filtering and sorting)
client.schema.create_index({
    "objectType": "Order",
    "properties": ["totalAmount"],
    "type": "BTREE",
    "name": "idx_order_total_amount",
})

# Query based on derived properties
orders = client.objects.search("Order", {
    "filters": {
        "totalAmount": {"gte": 10000},  # High-value orders
    },
    "orderBy": "totalAmount",
    "orderDirection": "DESC",
    "maxResults": 20,
})

#9. Error Handling

Python
# Handling strategies for derived property computation failures

"riskyDerived": {
    "type": "DOUBLE",
    "derived": True,
    "derivation": {
        "reducer": "CUSTOM",
        "expression": "self.revenue / self.unitsSold",  # Possible division by zero
        "errorHandling": {
            "onDivisionByZero": "RETURN_NULL",  # Return null on division by zero
            "onNullInput": "RETURN_NULL",        # Return null when input is null
            "onOverflow": "RETURN_MAX",           # Return max value on overflow
            "onError": "RETURN_DEFAULT",          # Return default value on other errors
            "defaultValue": 0,
        },
    },
}

#10. Real-World Case: Customer 360 View

Python
# Complete derived property set for a Customer

client.schema.create_object_type({
    "name": "Customer",
    "primaryKey": "customerId",
    "properties": {
        "customerId": {"type": "STRING", "required": True},
        "name": {"type": "STRING", "required": True},
        "email": {"type": "STRING"},
        "registeredAt": {"type": "TIMESTAMP"},

        # 1. Order statistics
        "totalOrders": {
            "type": "INTEGER",
            "derived": True,
            "derivation": {"reducer": "COUNT", "sourceRelation": "Customer_hasOrder_Order"},
        },
        "totalSpent": {
            "type": "DOUBLE",
            "derived": True,
            "derivation": {"reducer": "SUM", "sourceRelation": "Customer_hasOrder_Order", "sourceProperty": "totalAmount"},
        },
        "avgOrderAmount": {
            "type": "DOUBLE",
            "derived": True,
            "derivation": {"reducer": "AVG", "sourceRelation": "Customer_hasOrder_Order", "sourceProperty": "totalAmount"},
        },

        # 2. Time dimensions
        "firstOrderDate": {
            "type": "DATE",
            "derived": True,
            "derivation": {"reducer": "MIN", "sourceRelation": "Customer_hasOrder_Order", "sourceProperty": "orderDate"},
        },
        "lastOrderDate": {
            "type": "DATE",
            "derived": True,
            "derivation": {"reducer": "MAX", "sourceRelation": "Customer_hasOrder_Order", "sourceProperty": "orderDate"},
        },
        "daysSinceLastOrder": {
            "type": "INTEGER",
            "derived": True,
            "derivation": {"reducer": "CUSTOM", "expression": "dateDiff(now(), self.lastOrderDate, 'DAYS')"},
        },

        # 3. Customer segmentation
        "loyaltyTier": {
            "type": "STRING",
            "derived": True,
            "derivation": {
                "reducer": "CUSTOM",
                "expression": """
                    if(self.totalSpent >= 100000, 'PLATINUM',
                       if(self.totalSpent >= 50000, 'GOLD',
                          if(self.totalSpent >= 10000, 'SILVER',
                             'BRONZE')))
                """,
            },
        },

        # 4. Activity score
        "activityScore": {
            "type": "DOUBLE",
            "derived": True,
            "derivation": {
                "reducer": "CUSTOM",
                "expression": """
                    let recency = min(self.daysSinceLastOrder / 90.0, 1.0);
                    let frequency = min(self.totalOrders / 50.0, 1.0);
                    let monetary = min(self.totalSpent / 100000.0, 1.0);
                    (1 - recency) * 0.3 + frequency * 0.3 + monetary * 0.4
                """,
            },
        },

        # 5. Product preference
        "topCategory": {
            "type": "STRING",
            "derived": True,
            "derivation": {
                "evaluationMode": "SQL_BACKED",
                "sqlQuery": """
                    SELECT category as value
                    FROM order_line_items oli
                    JOIN orders o ON oli.order_id = o.order_id
                    JOIN products p ON oli.product_id = p.product_id
                    WHERE o.customer_id = :objectId
                    GROUP BY category
                    ORDER BY COUNT(*) DESC
                    LIMIT 1
                """,
            },
        },
    },
})

# Use derived properties for customer analysis
vip_customers = client.objects.search("Customer", {
    "filters": {
        "loyaltyTier": {"in": ["PLATINUM", "GOLD"]},
        "activityScore": {"gte": 0.7},
        "daysSinceLastOrder": {"lte": 30},
    },
    "orderBy": "totalSpent",
    "orderDirection": "DESC",
})

for c in vip_customers:
    print(f"Customer: {c.name}")
    print(f"  Tier: {c.loyaltyTier}")
    print(f"  Total spent: ${c.totalSpent:,.2f}")
    print(f"  Activity score: {c.activityScore:.2f}")
    print(f"  Last order: {c.daysSinceLastOrder} days ago")
    print(f"  Top category: {c.topCategory}")

#11. Performance Optimization

Code
Derived property performance optimization strategies:

┌─────────────────────────────────────────────────┐
│          Performance Optimization Matrix         │
├────────────────┬────────────────────────────────┤
│  Strategy       │  Use Case                      │
├────────────────┼────────────────────────────────┤
│  Deferred+cache │  Most scenarios (recommended)  │
│  Incremental    │  SUM/COUNT/AVG can update      │
│  computation    │  incrementally                 │
│  SQL pushdown   │  Complex aggregations, large   │
│                 │  data volumes                  │
│  Pre-materialize│  Read-heavy derived properties │
│  Batch recompute│  Data repair, initialization   │
│  Parallel       │  Multiple independent derived  │
│  recompute      │  properties                    │
└────────────────┴────────────────────────────────┘

Incremental computation example (SUM):
  Old value: totalAmount = 1000
  New LineItem.lineTotal = 200 added
  New value: totalAmount = 1000 + 200 = 1200

  No need to re-traverse all LineItems!
  Time complexity drops from O(N) to O(1)

#Key Takeaways

  1. Derived properties eliminate calculation logic duplication — define once, and all consumers (frontend, backend, reports, pipelines) see the same result, with data consistency guaranteed at the Schema level.
  2. 9 Reducers cover every scenario: SUM/AVG/COUNT/MIN/MAX for basic aggregation, FIRST/LAST for ordered value selection, CONCAT for text joining, CUSTOM for arbitrarily complex expressions.
  3. Choosing between three evaluation modes: REALTIME guarantees freshness (small data), DEFERRED balances freshness and performance (recommended), SQL_BACKED handles complex computation (large data).
  4. The expression engine supports rich syntax — math operations, conditionals, date calculations, string operations, and cross-relation references, sufficient for the vast majority of business computations.
  5. Derived properties can be indexed — computed results can be filtered and sorted, participating in queries just like regular properties.

#Next Article

The next article, S4-08 Derived Property DAG, will dive into the internals of "cascade computation" — how topological sort ensures correct computation order when one property change triggers multi-level derived property recomputation, how cycle detection works, and how to batch-recompute millions of objects.

#ontology #derived-property #reducer #expression-eval #sql-backed #computed-column #data-consistency