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:

indexed-properties.ts
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

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
reference-types.ts
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:

derived-property.ts
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 detected

Model 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:

naming.ts
// 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

natural-keys.ts
// 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

generated-keys.ts
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

ecommerce-model.ts
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

cms-model.ts
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

order-model.ts
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 string for 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?