Best Practices & Performance
Learn essential patterns and optimization techniques for building high-performance graph applications with Ductape.
Data Modeling
Use Descriptive Labels
// Good - clear and specific
await ductape.graph.createNode({
labels: ['Person', 'Employee', 'Engineer'],
properties: { name: 'Alice' },
});
// ❌ Avoid - too generic
await ductape.graph.createNode({
labels: ['Node'],
properties: { name: 'Alice', type: 'person' },
});
Benefits:
- Faster queries (filtered at label level)
- Better code readability
- Easier to create targeted indexes
Model Relationships Correctly
// Good - relationship types are verbs
await ductape.graph.createRelationship({
type: 'WORKS_FOR',
startNodeId: personId,
endNodeId: companyId,
});
await ductape.graph.createRelationship({
type: 'FRIENDS_WITH',
startNodeId: user1Id,
endNodeId: user2Id,
});
// ❌ Avoid - nouns or unclear relationships
await ductape.graph.createRelationship({
type: 'PERSON_COMPANY',
startNodeId: personId,
endNodeId: companyId,
});
Property vs. Node Decision
Use a property when:
- Simple value (string, number, date)
- Doesn't need its own relationships
- Not queried independently
// Property
await ductape.graph.createNode({
labels: ['Person'],
properties: {
name: 'Alice',
age: 28,
city: 'New York', // Simple value
},
});
Use a node when:
- Has its own properties
- Can have relationships
- Queried independently
- Reused across many nodes
// Separate nodes
const alice = await ductape.graph.createNode({
labels: ['Person'],
properties: { name: 'Alice', age: 28 },
});
const nyc = await ductape.graph.createNode({
labels: ['City'],
properties: {
name: 'New York',
population: 8000000,
timezone: 'EST',
},
});
await ductape.graph.createRelationship({
type: 'LIVES_IN',
startNodeId: alice.node.id,
endNodeId: nyc.node.id,
properties: { since: 2020 },
});
Avoid Property Overloading
// ❌ Bad - properties doing too much
await ductape.graph.createNode({
labels: ['User'],
properties: {
name: 'Alice',
emails: 'alice@work.com,alice@personal.com', // Comma-separated
skills: 'JS,TS,Python,React', // Comma-separated
},
});
// Good - structured properly
await ductape.graph.createNode({
labels: ['User'],
properties: {
name: 'Alice',
emails: ['alice@work.com', 'alice@personal.com'], // Array
skills: ['JavaScript', 'TypeScript', 'Python', 'React'], // Array
},
});
// Even better - use nodes for complex relationships
const alice = await ductape.graph.createNode({
labels: ['User'],
properties: { name: 'Alice' },
});
const javascript = await ductape.graph.createNode({
labels: ['Skill'],
properties: { name: 'JavaScript' },
});
await ductape.graph.createRelationship({
type: 'HAS_SKILL',
startNodeId: alice.node.id,
endNodeId: javascript.node.id,
properties: {
level: 'expert',
yearsExperience: 5,
},
});
Indexing Strategy
Index High-Cardinality Properties
// Good - index unique or near-unique values
await ductape.graph.createNodeIndex({
name: 'idx_user_email',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['email'], // High cardinality
});
await ductape.graph.createNodeIndex({
name: 'idx_product_sku',
type: NodeIndexType.BTREE,
label: 'Product',
properties: ['sku'], // High cardinality
});
// ❌ Don't index low-cardinality properties alone
// Bad index on boolean (only 2 values)
await ductape.graph.createNodeIndex({
name: 'idx_user_active',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['isActive'], // Only true/false
});
Use Composite Indexes Wisely
// Good - matches query patterns
await ductape.graph.createNodeIndex({
name: 'idx_order_user_status',
type: NodeIndexType.BTREE,
label: 'Order',
properties: ['userId', 'status'], // Most selective first
});
// Efficiently supports:
const orders = await ductape.graph.findNodes({
labels: ['Order'],
where: {
userId: '123',
status: 'pending',
},
});
Create Constraints for Uniqueness
// Use constraints instead of just indexes
await ductape.graph.createNodeConstraint({
name: 'unique_user_email',
type: NodeConstraintType.UNIQUE,
label: 'User',
properties: ['email'],
});
// Automatically creates an index + enforces uniqueness
Query Optimization
Filter Early
// Good - filter at query time
const users = await ductape.graph.findNodes({
labels: ['User'],
where: {
status: 'active',
city: 'New York',
},
limit: 10,
});
// ❌ Bad - fetch all then filter in code
const allUsers = await ductape.graph.findNodes({
labels: ['User'],
});
const filtered = allUsers.nodes.filter(
u => u.properties.status === 'active' && u.properties.city === 'New York'
).slice(0, 10);
Use Specific Labels
// Good - specific label
const engineers = await ductape.graph.findNodes({
labels: ['Engineer'],
where: { experience: { $GT: 5 } },
});
// ❌ Slower - generic label + property filter
const engineers = await ductape.graph.findNodes({
labels: ['Person'],
where: {
type: 'engineer',
experience: { $GT: 5 },
},
});
Limit Traversal Depth
// Good - reasonable depth
const network = await ductape.graph.traverse({
startNodeId: userId,
relationshipTypes: ['FRIENDS_WITH'],
maxDepth: 3, // Friends of friends of friends
});
// ❌ Dangerous - could explore millions of nodes
const network = await ductape.graph.traverse({
startNodeId: userId,
maxDepth: 10, // Exponential growth
});
Use Relationship Types
// Good - specific relationship types
const friends = await ductape.graph.traverse({
startNodeId: userId,
relationshipTypes: ['FRIENDS_WITH', 'KNOWS'],
maxDepth: 2,
});
// ❌ Slower - follows all relationships
const connections = await ductape.graph.traverse({
startNodeId: userId,
// No relationshipTypes specified
maxDepth: 2,
});
Paginate Large Result Sets
// Good - paginate results
async function getUsersPaginated(page: number = 1, pageSize: number = 50) {
const offset = (page - 1) * pageSize;
const result = await ductape.graph.findNodes({
labels: ['User'],
where: { status: 'active' },
limit: pageSize,
skip: offset,
});
return {
users: result.nodes,
page,
pageSize,
hasMore: result.nodes.length === pageSize,
};
}
Performance Patterns
Batch Operations in Transactions
// Good - batch in single transaction
await ductape.graph.executeTransaction(async (tx) => {
for (const userData of largeUserList) {
await ductape.graph.createNode({
labels: ['User'],
properties: userData,
}, tx);
}
});
// ❌ Bad - separate transaction per operation
for (const userData of largeUserList) {
await ductape.graph.createNode({
labels: ['User'],
properties: userData,
});
}
Cache Frequently Accessed Data
// Good - cache popular queries
const cache = new Map<string, any>();
async function getPopularPosts() {
const cacheKey = 'popular_posts';
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
return cached.data; // Return cached data (5 min TTL)
}
const posts = await ductape.graph.findNodes({
labels: ['Post'],
where: { likes: { $GT: 1000 } },
limit: 10,
});
cache.set(cacheKey, {
data: posts.nodes,
timestamp: Date.now(),
});
return posts.nodes;
}
Use Connection Pooling
// Good - reuse connections
const ductape = new Ductape({
workspace_id: 'workspace-123',
user_id: 'user-456',
private_key: 'your-private-key',
});
// Register once at startup
await ductape.graph.register({
tag: 'main-graph',
driver: GraphDriver.NEO4J,
config: {
uri: 'neo4j://localhost:7687',
username: 'neo4j',
password: 'password',
},
options: {
maxConnectionPoolSize: 50,
connectionTimeout: 30000,
},
});
// Connect once
await ductape.graph.connect({ tag: 'main-graph' });
// Reuse connection for all operations
// Connection is automatically pooled
Avoid N+1 Query Problems
// ❌ Bad - N+1 queries
const users = await ductape.graph.findNodes({
labels: ['User'],
limit: 10,
});
// Separate query for each user's posts (N queries)
for (const user of users.nodes) {
const posts = await ductape.graph.findRelationships({
startNodeId: user.id,
type: 'POSTED',
});
user.posts = posts.relationships;
}
// Good - single query with pattern
const usersWithPosts = await ductape.graph.query({
query: `
MATCH (u:User)-[r:POSTED]->(p:Post)
WHERE u.status = $status
RETURN u, collect({post: p, relationship: r}) as posts
LIMIT 10
`,
params: { status: 'active' },
});
Data Consistency
Use Transactions for Multi-Step Operations
// Always use transactions for related operations
await ductape.graph.executeTransaction(async (tx) => {
const order = await ductape.graph.createNode({
labels: ['Order'],
properties: { total: 100, status: 'pending' },
}, tx);
await ductape.graph.updateNode({
id: userId,
properties: { orderCount: { $INCREMENT: 1 } },
}, tx);
await ductape.graph.createRelationship({
type: 'PLACED',
startNodeId: userId,
endNodeId: order.node.id,
}, tx);
});
Validate Data Before Writing
// Good - validate first
function validateUser(data: any) {
if (!data.email || !data.email.includes('@')) {
throw new Error('Invalid email');
}
if (!data.name || data.name.length < 2) {
throw new Error('Name too short');
}
return true;
}
async function createUser(data: any) {
validateUser(data);
return ductape.graph.createNode({
labels: ['User'],
properties: data,
});
}
Use Merge for Idempotent Operations
// Good - merge is idempotent
const user = await ductape.graph.mergeNode({
labels: ['User'],
matchProperties: { email: 'alice@example.com' },
onCreate: {
email: 'alice@example.com',
name: 'Alice',
createdAt: new Date(),
},
onMatch: {
lastSeen: new Date(),
},
});
// Safe to call multiple times
Schema Design
Denormalize Carefully
Sometimes denormalization improves performance:
// Store frequently accessed counts
await ductape.graph.createNode({
labels: ['User'],
properties: {
name: 'Alice',
followerCount: 1234, // Denormalized count
followingCount: 567, // Denormalized count
},
});
// Update counts when relationships change
await ductape.graph.executeTransaction(async (tx) => {
// Create follow relationship
await ductape.graph.createRelationship({
type: 'FOLLOWS',
startNodeId: user1Id,
endNodeId: user2Id,
}, tx);
// Update denormalized counts
await ductape.graph.updateNode({
id: user1Id,
properties: { followingCount: { $INCREMENT: 1 } },
}, tx);
await ductape.graph.updateNode({
id: user2Id,
properties: { followerCount: { $INCREMENT: 1 } },
}, tx);
});
Use Intermediate Nodes for Complex Relationships
// ❌ Basic - loses information
await ductape.graph.createRelationship({
type: 'ENROLLED_IN',
startNodeId: studentId,
endNodeId: courseId,
properties: {
grade: 'A',
semester: 'Fall 2024',
},
});
// Better - enrollment as node
const enrollment = await ductape.graph.createNode({
labels: ['Enrollment'],
properties: {
grade: 'A',
semester: 'Fall 2024',
credits: 3,
status: 'completed',
},
});
await ductape.graph.createRelationship({
type: 'HAS_ENROLLMENT',
startNodeId: studentId,
endNodeId: enrollment.node.id,
});
await ductape.graph.createRelationship({
type: 'FOR_COURSE',
startNodeId: enrollment.node.id,
endNodeId: courseId,
});
Version Your Schema
// Add schema version to nodes
await ductape.graph.createNode({
labels: ['User'],
properties: {
name: 'Alice',
email: 'alice@example.com',
schemaVersion: 2, // Track schema version
},
});
// Handle multiple versions
async function getUser(id: string) {
const user = await ductape.graph.findNodeById(id);
if (user.properties.schemaVersion === 1) {
// Migrate old schema
return migrateUserV1ToV2(user);
}
return user;
}
Error Handling
Catch Specific Errors
// Good - handle specific errors
try {
await ductape.graph.createNode({
labels: ['User'],
properties: { email: 'alice@example.com' },
});
} catch (error) {
if (error.message.includes('constraint')) {
console.log('Email already exists');
// Return existing user or show error to user
} else if (error.message.includes('connection')) {
console.log('Database connection failed');
// Retry or show maintenance message
} else {
console.error('Unexpected error:', error);
// Log and report
}
}
Implement Retry Logic
// Retry transient errors
async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
const isTransient =
error.message.includes('connection') ||
error.message.includes('timeout') ||
error.message.includes('deadlock');
if (isTransient && i < maxRetries - 1) {
console.log(`Retry ${i + 1}/${maxRetries}`);
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
// Usage
const user = await withRetry(() =>
ductape.graph.createNode({
labels: ['User'],
properties: { email: 'alice@example.com' },
})
);
Monitoring and Debugging
Log Slow Queries
// Monitor query performance
async function timedQuery<T>(
name: string,
operation: () => Promise<T>
): Promise<T> {
const start = Date.now();
try {
const result = await operation();
const duration = Date.now() - start;
if (duration > 1000) {
console.warn(`Slow query: ${name} took ${duration}ms`);
}
return result;
} catch (error) {
console.error(`Query failed: ${name}`, error);
throw error;
}
}
// Usage
const users = await timedQuery('find-active-users', () =>
ductape.graph.findNodes({
labels: ['User'],
where: { status: 'active' },
})
);
Use Explain for Query Analysis
// Analyze query performance (Neo4j)
const plan = await ductape.graph.query({
query: `
EXPLAIN MATCH (u:User)-[:FRIENDS_WITH]->(f:User)
WHERE u.city = $city
RETURN f.name
`,
params: { city: 'New York' },
});
console.log('Query plan:', plan.records);
// Look for "NodeByLabelScan" vs "NodeIndexSeek"
Track Database Statistics
// Monitor graph health
async function getGraphHealth() {
const stats = await ductape.graph.getStatistics();
console.log('Nodes:', stats.nodeCount);
console.log('Relationships:', stats.relationshipCount);
console.log('Labels:', stats.labels);
console.log('Relationship types:', stats.relationshipTypes);
return stats;
}
// Run periodically
setInterval(getGraphHealth, 5 * 60 * 1000); // Every 5 minutes
Security
Sanitize User Input
// Good - use parameterized queries
async function findUserByEmail(email: string) {
return ductape.graph.findNodes({
labels: ['User'],
where: { email }, // Safely parameterized
});
}
// ❌ Dangerous - injection risk
async function findUserByEmailUnsafe(email: string) {
return ductape.graph.query({
query: `MATCH (u:User {email: "${email}"}) RETURN u`, // DON'T DO THIS
});
}
Use Read-Only Transactions for Queries
// Good - read-only transaction for analytics
await ductape.graph.executeTransaction(
async (tx) => {
const stats = await ductape.graph.getStatistics(tx);
const users = await ductape.graph.findNodes({
labels: ['User'],
}, tx);
return { stats, userCount: users.nodes.length };
},
{ readOnly: true }
);
Limit Result Sizes
// Always limit query results
async function searchUsers(query: string) {
return ductape.graph.findNodes({
labels: ['User'],
where: {
name: { $CONTAINS: query },
},
limit: 100, // Prevent unbounded results
});
}
Testing
Use Transactions in Tests
// Good - test in transaction, rollback after
import { describe, it, beforeEach, afterEach } from 'vitest';
describe('User operations', () => {
let tx: IGraphTransaction;
beforeEach(async () => {
tx = await ductape.graph.beginTransaction();
});
afterEach(async () => {
await ductape.graph.rollbackTransaction(tx);
});
it('should create user', async () => {
const user = await ductape.graph.createNode({
labels: ['User'],
properties: { email: 'test@example.com' },
}, tx);
expect(user.node.properties.email).toBe('test@example.com');
// Changes rolled back automatically
});
});
Test with Realistic Data
// Create test fixtures
async function setupTestData() {
return ductape.graph.executeTransaction(async (tx) => {
const users = [];
for (let i = 0; i < 10; i++) {
const user = await ductape.graph.createNode({
labels: ['User'],
properties: {
name: `User ${i}`,
email: `user${i}@test.com`,
},
}, tx);
users.push(user.node);
}
// Create relationships
for (let i = 0; i < users.length - 1; i++) {
await ductape.graph.createRelationship({
type: 'FRIENDS_WITH',
startNodeId: users[i].id,
endNodeId: users[i + 1].id,
}, tx);
}
return users;
});
}
Deployment
Use Environment Variables
// Good - environment-based config
const config = {
uri: process.env.NEO4J_URI,
username: process.env.NEO4J_USERNAME,
password: process.env.NEO4J_PASSWORD,
};
await ductape.graph.register({
tag: 'main-graph',
driver: GraphDriver.NEO4J,
config,
});
Connection Pooling in Production
// Configure for production load
await ductape.graph.register({
tag: 'main-graph',
driver: GraphDriver.NEO4J,
config: {
uri: process.env.NEO4J_URI,
username: process.env.NEO4J_USERNAME,
password: process.env.NEO4J_PASSWORD,
},
options: {
maxConnectionPoolSize: 50,
connectionTimeout: 30000,
maxTransactionRetryTime: 30000,
},
});
Health Checks
// Implement health check endpoint
async function healthCheck() {
try {
await ductape.graph.testConnection({ tag: 'main-graph' });
return { status: 'healthy', database: 'connected' };
} catch (error) {
return { status: 'unhealthy', error: error.message };
}
}
// Express example
app.get('/health', async (req, res) => {
const health = await healthCheck();
res.status(health.status === 'healthy' ? 200 : 503).json(health);
});
Summary Checklist
Data Modeling:
- Use descriptive labels
- Relationships are verbs
- Properties for simple values, nodes for complex entities
Performance:
- Create indexes on frequently queried properties
- Use constraints for uniqueness
- Limit traversal depth
- Batch operations in transactions
- Paginate large results
Data Integrity:
- Use transactions for multi-step operations
- Validate data before writing
- Use merge for idempotent operations
Monitoring:
- Log slow queries
- Track database statistics
- Implement health checks
Security:
- Use parameterized queries
- Sanitize user input
- Limit result sizes
Next Steps
- Indexes & Constraints - Optimize query performance
- Transactions - Ensure data consistency
- Traversals - Graph pathfinding patterns
- Nodes - Node operations reference
See Also
- Graph Overview - Full API reference
- Performance Tuning - Advanced optimization