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<string>, Array<int>, Array<float>, Array<date>, json, any
Index properties for filtering
Properties you filter on should be indexed for fast queries. dcupl always indexes references and builds a property's index on demand (for example the first time you request facets for it); until then, filters on that property scan the items. Set index: true to build the index while the data is processed, so eq and in filters on frequently looked-up keys use it from the first query:
const productModel: ModelDefinition = {
key: 'Product',
properties: [
// Standard properties (indexed on demand, e.g. for facets)
{ 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' },
],
};filter: true on a property doesn't index anything: it only selects the attributes that catalog.filters returns for building filter UIs.
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
],
};Mark optional properties
Every declared property and reference is required by default. An item without a value for one of them records an UndefinedValue quality error (a single UndefinedAttribute when no item has the attribute at all). init() still resolves true, the item loads without the value and nothing is logged, so read dcupl.quality.errors.summary() during development. Set quality: { required: false } on attributes that are missing on some items (a value sent as null is a separate NullValue error, which quality: { nullable: true } allows):
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
// Only products on sale have a sale price
{ key: 'salePrice', type: 'float', quality: { required: false } },
],
};To make most attributes of a model optional, set quality: { attributes: { required: false } } on the model and mark the must-have ones quality: { required: true }. References take required too: the root of a category tree has no parent. See Model-wide Defaults.
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 |
| One-to-many | multiValued, or a singleValued reference on the child plus a resolved reference on the parent |
Order has many OrderItems |
| 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',
});Each level resolves one more reference for every item the condition evaluates, so the cost grows with the depth and with the number of items. How much depends on your data and on whether the list has already resolved the path, so measure with your own data (see Profiling and Debugging).
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 between models are detected and rejected. Design one-way relationships and query in reverse when needed.
A reference from a model to itself is not a cycle: a Category with a parent reference to Category is fine. dcupl resolves such references against all rows of the model, whatever the row order, so hierarchies work with deep queries (parent.name) and facets.
// 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!
],
};
// await dcupl.init() logs "Circular dependency detected: Order → Customer → Order"
// and resolves falseIf you need the orders on each customer, declare a resolved reference instead. It collects the items that point at this one and is not part of the dependency check, so it doesn't create a cycle:
const customerModel: ModelDefinition = {
key: 'Customer',
properties: [{ key: 'name', type: 'string' }],
references: [
// Every Order whose `customer` reference points at this customer
{ key: 'orders', model: 'Order', type: 'multiValued', resolve: { model: 'Order', reference: 'customer' } },
],
};See Models API for resolved and derived references.
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:
// 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. If the identifier is in another column, set keyProperty on the model instead of copying it into key: keyProperty: 'sku', or an array such as ['sku', 'currency'] for a composite key (stored as 'SKU-1::EUR'; see Types).
Duplicate or missing keys don't stop loading. When rows loaded together (one data.set() call, a model's data or one resource) share a key, the last row wins as a whole and dcupl records a NonUniqueKey quality error for each row it replaced. A row without a key (no value for keyProperty, or for every part of a composite key) is dropped as MissingKey; a composite key with only some parts missing keeps each missing part as 'undefined' ('null' for a null value), e.g. 'undefined::EUR'. On a model with derived, grouped, resolved or query references, or derived or expression properties, a keyless row currently makes init() (or an update() that rebuilds) log a failure and resolve false, so remove such rows before loading. After init(), a keyless row in a data.upsert() that a partial update applies is currently dropped without a MissingKey record; the next full rebuild records it, and on those models resolves false. See Error Types.
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', quality: { required: false } },
{ 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 },
],
references: [
// Root categories have no parent
{ key: 'parent', model: 'Category', type: 'singleValued', quality: { required: false } },
],
};
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) - Exact-match lookups use references or
index: trueproperties - Relationships use references instead of duplicated data
- Reference depth is 3 levels or less
- Deep query paths use derived properties
- No circular references between models exist (a model may reference itself)
- Unused properties are removed
- Optional properties and references have
quality: { required: false } - Keys are unique and meaningful