Data Modeling Best Practices
Effective data modeling is the foundation of a performant dcupl application. This guide covers patterns and recommendations for structuring your models, properties, and relationships.
Core Principles
Good data models share these characteristics:
- Explicit structure - Every property has a defined type
- Minimal duplication - Use references instead of copying data
- Query-optimized - Properties used in filters are indexed
- Right-sized - Only include properties you actually need
Property Design
Choose Specific Types
Use the most specific type for each property. This enables better validation and optimization.
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'quantity', type: 'int' },
{ key: 'isActive', type: 'boolean' },
{ key: 'tags', type: 'Array<string>' },
{ key: 'createdAt', type: 'date' },
],
};const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'string' }, // Should be 'float'
{ key: 'quantity', type: 'string' }, // Should be 'int'
{ key: 'isActive', type: 'string' }, // Should be 'boolean'
{ key: 'tags', type: 'string' }, // Should be 'Array<string>'
{ key: 'createdAt', type: 'string' }, // Should be 'date'
],
};Available types: string, int, float, boolean, date, Array, Array, Array, json
Index Properties for Filtering
Properties you filter on should be indexed for fast queries. dcupl indexes properties automatically, but you can add explicit indexing for frequently looked-up keys:
const productModel: ModelDefinition = {
key: 'Product',
properties: [
// Standard properties (auto-indexed for filtering)
{ key: 'name', type: 'string' },
{ key: 'category', type: 'string' },
{ key: 'price', type: 'float' },
// Explicit index for fast key lookups
{ key: 'sku', type: 'string', index: true },
// Display-only (no filtering needed)
{ key: 'description', type: 'string' },
],
};Only Include What You Need
Every property increases memory usage and processing time. Remove properties you do not query or display.
// Lean model - only what the UI needs
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'category', type: 'string' },
{ key: 'imageUrl', type: 'string' },
],
};// Bloated model - includes unused fields
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'category', type: 'string' },
{ key: 'imageUrl', type: 'string' },
{ key: 'legacyId', type: 'string' }, // Never used
{ key: 'internalNotes', type: 'string' }, // Never used
{ key: 'importTimestamp', type: 'string' }, // Never used
{ key: 'rawData', type: 'json' }, // Never used
],
};Reference Design
Normalize Related Data
Use references instead of duplicating data across models. This keeps data consistent and reduces memory usage.
// Normalized: Customer data lives in one place
const orderModel: ModelDefinition = {
key: 'Order',
properties: [
{ key: 'orderNumber', type: 'string' },
{ key: 'total', type: 'float' },
{ key: 'status', type: 'string' },
],
references: [{ key: 'customer', model: 'Customer', type: 'singleValued' }],
};
const customerModel: ModelDefinition = {
key: 'Customer',
properties: [
{ key: 'name', type: 'string' },
{ key: 'email', type: 'string' },
{ key: 'phone', type: 'string' },
],
};// Denormalized: Customer data duplicated in every order
const orderModel: ModelDefinition = {
key: 'Order',
properties: [
{ key: 'orderNumber', type: 'string' },
{ key: 'total', type: 'float' },
{ key: 'status', type: 'string' },
{ key: 'customerName', type: 'string' }, // Duplicated
{ key: 'customerEmail', type: 'string' }, // Duplicated
{ key: 'customerPhone', type: 'string' }, // Duplicated
],
};Use Correct Reference Types
Choose the right reference type based on the relationship:
| Relationship | Reference Type | Example |
|---|---|---|
| Many-to-one | singleValued |
Order has one Customer |
| Many-to-many | multiValued |
Product has many Tags |
const productModel: ModelDefinition = {
key: 'Product',
properties: [{ key: 'name', type: 'string' }],
references: [
// Many products belong to one category
{ key: 'category', model: 'Category', type: 'singleValued' },
// One product has many tags
{ key: 'tags', model: 'Tag', type: 'multiValued' },
],
};Limit Reference Depth
Keep reference chains to 2-3 levels. Deep chains slow down queries significantly.
// 2-level depth - fast queries
orderList.catalog.query.addCondition({
attribute: 'customer.country',
operator: 'eq',
value: 'USA',
});// 4-level depth - very slow
orderList.catalog.query.addCondition({
attribute: 'customer.address.country.region',
operator: 'eq',
value: 'West Coast',
});Performance impact by depth:
| Depth | Relative Speed |
|---|---|
| 1 level | Baseline |
| 2 levels | 4-6x slower |
| 3 levels | 6-9x slower |
| 4+ levels | 10-15x+ slower |
Use Derived Properties for Deep Queries
For frequently-accessed deep data, use derived properties to flatten the access path:
const orderModel: ModelDefinition = {
key: 'Order',
properties: [
{ key: 'orderNumber', type: 'string' },
{ key: 'total', type: 'float' },
// Derived property for fast queries
{
key: 'customerCountry',
type: 'string',
derive: {
localReference: 'customer',
remoteProperty: 'country',
},
},
],
references: [{ key: 'customer', model: 'Customer', type: 'singleValued' }],
};
// Fast: Query derived property directly
orderList.catalog.query.addCondition({
attribute: 'customerCountry',
operator: 'eq',
value: 'USA',
});Avoid Circular References
Circular references are detected and rejected. Design one-way relationships and query in reverse when needed.
// One-way reference: Order -> Customer
const orderModel: ModelDefinition = {
key: 'Order',
properties: [{ key: 'orderNumber', type: 'string' }],
references: [{ key: 'customer', model: 'Customer', type: 'singleValued' }],
};
const customerModel: ModelDefinition = {
key: 'Customer',
properties: [{ key: 'name', type: 'string' }],
// No reference back to Order
};
// Query orders for a customer (reverse direction)
orderList.catalog.query.addCondition({
attribute: 'customer',
operator: 'eq',
value: 'customer-123',
});// Circular reference: Order -> Customer -> Order
const orderModel: ModelDefinition = {
key: 'Order',
references: [{ key: 'customer', model: 'Customer', type: 'singleValued' }],
};
const customerModel: ModelDefinition = {
key: 'Customer',
references: [
{ key: 'orders', model: 'Order', type: 'multiValued' }, // Circular!
],
};
// Error: Circular reference detectedModel Organization
One Model Per Entity
Each distinct entity should have its own model. Do not combine unrelated data.
// Separate models for distinct entities
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
],
};
const categoryModel: ModelDefinition = {
key: 'Category',
properties: [
{ key: 'name', type: 'string' },
{ key: 'slug', type: 'string' },
],
};// Generic model for everything
const entityModel: ModelDefinition = {
key: 'Entity',
properties: [
{ key: 'type', type: 'string' }, // 'product' or 'category'
{ key: 'data', type: 'json' }, // Unstructured blob
],
};Consistent Key Naming
Use consistent naming conventions across all models:
// Recommended: camelCase for properties, PascalCase for models
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'productName', type: 'string' },
{ key: 'listPrice', type: 'float' },
{ key: 'isAvailable', type: 'boolean' },
{ key: 'createdAt', type: 'date' },
],
references: [{ key: 'parentCategory', model: 'Category', type: 'singleValued' }],
};Data Key Strategy
Every item needs a unique key. Choose a strategy that fits your data:
Use Natural Keys When Available
// Products with SKU
dcupl.data.set(
[
{ key: 'SKU-12345', name: 'Laptop', price: 999 },
{ key: 'SKU-67890', name: 'Mouse', price: 29 },
],
{ model: 'Product' }
);
// Users with email
dcupl.data.set(
[
{ key: 'alice@example.com', name: 'Alice' },
{ key: 'bob@example.com', name: 'Bob' },
],
{ model: 'User' }
);Generate Keys for New Data
function generateKey(): string {
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
// Or use a library like nanoid
import { nanoid } from 'nanoid';
const newProduct = {
key: nanoid(),
name: 'New Product',
price: 49.99,
};Common Patterns
E-Commerce Product Catalog
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'sku', type: 'string', index: true },
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'salePrice', type: 'float' },
{ key: 'inStock', type: 'boolean' },
{ key: 'rating', type: 'float' },
{ key: 'imageUrl', type: 'string' },
],
references: [
{ key: 'category', model: 'Category', type: 'singleValued' },
{ key: 'brand', model: 'Brand', type: 'singleValued' },
{ key: 'tags', model: 'Tag', type: 'multiValued' },
],
};
const categoryModel: ModelDefinition = {
key: 'Category',
properties: [
{ key: 'name', type: 'string' },
{ key: 'slug', type: 'string', index: true },
{ key: 'parentId', type: 'string' },
],
};
const brandModel: ModelDefinition = {
key: 'Brand',
properties: [
{ key: 'name', type: 'string' },
{ key: 'logoUrl', type: 'string' },
],
};Content Management
const articleModel: ModelDefinition = {
key: 'Article',
properties: [
{ key: 'slug', type: 'string', index: true },
{ key: 'title', type: 'string' },
{ key: 'excerpt', type: 'string' },
{ key: 'content', type: 'string' },
{ key: 'status', type: 'string' },
{ key: 'publishedAt', type: 'date' },
],
references: [
{ key: 'author', model: 'Author', type: 'singleValued' },
{ key: 'categories', model: 'Category', type: 'multiValued' },
{ key: 'tags', model: 'Tag', type: 'multiValued' },
],
};Order Management
const orderModel: ModelDefinition = {
key: 'Order',
properties: [
{ key: 'orderNumber', type: 'string', index: true },
{ key: 'status', type: 'string' },
{ key: 'total', type: 'float' },
{ key: 'itemCount', type: 'int' },
{ key: 'createdAt', type: 'date' },
// Derived for fast queries
{
key: 'customerEmail',
type: 'string',
derive: {
localReference: 'customer',
remoteProperty: 'email',
},
},
],
references: [
{ key: 'customer', model: 'Customer', type: 'singleValued' },
{ key: 'items', model: 'OrderItem', type: 'multiValued' },
],
};
const orderItemModel: ModelDefinition = {
key: 'OrderItem',
properties: [
{ key: 'quantity', type: 'int' },
{ key: 'unitPrice', type: 'float' },
{ key: 'lineTotal', type: 'float' },
],
references: [{ key: 'product', model: 'Product', type: 'singleValued' }],
};Modeling Checklist
Before finalizing your models, verify:
- Every property has a specific type (not just
stringfor everything) - Properties used in filters are defined
- Relationships use references instead of duplicated data
- Reference depth is 3 levels or less
- Deep query paths use derived properties
- No circular references exist
- Unused properties are removed
- Keys are unique and meaningful
What's Next?
- Performance - Optimize queries and updates
- Error Handling - Handle errors gracefully
- Models - Property types, references, and options