Preview
Preview Feature — This feature is currently in preview and under active development. APIs and functionality may change. We recommend testing thoroughly before using in production.
Best Practices & Performance
Learn essential patterns and optimization techniques for building high-performance graph applications with Ductape.
Data Modeling
Use Descriptive Labels
- TypeScript
- Java
- Go
- .NET
// 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' },
});
// Good - clear and specific
ductape.graph.createNode(Map.of(
labels: ['Person', 'Employee', 'Engineer'],
properties: Map.of( "name", "Alice" )
));
// ❌ Avoid - too generic
ductape.graph.createNode(Map.of(
labels: ['Node'],
properties: Map.of( "name", "Alice", "type", "person" )
));
// Good - clear and specific
client.graph.createNode({
labels: ['Person', 'Employee', 'Engineer'],
properties: { "name": "Alice" },
});
// ❌ Avoid - too generic
client.graph.createNode({
labels: ['Node'],
properties: { "name": "Alice", "type": "person" },
});
// 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
- TypeScript
- Java
- Go
- .NET
// 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,
});
// Good - relationship types are verbs
ductape.graph.createRelationship(Map.of(
"type", "WORKS_FOR",
startNodeId: personId,
endNodeId: companyId
));
ductape.graph.createRelationship(Map.of(
"type", "FRIENDS_WITH",
startNodeId: user1Id,
endNodeId: user2Id
));
// ❌ Avoid - nouns or unclear relationships
ductape.graph.createRelationship(Map.of(
"type", "PERSON_COMPANY",
startNodeId: personId,
endNodeId: companyId
));
// Good - relationship types are verbs
client.graph.createRelationship({
"type": "WORKS_FOR",
startNodeId: personId,
endNodeId: companyId,
});
client.graph.createRelationship({
"type": "FRIENDS_WITH",
startNodeId: user1Id,
endNodeId: user2Id,
});
// ❌ Avoid - nouns or unclear relationships
client.graph.createRelationship({
"type": "PERSON_COMPANY",
startNodeId: personId,
endNodeId: companyId,
});
// 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
- TypeScript
- Java
- Go
- .NET
// Property
await ductape.graph.createNode({
labels: ['Person'],
properties: {
name: 'Alice',
age: 28,
city: 'New York', // Simple value
},
});
// Property
ductape.graph.createNode(Map.of(
labels: ['Person'],
properties: Map.of(
"name", "Alice",
"age", 28,
"city", "New York", // Simple value
)
));
// Property
client.graph.createNode({
labels: ['Person'],
properties: {
"name": "Alice",
"age": 28,
"city": "New York", // Simple value
},
});
// 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
- TypeScript
- Java
- Go
- .NET
// 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 },
});
// Separate nodes
Map<String, Object> alice = ductape.graph.createNode(Map.of(
labels: ['Person'],
properties: Map.of( "name", "Alice", "age", 28 )
));
Map<String, Object> nyc = ductape.graph.createNode(Map.of(
labels: ['City'],
properties: Map.of(
"name", "New York",
"population", 8000000,
"timezone", "EST"
)
));
ductape.graph.createRelationship(Map.of(
"type", "LIVES_IN",
startNodeId: alice.node.id,
endNodeId: nyc.node.id,
properties: Map.of( "since", 2020 )
));
// Separate nodes
alice := client.graph.createNode({
labels: ['Person'],
properties: { "name": "Alice", "age": 28 },
});
nyc := client.graph.createNode({
labels: ['City'],
properties: {
"name": "New York",
"population": 8000000,
"timezone": "EST",
},
});
client.graph.createRelationship({
"type": "LIVES_IN",
startNodeId: alice.node.id,
endNodeId: nyc.node.id,
properties: { "since": 2020 },
});
// Separate nodes
var alice = await ductape.graph.createNode({
labels: ['Person'],
properties: { ["name"] = "Alice", ["age"] = 28 },
});
var 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
- TypeScript
- Java
- Go
- .NET
// ❌ 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,
},
});
// ❌ Bad - properties doing too much
ductape.graph.createNode(Map.of(
labels: ['User'],
properties: Map.of(
"name", "Alice",
"emails", "alice@work.com,alice@personal.com", // Comma-separated
"skills", "JS,TS,Python,React", // Comma-separated
)
));
// Good - structured properly
ductape.graph.createNode(Map.of(
labels: ['User'],
properties: Map.of(
"name", "Alice",
emails: ['alice@work.com', 'alice@personal.com'], // Array
skills: ['JavaScript', 'TypeScript', 'Python', 'React'], // Array
)
));
// Even better - use nodes for complex relationships
Map<String, Object> alice = ductape.graph.createNode(Map.of(
labels: ['User'],
properties: Map.of( "name", "Alice" )
));
Map<String, Object> javascript = ductape.graph.createNode(Map.of(
labels: ['Skill'],
properties: Map.of( "name", "JavaScript" )
));
ductape.graph.createRelationship(Map.of(
"type", "HAS_SKILL",
startNodeId: alice.node.id,
endNodeId: javascript.node.id,
properties: Map.of(
"level", "expert",
"yearsExperience", 5
)
));
// ❌ Bad - properties doing too much
client.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
client.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
alice := client.graph.createNode({
labels: ['User'],
properties: { "name": "Alice" },
});
javascript := client.graph.createNode({
labels: ['Skill'],
properties: { "name": "JavaScript" },
});
client.graph.createRelationship({
"type": "HAS_SKILL",
startNodeId: alice.node.id,
endNodeId: javascript.node.id,
properties: {
"level": "expert",
"yearsExperience": 5,
},
});
// ❌ 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
var alice = await ductape.graph.createNode({
labels: ['User'],
properties: { ["name"] = "Alice" },
});
var 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
- TypeScript
- Java
- Go
- .NET
// 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
});
// Good - index unique or near-unique values
ductape.graph.createNodeIndex(Map.of(
"name", "idx_user_email",
type: NodeIndexType.BTREE,
"label", "User",
properties: ['email'], // High cardinality
));
ductape.graph.createNodeIndex(Map.of(
"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)
ductape.graph.createNodeIndex(Map.of(
"name", "idx_user_active",
type: NodeIndexType.BTREE,
"label", "User",
properties: ['isActive'], // Only true/false
));
// Good - index unique or near-unique values
client.graph.createNodeIndex({
"name": "idx_user_email",
type: NodeIndexType.BTREE,
"label": "User",
properties: ['email'], // High cardinality
});
client.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)
client.graph.createNodeIndex({
"name": "idx_user_active",
type: NodeIndexType.BTREE,
"label": "User",
properties: ['isActive'], // Only true/false
});
// 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
- TypeScript
- Java
- Go
- .NET
// 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',
},
});
// Good - matches query patterns
ductape.graph.createNodeIndex(Map.of(
"name", "idx_order_user_status",
type: NodeIndexType.BTREE,
"label", "Order",
properties: ['userId', 'status'], // Most selective first
));
// Efficiently supports:
Map<String, Object> orders = ductape.graph.findNodes(Map.of(
labels: ['Order'],
where: Map.of(
"userId", "123",
"status", "pending"
)
));
// Good - matches query patterns
client.graph.createNodeIndex({
"name": "idx_order_user_status",
type: NodeIndexType.BTREE,
"label": "Order",
properties: ['userId', 'status'], // Most selective first
});
// Efficiently supports:
orders := client.graph.findNodes({
labels: ['Order'],
where: {
"userId": "123",
"status": "pending",
},
});
// 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:
var orders = await ductape.graph.findNodes({
labels: ['Order'],
where: {
["userId"] = "123",
["status"] = "pending",
},
});
Create Constraints for Uniqueness
- TypeScript
- Java
- Go
- .NET
// 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
// Use constraints instead of just indexes
ductape.graph.createNodeConstraint(Map.of(
"name", "unique_user_email",
type: NodeConstraintType.UNIQUE,
"label", "User",
properties: ['email']
));
// Automatically creates an index + enforces uniqueness
// Use constraints instead of just indexes
client.graph.createNodeConstraint({
"name": "unique_user_email",
type: NodeConstraintType.UNIQUE,
"label": "User",
properties: ['email'],
});
// Automatically creates an index + enforces 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
- TypeScript
- Java
- Go
- .NET
// 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);
// Good - filter at query time
Map<String, Object> users = ductape.graph.findNodes(Map.of(
labels: ['User'],
where: Map.of(
"status", "active",
"city", "New York"
),
"limit", 10
));
// ❌ Bad - fetch all then filter in code
Map<String, Object> allUsers = ductape.graph.findNodes(Map.of(
labels: ['User']
));
Map<String, Object> filtered = allUsers.nodes.filter(
u => u.properties.status === 'active' && u.properties.city === 'New York'
).slice(0, 10);
// Good - filter at query time
users := client.graph.findNodes({
labels: ['User'],
where: {
"status": "active",
"city": "New York",
},
"limit": 10,
});
// ❌ Bad - fetch all then filter in code
allUsers := client.graph.findNodes({
labels: ['User'],
});
filtered := allUsers.nodes.filter(
u => u.properties.status === 'active' && u.properties.city === 'New York'
).slice(0, 10);
// Good - filter at query time
var users = await ductape.graph.findNodes({
labels: ['User'],
where: {
["status"] = "active",
["city"] = "New York",
},
["limit"] = 10,
});
// ❌ Bad - fetch all then filter in code
var allUsers = await ductape.graph.findNodes({
labels: ['User'],
});
var filtered = allUsers.nodes.filter(
u => u.properties.status === 'active' && u.properties.city === 'New York'
).slice(0, 10);
Use Specific Labels
- TypeScript
- Java
- Go
- .NET
// 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 },
},
});
// Good - specific label
Map<String, Object> engineers = ductape.graph.findNodes(Map.of(
labels: ['Engineer'],
where: Map.of( experience: Map.of( $"GT", 5 ) )
));
// ❌ Slower - generic label + property filter
Map<String, Object> engineers = ductape.graph.findNodes(Map.of(
labels: ['Person'],
where: Map.of(
"type", "engineer",
experience: Map.of( $"GT", 5 )
)
));
// Good - specific label
engineers := client.graph.findNodes({
labels: ['Engineer'],
where: { experience: { $"GT": 5 } },
});
// ❌ Slower - generic label + property filter
engineers := client.graph.findNodes({
labels: ['Person'],
where: {
"type": "engineer",
experience: { $"GT": 5 },
},
});
// Good - specific label
var engineers = await ductape.graph.findNodes({
labels: ['Engineer'],
where: { experience: { $["GT"] = 5 } },
});
// ❌ Slower - generic label + property filter
var engineers = await ductape.graph.findNodes({
labels: ['Person'],
where: {
["type"] = "engineer",
experience: { $["GT"] = 5 },
},
});
Limit Traversal Depth
- TypeScript
- Java
- Go
- .NET
// 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
});
// Good - reasonable depth
Map<String, Object> network = ductape.graphs().traverse(Map<String, Object>.of(
startNodeId: userId,
relationshipTypes: ['FRIENDS_WITH'],
"maxDepth", 3, // Friends of friends of friends
));
// ❌ Dangerous - could explore millions of nodes
Map<String, Object> network = ductape.graphs().traverse(Map<String, Object>.of(
startNodeId: userId,
"maxDepth", 10, // Exponential growth
));
// Good - reasonable depth
network := client.graph.traverse({
startNodeId: userId,
relationshipTypes: ['FRIENDS_WITH'],
"maxDepth": 3, // Friends of friends of friends
});
// ❌ Dangerous - could explore millions of nodes
network := client.graph.traverse({
startNodeId: userId,
"maxDepth": 10, // Exponential growth
});
// Good - reasonable depth
var network = await ductape.graph.traverse({
startNodeId: userId,
relationshipTypes: ['FRIENDS_WITH'],
["maxDepth"] = 3, // Friends of friends of friends
});
// ❌ Dangerous - could explore millions of nodes
var network = await ductape.graph.traverse({
startNodeId: userId,
["maxDepth"] = 10, // Exponential growth
});
Use Relationship Types
- TypeScript
- Java
- Go
- .NET
// 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,
});
// Good - specific relationship types
Map<String, Object> friends = ductape.graphs().traverse(Map<String, Object>.of(
startNodeId: userId,
relationshipTypes: ['FRIENDS_WITH', 'KNOWS'],
"maxDepth", 2
));
// ❌ Slower - follows all relationships
Map<String, Object> connections = ductape.graphs().traverse(Map<String, Object>.of(
startNodeId: userId,
// No relationshipTypes specified
"maxDepth", 2
));
// Good - specific relationship types
friends := client.graph.traverse({
startNodeId: userId,
relationshipTypes: ['FRIENDS_WITH', 'KNOWS'],
"maxDepth": 2,
});
// ❌ Slower - follows all relationships
connections := client.graph.traverse({
startNodeId: userId,
// No relationshipTypes specified
"maxDepth": 2,
});
// Good - specific relationship types
var friends = await ductape.graph.traverse({
startNodeId: userId,
relationshipTypes: ['FRIENDS_WITH', 'KNOWS'],
["maxDepth"] = 2,
});
// ❌ Slower - follows all relationships
var connections = await ductape.graph.traverse({
startNodeId: userId,
// No relationshipTypes specified
["maxDepth"] = 2,
});
Paginate Large Result Sets
- TypeScript
- Java
- Go
- .NET
// 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,
};
}
// Good - paginate results
async function getUsersPaginated(page: number = 1, pageSize: number = 50) Map.of(
Map<String, Object> offset = (page - 1) * pageSize;
Map<String, Object> result = ductape.graph.findNodes(Map.of(
labels: ['User'],
where: Map.of( "status", "active" ),
limit: pageSize,
skip: offset
));
return Map.of(
users: result.nodes,
page,
pageSize,
hasMore: result.nodes.length === pageSize
);
)
// Good - paginate results
async function getUsersPaginated(page: number = 1, pageSize: number = 50) {
offset := (page - 1) * pageSize;
result := client.graph.findNodes({
labels: ['User'],
where: { "status": "active" },
limit: pageSize,
skip: offset,
});
return {
users: result.nodes,
page,
pageSize,
hasMore: result.nodes.length === pageSize,
};
}
// Good - paginate results
async function getUsersPaginated(page: number = 1, pageSize: number = 50) {
var offset = (page - 1) * pageSize;
var 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
- TypeScript
- Java
- Go
- .NET
// 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,
});
}
// Good - batch in single transaction
ductape.graph.executeTransaction(async (tx) => Map.of(
for (Map<String, Object> userData of largeUserList) Map.of(
ductape.graph.createNode(Map.of(
labels: ['User'],
properties: userData
), tx);
)
));
// ❌ Bad - separate transaction per operation
for (Map<String, Object> userData of largeUserList) Map.of(
ductape.graph.createNode(Map.of(
labels: ['User'],
properties: userData
));
)
// Good - batch in single transaction
client.graph.executeTransaction(async (tx) => {
for (const userData of largeUserList) {
client.graph.createNode({
labels: ['User'],
properties: userData,
}, tx);
}
});
// ❌ Bad - separate transaction per operation
for (const userData of largeUserList) {
client.graph.createNode({
labels: ['User'],
properties: userData,
});
}
// Good - batch in single transaction
await ductape.graph.executeTransaction(async (tx) => {
for (var userData of largeUserList) {
await ductape.graph.createNode({
labels: ['User'],
properties: userData,
}, tx);
}
});
// ❌ Bad - separate transaction per operation
for (var userData of largeUserList) {
await ductape.graph.createNode({
labels: ['User'],
properties: userData,
});
}
Cache Frequently Accessed Data
- TypeScript
- Java
- Go
- .NET
// 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;
}
// Good - cache popular queries
Map<String, Object> cache = new Map<string, any>();
async function getPopularPosts() Map.of(
Map<String, Object> cacheKey = 'popular_posts';
Map<String, Object> cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) Map.of(
return cached.data; // Return cached data (5 min TTL)
)
Map<String, Object> posts = ductape.graph.findNodes(Map.of(
labels: ['Post'],
where: Map.of( likes: Map.of( $"GT", 1000 ) ),
"limit", 10
));
cache.set(cacheKey, Map.of(
data: posts.nodes,
timestamp: Date.now()
));
return posts.nodes;
)
// Good - cache popular queries
cache := new Map<string, any>();
async function getPopularPosts() {
cacheKey := 'popular_posts';
cached := cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
return cached.data; // Return cached data (5 min TTL)
}
posts := client.graph.findNodes({
labels: ['Post'],
where: { likes: { $"GT": 1000 } },
"limit": 10,
});
cache.set(cacheKey, {
data: posts.nodes,
timestamp: Date.now(),
});
return posts.nodes;
}
// Good - cache popular queries
var cache = new Map<string, any>();
async function getPopularPosts() {
var cacheKey = 'popular_posts';
var cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
return cached.data; // Return cached data (5 min TTL)
}
var 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
- TypeScript
- Java
- Go
- .NET
// 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
// Good - reuse connections
Map<String, Object> ductape = new Ductape(Map.of(
"workspace_id", "workspace-123",
"user_id", "user-456",
"private_key", "your-private-key"
));
// Register once at startup
ductape.graph.register(Map.of(
"tag", "main-graph",
driver: GraphDriver.NEO4J,
config: Map.of(
"uri", "neo4j://"localhost", 7687",
"username", "neo4j",
"password", "password"
),
options: Map.of(
"maxConnectionPoolSize", 50,
"connectionTimeout", 30000
)
));
// Connect once
ductape.graphs().connect(Map<String, Object>.of(
"tag", "main-graph" ));
// Reuse connection for all operations
// Connection is automatically pooled
import "context"
// Good - reuse connections
ductape := new Ductape({
"workspace_id": "workspace-123",
"user_id": "user-456",
"private_key": "your-private-key",
});
// Register once at startup
client.graph.register({
"tag": "main-graph",
driver: GraphDriver.NEO4J,
config: {
"uri": "neo4j://"localhost": 7687",
"username": "neo4j",
"password": "password",
},
options: {
"maxConnectionPoolSize": 50,
"connectionTimeout": 30000,
},
});
// Connect once
client.GraphAPI.Connect(ctx, map[string]any{
"tag": "main-graph" });
// Reuse connection for all operations
// Connection is automatically pooled
// Good - reuse connections
var 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(new Dictionary<string, object?>
{
["tag"] = "main-graph" });
// Reuse connection for all operations
// Connection is automatically pooled
Avoid N+1 Query Problems
- TypeScript
- Java
- Go
- .NET
// ❌ 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' },
});
// ❌ Bad - N+1 queries
Map<String, Object> users = ductape.graph.findNodes(Map.of(
labels: ['User'],
"limit", 10
));
// Separate query for each user's posts (N queries)
for (Map<String, Object> user of users.nodes) Map.of(
Map<String, Object> posts = ductape.graph.findRelationships(Map.of(
startNodeId: user.id,
"type", "POSTED"
));
user.posts = posts.relationships;
)
// Good - single query with pattern
Map<String, Object> usersWithPosts = ductape.graph.query(Map.of(
query: `
MATCH (u:User)-[r:POSTED]->(p:Post)
WHERE u.status = $status
RETURN u, collect(Map.of(post: p, relationship: r)) as posts
LIMIT 10
`,
params: Map.of( "status", "active" )
));
// ❌ Bad - N+1 queries
users := client.graph.findNodes({
labels: ['User'],
"limit": 10,
});
// Separate query for each user's posts (N queries)
for (const user of users.nodes) {
posts := client.graph.findRelationships({
startNodeId: user.id,
"type": "POSTED",
});
user.posts = posts.relationships;
}
// Good - single query with pattern
usersWithPosts := client.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" },
});
// ❌ Bad - N+1 queries
var users = await ductape.graph.findNodes({
labels: ['User'],
["limit"] = 10,
});
// Separate query for each user's posts (N queries)
for (var user of users.nodes) {
var posts = await ductape.graph.findRelationships({
startNodeId: user.id,
["type"] = "POSTED",
});
user.posts = posts.relationships;
}
// Good - single query with pattern
var 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
- TypeScript
- Java
- Go
- .NET
// 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);
});
// Always use transactions for related operations
ductape.graph.executeTransaction(async (tx) => Map.of(
Map<String, Object> order = ductape.graph.createNode(Map.of(
labels: ['Order'],
properties: Map.of( "total", 100, "status", "pending" )
), tx);
ductape.graph.updateNode(Map.of(
id: userId,
properties: Map.of( orderCount: Map.of( $"INCREMENT", 1 ) )
), tx);
ductape.graph.createRelationship(Map.of(
"type", "PLACED",
startNodeId: userId,
endNodeId: order.node.id
), tx);
));
// Always use transactions for related operations
client.graph.executeTransaction(async (tx) => {
order := client.graph.createNode({
labels: ['Order'],
properties: { "total": 100, "status": "pending" },
}, tx);
client.graph.updateNode({
id: userId,
properties: { orderCount: { $"INCREMENT": 1 } },
}, tx);
client.graph.createRelationship({
"type": "PLACED",
startNodeId: userId,
endNodeId: order.node.id,
}, tx);
});
// Always use transactions for related operations
await ductape.graph.executeTransaction(async (tx) => {
var 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
- TypeScript
- Java
- Go
- .NET
// 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,
});
}
// Good - validate first
function validateUser(data: any) Map.of(
if (!data.email || !data.email.includes('@')) Map.of(
throw new Error('Invalid email');
)
if (!data.name || data.name.length < 2) Map.of(
throw new Error('Name too short');
)
return true;
)
async function createUser(data: any) Map.of(
validateUser(data);
return ductape.graph.createNode(Map.of(
labels: ['User'],
properties: data
));
)
// 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 client.graph.createNode({
labels: ['User'],
properties: data,
});
}
// 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
- TypeScript
- Java
- Go
- .NET
// 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
// Good - merge is idempotent
Map<String, Object> user = ductape.graph.mergeNode(Map.of(
labels: ['User'],
matchProperties: Map.of( "email", "alice@example.com" ),
onCreate: Map.of(
"email", "alice@example.com",
"name", "Alice",
createdAt: Instant.now()
),
onMatch: Map.of(
lastSeen: Instant.now()
)
));
// Safe to call multiple times
// Good - merge is idempotent
user := client.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
// Good - merge is idempotent
var user = await ductape.graph.mergeNode({
labels: ['User'],
matchProperties: { ["email"] = "alice@example.com" },
onCreate: {
["email"] = "alice@example.com",
["name"] = "Alice",
createdAt: DateTime.UtcNow,
},
onMatch: {
lastSeen: DateTime.UtcNow,
},
});
// Safe to call multiple times
Schema Design
Denormalize Carefully
Sometimes denormalization improves performance:
- TypeScript
- Java
- Go
- .NET
// 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);
});
// Store frequently accessed counts
ductape.graph.createNode(Map.of(
labels: ['User'],
properties: Map.of(
"name", "Alice",
"followerCount", 1234, // Denormalized count
"followingCount", 567, // Denormalized count
)
));
// Update counts when relationships change
ductape.graph.executeTransaction(async (tx) => Map.of(
// Create follow relationship
ductape.graph.createRelationship(Map.of(
"type", "FOLLOWS",
startNodeId: user1Id,
endNodeId: user2Id
), tx);
// Update denormalized counts
ductape.graph.updateNode(Map.of(
id: user1Id,
properties: Map.of( followingCount: Map.of( $"INCREMENT", 1 ) )
), tx);
ductape.graph.updateNode(Map.of(
id: user2Id,
properties: Map.of( followerCount: Map.of( $"INCREMENT", 1 ) )
), tx);
));
// Store frequently accessed counts
client.graph.createNode({
labels: ['User'],
properties: {
"name": "Alice",
"followerCount": 1234, // Denormalized count
"followingCount": 567, // Denormalized count
},
});
// Update counts when relationships change
client.graph.executeTransaction(async (tx) => {
// Create follow relationship
client.graph.createRelationship({
"type": "FOLLOWS",
startNodeId: user1Id,
endNodeId: user2Id,
}, tx);
// Update denormalized counts
client.graph.updateNode({
id: user1Id,
properties: { followingCount: { $"INCREMENT": 1 } },
}, tx);
client.graph.updateNode({
id: user2Id,
properties: { followerCount: { $"INCREMENT": 1 } },
}, tx);
});
// 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
- TypeScript
- Java
- Go
- .NET
// ❌ 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,
});
// ❌ Basic - loses information
ductape.graph.createRelationship(Map.of(
"type", "ENROLLED_IN",
startNodeId: studentId,
endNodeId: courseId,
properties: Map.of(
"grade", "A",
"semester", "Fall 2024"
)
));
// Better - enrollment as node
Map<String, Object> enrollment = ductape.graph.createNode(Map.of(
labels: ['Enrollment'],
properties: Map.of(
"grade", "A",
"semester", "Fall 2024",
"credits", 3,
"status", "completed"
)
));
ductape.graph.createRelationship(Map.of(
"type", "HAS_ENROLLMENT",
startNodeId: studentId,
endNodeId: enrollment.node.id
));
ductape.graph.createRelationship(Map.of(
"type", "FOR_COURSE",
startNodeId: enrollment.node.id,
endNodeId: courseId
));
// ❌ Basic - loses information
client.graph.createRelationship({
"type": "ENROLLED_IN",
startNodeId: studentId,
endNodeId: courseId,
properties: {
"grade": "A",
"semester": "Fall 2024",
},
});
// Better - enrollment as node
enrollment := client.graph.createNode({
labels: ['Enrollment'],
properties: {
"grade": "A",
"semester": "Fall 2024",
"credits": 3,
"status": "completed",
},
});
client.graph.createRelationship({
"type": "HAS_ENROLLMENT",
startNodeId: studentId,
endNodeId: enrollment.node.id,
});
client.graph.createRelationship({
"type": "FOR_COURSE",
startNodeId: enrollment.node.id,
endNodeId: courseId,
});
// ❌ Basic - loses information
await ductape.graph.createRelationship({
["type"] = "ENROLLED_IN",
startNodeId: studentId,
endNodeId: courseId,
properties: {
["grade"] = "A",
["semester"] = "Fall 2024",
},
});
// Better - enrollment as node
var 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
- TypeScript
- Java
- Go
- .NET
// 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;
}
// Add schema version to nodes
ductape.graph.createNode(Map.of(
labels: ['User'],
properties: Map.of(
"name", "Alice",
"email", "alice@example.com",
"schemaVersion", 2, // Track schema version
)
));
// Handle multiple versions
async function getUser(id: string) Map.of(
Map<String, Object> user = ductape.graph.findNodeById(id);
if (user.properties.schemaVersion === 1) Map.of(
// Migrate old schema
return migrateUserV1ToV2(user);
)
return user;
)
// Add schema version to nodes
client.graph.createNode({
labels: ['User'],
properties: {
"name": "Alice",
"email": "alice@example.com",
"schemaVersion": 2, // Track schema version
},
});
// Handle multiple versions
async function getUser(id: string) {
user := client.graph.findNodeById(id);
if (user.properties.schemaVersion === 1) {
// Migrate old schema
return migrateUserV1ToV2(user);
}
return user;
}
// 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) {
var user = await ductape.graph.findNodeById(id);
if (user.properties.schemaVersion === 1) {
// Migrate old schema
return migrateUserV1ToV2(user);
}
return user;
}
Error Handling
Catch Specific Errors
- TypeScript
- Java
- Go
- .NET
// 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
}
}
// Good - handle specific errors
try Map.of(
ductape.graph.createNode(Map.of(
labels: ['User'],
properties: Map.of( "email", "alice@example.com" )
));
) catch (error) Map.of(
if (error.message.includes('constraint')) Map.of(
System.out.println('Email already exists');
// Return existing user or show error to user
) else if (error.message.includes('connection')) Map.of(
System.out.println('Database connection failed');
// Retry or show maintenance message
) else Map.of(
console.error('Unexpected error:', error);
// Log and report
)
)
// Good - handle specific errors
try {
client.graph.createNode({
labels: ['User'],
properties: { "email": "alice@example.com" },
});
} catch (error) {
if (error.message.includes('constraint')) {
fmt.Println('Email already exists');
// Return existing user or show error to user
} else if (error.message.includes('connection')) {
fmt.Println('Database connection failed');
// Retry or show maintenance message
} else {
console.error('Unexpected error:', error);
// Log and report
}
}
// Good - handle specific errors
try {
await ductape.graph.createNode({
labels: ['User'],
properties: { ["email"] = "alice@example.com" },
});
} catch (error) {
if (error.message.includes('constraint')) {
Console.WriteLine('Email already exists');
// Return existing user or show error to user
} else if (error.message.includes('connection')) {
Console.WriteLine('Database connection failed');
// Retry or show maintenance message
} else {
console.error('Unexpected error:', error);
// Log and report
}
}
Implement Retry Logic
- TypeScript
- Java
- Go
- .NET
// 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' },
})
);
// Retry transient errors
async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> Map.of(
for (Map<String, Object> i = 0; i < maxRetries; i++) Map.of(
try Map.of(
return operation();
) catch (error) Map.of(
Map<String, Object> isTransient =
error.message.includes('connection') ||
error.message.includes('timeout') ||
error.message.includes('deadlock');
if (isTransient && i < maxRetries - 1) Map.of(
System.out.println(`Retry $Map.of(i + 1)/$Map.of(maxRetries)`);
new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
continue;
)
throw error;
)
)
throw new Error('Max retries exceeded');
)
// Usage
Map<String, Object> user = withRetry(() =>
ductape.graph.createNode(Map.of(
labels: ['User'],
properties: Map.of( "email", "alice@example.com" )
))
);
// Retry transient errors
async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
for (i := 0; i < maxRetries; i++) {
try {
return operation();
} catch (error) {
isTransient :=
error.message.includes('connection') ||
error.message.includes('timeout') ||
error.message.includes('deadlock');
if (isTransient && i < maxRetries - 1) {
fmt.Println(`Retry ${i + 1}/${maxRetries}`);
new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
// Usage
user := withRetry(() =>
client.graph.createNode({
labels: ['User'],
properties: { "email": "alice@example.com" },
})
);
// Retry transient errors
async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
for (var i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
var isTransient =
error.message.includes('connection') ||
error.message.includes('timeout') ||
error.message.includes('deadlock');
if (isTransient && i < maxRetries - 1) {
Console.WriteLine(`Retry ${i + 1}/${maxRetries}`);
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
// Usage
var user = await withRetry(() =>
ductape.graph.createNode({
labels: ['User'],
properties: { ["email"] = "alice@example.com" },
})
);
Monitoring and Debugging
Log Slow Queries
- TypeScript
- Java
- Go
- .NET
// 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' },
})
);
// Monitor query performance
async function timedQuery<T>(
name: string,
operation: () => Promise<T>
): Promise<T> Map.of(
Map<String, Object> start = Date.now();
try Map.of(
Map<String, Object> result = operation();
Map<String, Object> duration = Date.now() - start;
if (duration > 1000) Map.of(
console.warn(`Slow query: $Map.of(name) took $Map.of(duration)ms`);
)
return result;
) catch (error) Map.of(
console.error(`Query failed: $Map.of(name)`, error);
throw error;
)
)
// Usage
Map<String, Object> users = timedQuery('find-active-users', () =>
ductape.graph.findNodes(Map.of(
labels: ['User'],
where: Map.of( "status", "active" )
))
);
// Monitor query performance
async function timedQuery<T>(
name: string,
operation: () => Promise<T>
): Promise<T> {
start := Date.now();
try {
result := operation();
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
users := timedQuery('find-active-users', () =>
client.graph.findNodes({
labels: ['User'],
where: { "status": "active" },
})
);
// Monitor query performance
async function timedQuery<T>(
name: string,
operation: () => Promise<T>
): Promise<T> {
var start = Date.now();
try {
var result = await operation();
var 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
var users = await timedQuery('find-active-users', () =>
ductape.graph.findNodes({
labels: ['User'],
where: { ["status"] = "active" },
})
);
Use Explain for Query Analysis
- TypeScript
- Java
- Go
- .NET
// 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"
// Analyze query performance (Neo4j)
Map<String, Object> plan = ductape.graph.query(Map.of(
query: `
EXPLAIN MATCH (u:User)-[:FRIENDS_WITH]->(f:User)
WHERE u.city = $city
RETURN f.name
`,
params: Map.of( "city", "New York" )
));
System.out.println('Query plan:', plan.records);
// Look for "NodeByLabelScan" vs "NodeIndexSeek"
// Analyze query performance (Neo4j)
plan := client.graph.query({
query: `
EXPLAIN MATCH (u:User)-[:FRIENDS_WITH]->(f:User)
WHERE u.city = $city
RETURN f.name
`,
params: { "city": "New York" },
});
fmt.Println('Query plan:', plan.records);
// Look for "NodeByLabelScan" vs "NodeIndexSeek"
// Analyze query performance (Neo4j)
var 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.WriteLine('Query plan:', plan.records);
// Look for "NodeByLabelScan" vs "NodeIndexSeek"
Track Database Statistics
- TypeScript
- Java
- Go
- .NET
// 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
// Monitor graph health
async function getGraphHealth() Map.of(
Map<String, Object> stats = ductape.graph.getStatistics();
System.out.println('"Nodes", ", stats.nodeCount);
System.out.println(""Relationships", ", stats.relationshipCount);
System.out.println(""Labels", ", stats.labels);
System.out.println("Relationship types:', stats.relationshipTypes);
return stats;
)
// Run periodically
setInterval(getGraphHealth, 5 * 60 * 1000); // Every 5 minutes
// Monitor graph health
async function getGraphHealth() {
stats := client.graph.getStatistics();
fmt.Println('"Nodes": ", stats.nodeCount);
fmt.Println(""Relationships": ", stats.relationshipCount);
fmt.Println(""Labels": ", stats.labels);
fmt.Println("Relationship types:', stats.relationshipTypes);
return stats;
}
// Run periodically
setInterval(getGraphHealth, 5 * 60 * 1000); // Every 5 minutes
// Monitor graph health
async function getGraphHealth() {
var stats = await ductape.graph.getStatistics();
Console.WriteLine('["Nodes"] = ", stats.nodeCount);
Console.WriteLine("["Relationships"] = ", stats.relationshipCount);
Console.WriteLine("["Labels"] = ", stats.labels);
Console.WriteLine("Relationship types:', stats.relationshipTypes);
return stats;
}
// Run periodically
setInterval(getGraphHealth, 5 * 60 * 1000); // Every 5 minutes
Security
Sanitize User Input
- TypeScript
- Java
- Go
- .NET
// 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
});
}
// Good - use parameterized queries
async function findUserByEmail(email: string) Map.of(
return ductape.graph.findNodes(Map.of(
labels: ['User'],
where: Map.of( email ), // Safely parameterized
));
)
// ❌ Dangerous - injection risk
async function findUserByEmailUnsafe(email: string) Map.of(
return ductape.graph.query(Map.of(
query: `MATCH (u:User Map.of("email", "$Map.of(email)")) RETURN u`, // DON'T DO THIS
));
)
// Good - use parameterized queries
async function findUserByEmail(email: string) {
return client.graph.findNodes({
labels: ['User'],
where: { email }, // Safely parameterized
});
}
// ❌ Dangerous - injection risk
async function findUserByEmailUnsafe(email: string) {
return client.graph.query({
query: `MATCH (u:User {"email": "${email}"}) RETURN u`, // DON'T DO THIS
});
}
// 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
- TypeScript
- Java
- Go
- .NET
// 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 }
);
// Good - read-only transaction for analytics
ductape.graph.executeTransaction(
async (tx) => Map.of(
Map<String, Object> stats = ductape.graph.getStatistics(tx);
Map<String, Object> users = ductape.graph.findNodes(Map.of(
labels: ['User']
), tx);
return Map.of( stats, userCount: users.nodes.length );
),
Map.of( "readOnly", true )
);
// Good - read-only transaction for analytics
client.graph.executeTransaction(
async (tx) => {
stats := client.graph.getStatistics(tx);
users := client.graph.findNodes({
labels: ['User'],
}, tx);
return { stats, userCount: users.nodes.length };
},
{ "readOnly": true }
);
// Good - read-only transaction for analytics
await ductape.graph.executeTransaction(
async (tx) => {
var stats = await ductape.graph.getStatistics(tx);
var users = await ductape.graph.findNodes({
labels: ['User'],
}, tx);
return { stats, userCount: users.nodes.length };
},
{ ["readOnly"] = true }
);
Limit Result Sizes
- TypeScript
- Java
- Go
- .NET
// Always limit query results
async function searchUsers(query: string) {
return ductape.graph.findNodes({
labels: ['User'],
where: {
name: { $CONTAINS: query },
},
limit: 100, // Prevent unbounded results
});
}
// Always limit query results
async function searchUsers(query: string) Map.of(
return ductape.graph.findNodes(Map.of(
labels: ['User'],
where: Map.of(
name: Map.of( $CONTAINS: query )
),
"limit", 100, // Prevent unbounded results
));
)
// Always limit query results
async function searchUsers(query: string) {
return client.graph.findNodes({
labels: ['User'],
where: {
name: { $CONTAINS: query },
},
"limit": 100, // Prevent unbounded results
});
}
// 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
- TypeScript
- Java
- Go
- .NET
// 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
});
});
// Good - test in transaction, rollback after
import Map.of( describe, it, beforeEach, afterEach ) from 'vitest';
describe('User operations', () => Map.of(
Map<String, Object> tx: IGraphTransaction;
beforeEach(async () => Map.of(
tx = ductape.graph.beginTransaction();
));
afterEach(async () => Map.of(
ductape.graph.rollbackTransaction(tx);
));
it('should create user', async () => Map.of(
Map<String, Object> user = ductape.graph.createNode(Map.of(
labels: ['User'],
properties: Map.of( "email", "test@example.com" )
), tx);
expect(user.node.properties.email).toBe('test@example.com');
// Changes rolled back automatically
));
));
// Good - test in transaction, rollback after
import { describe, it, beforeEach, afterEach } from 'vitest';
describe('User operations', () => {
let tx: IGraphTransaction;
beforeEach(async () => {
tx = client.graph.beginTransaction();
});
afterEach(async () => {
client.graph.rollbackTransaction(tx);
});
it('should create user', async () => {
user := client.graph.createNode({
labels: ['User'],
properties: { "email": "test@example.com" },
}, tx);
expect(user.node.properties.email).toBe('test@example.com');
// Changes rolled back automatically
});
});
// Good - test in transaction, rollback after
import { describe, it, beforeEach, afterEach } from 'vitest';
describe('User operations', () => {
var tx: IGraphTransaction;
beforeEach(async () => {
tx = await ductape.graph.beginTransaction();
});
afterEach(async () => {
await ductape.graph.rollbackTransaction(tx);
});
it('should create user', async () => {
var 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
- TypeScript
- Java
- Go
- .NET
// 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;
});
}
// Create test fixtures
async function setupTestData() Map.of(
return ductape.graph.executeTransaction(async (tx) => Map.of(
Map<String, Object> users = [];
for (Map<String, Object> i = 0; i < 10; i++) Map.of(
Map<String, Object> user = ductape.graph.createNode(Map.of(
labels: ['User'],
properties: Map.of(
name: `User $Map.of(i)`,
email: `user$Map.of(i)@test.com`
)
), tx);
users.push(user.node);
)
// Create relationships
for (Map<String, Object> i = 0; i < users.length - 1; i++) Map.of(
ductape.graph.createRelationship(Map.of(
"type", "FRIENDS_WITH",
startNodeId: users[i].id,
endNodeId: users[i + 1].id
), tx);
)
return users;
));
)
// Create test fixtures
async function setupTestData() {
return client.graph.executeTransaction(async (tx) => {
users := [];
for (i := 0; i < 10; i++) {
user := client.graph.createNode({
labels: ['User'],
properties: {
name: `User ${i}`,
email: `user${i}@test.com`,
},
}, tx);
users.push(user.node);
}
// Create relationships
for (i := 0; i < users.length - 1; i++) {
client.graph.createRelationship({
"type": "FRIENDS_WITH",
startNodeId: users[i].id,
endNodeId: users[i + 1].id,
}, tx);
}
return users;
});
}
// Create test fixtures
async function setupTestData() {
return ductape.graph.executeTransaction(async (tx) => {
var users = [];
for (var i = 0; i < 10; i++) {
var user = await ductape.graph.createNode({
labels: ['User'],
properties: {
name: `User ${i}`,
email: `user${i}@test.com`,
},
}, tx);
users.push(user.node);
}
// Create relationships
for (var 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
- TypeScript
- Java
- Go
- .NET
// 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,
});
// Good - environment-based config
Map<String, Object> config = Map.of(
uri: System.getenv("NEO4J_URI"),
username: System.getenv("NEO4J_USERNAME"),
password: System.getenv("NEO4J_PASSWORD")
);
ductape.graph.register(Map.of(
"tag", "main-graph",
driver: GraphDriver.NEO4J,
config
));
// Good - environment-based config
config := map[string]any{
uri: os.Getenv("NEO4J_URI"),
username: os.Getenv("NEO4J_USERNAME"),
password: os.Getenv("NEO4J_PASSWORD"),
};
client.graph.register({
"tag": "main-graph",
driver: GraphDriver.NEO4J,
config,
});
// Good - environment-based config
var config = new Dictionary<string, object?>
{
uri: Environment.GetEnvironmentVariable("NEO4J_URI"),
username: Environment.GetEnvironmentVariable("NEO4J_USERNAME"),
password: Environment.GetEnvironmentVariable("NEO4J_PASSWORD"),
};
await ductape.graph.register({
["tag"] = "main-graph",
driver: GraphDriver.NEO4J,
config,
});
Connection Pooling in Production
- TypeScript
- Java
- Go
- .NET
// 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,
},
});
// Configure for production load
ductape.graph.register(Map.of(
"tag", "main-graph",
driver: GraphDriver.NEO4J,
config: Map.of(
uri: System.getenv("NEO4J_URI"),
username: System.getenv("NEO4J_USERNAME"),
password: System.getenv("NEO4J_PASSWORD")
),
options: Map.of(
"maxConnectionPoolSize", 50,
"connectionTimeout", 30000,
"maxTransactionRetryTime", 30000
)
));
// Configure for production load
client.graph.register({
"tag": "main-graph",
driver: GraphDriver.NEO4J,
config: {
uri: os.Getenv("NEO4J_URI"),
username: os.Getenv("NEO4J_USERNAME"),
password: os.Getenv("NEO4J_PASSWORD"),
},
options: {
"maxConnectionPoolSize": 50,
"connectionTimeout": 30000,
"maxTransactionRetryTime": 30000,
},
});
// Configure for production load
await ductape.graph.register({
["tag"] = "main-graph",
driver: GraphDriver.NEO4J,
config: {
uri: Environment.GetEnvironmentVariable("NEO4J_URI"),
username: Environment.GetEnvironmentVariable("NEO4J_USERNAME"),
password: Environment.GetEnvironmentVariable("NEO4J_PASSWORD"),
},
options: {
["maxConnectionPoolSize"] = 50,
["connectionTimeout"] = 30000,
["maxTransactionRetryTime"] = 30000,
},
});
Health Checks
- TypeScript
- Java
- Go
- .NET
// 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);
});
// Implement health check endpoint
async function healthCheck() Map.of(
try Map.of(
ductape.graph.testConnection(Map.of( "tag", "main-graph" ));
return Map.of( "status", "healthy", "database", "connected" );
) catch (error) Map.of(
return Map.of( "status", "unhealthy", error: error.message );
)
)
// Express example
app.get('/health', async (req, res) => Map.of(
Map<String, Object> health = healthCheck();
res.status(health.status === 'healthy' ? 200 : 503).json(health);
));
// Implement health check endpoint
async function healthCheck() {
try {
client.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) => {
health := healthCheck();
res.status(health.status === 'healthy' ? 200 : 503).json(health);
});
// 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) => {
var 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