Best Practices
This guide covers essential best practices for building robust, performant applications with TypeDB drivers. Following these practices will help you avoid common pitfalls and create maintainable database code.
Connection management
Resource cleanup
Always ensure connections are properly closed to avoid resource leaks:
Recommended: Automatic management
Use language-specific automatic resource management features:
-
Python
-
Java
-
Rust
with TypeDB.driver(address, credentials, options) as driver:
# Connection automatically closed when exiting block
with driver.transaction(DB_NAME, TransactionType.READ) as tx:
# Transaction automatically closed
results = tx.query("match $x isa entity;")
try (Driver driver = TypeDB.driver(address, credentials, options);
Transaction tx = driver.transaction(dbName, TransactionType.READ)) {
// Connection and transaction automatically closed
QueryAnswer results = tx.query("match $x isa entity;");
}
let driver = TypeDBDriver::new(address, credentials, options).await?;
let tx = driver.transaction(DB_NAME, TransactionType::Read).await?;
let results = tx.query("match $x isa entity;").await?;
// Driver and transaction automatically closed when going out of scope
Manual management
If you need manual resource management, always use try/finally blocks:
-
Python
-
Java
# MANUAL: Explicit cleanup required
driver = TypeDB.driver(address, credentials, options)
try:
tx = driver.transaction(DB_NAME, TransactionType.READ)
try:
results = tx.query("match $x isa entity;")
# Process results...
finally:
tx.close() # Always close transaction
finally:
driver.close() # Always close driver
// MANUAL: Explicit cleanup required
Driver driver = TypeDB.driver(address, credentials, options);
try {
Transaction tx = driver.transaction(dbName, TransactionType.READ);
try {
QueryAnswer results = tx.query("match $x isa entity;");
// Process results...
} finally {
tx.close(); // Always close transaction
}
} finally {
driver.close(); // Always close driver
}
Connection reuse
Connections are expensive to create and should be reused across multiple operations:
# GOOD: Reuse a single connection for multiple operations
with TypeDB.driver(address, credentials, options) as driver:
# Multiple database operations with same connection
for db_name in ["users", "products", "orders"]:
with driver.transaction(db_name, TransactionType.READ) as tx:
results = tx.query("match $x isa entity;")
# Process results...
# AVOID: Creating new connections for each operation
for db_name in ["users", "products", "orders"]:
with TypeDB.driver(address, credentials, options) as driver: # Inefficient!
with driver.transaction(db_name, TransactionType.READ) as tx:
results = tx.query("match $x isa entity;")
Connection pooling and concurrency
TypeDB drivers handle connection pooling internally:
-
Single driver instance: Can handle multiple concurrent operations
-
Thread safety: Drivers are designed for concurrent use across threads
-
Automatic pooling: No need to manage connection pools manually
class TypeDBClient:
def __init__(self, address, credentials, options):
self.driver = TypeDB.driver(address, credentials, options)
def get_users(self):
with self.driver.transaction("users", TransactionType.READ) as tx:
return list(tx.query("match $u isa user; fetch {{ $u.*; }};").resolve().as_concept_documents())
def get_products(self):
with self.driver.transaction("products", TransactionType.READ) as tx:
return list(tx.query("match $p isa product; fetch {{ $p.* }};").resolve().as_concept_documents())
def close(self):
self.driver.close()
Error handling
Server-side errors
Server-side errors are propagated to the driver. Server errors use fixed Error Codes, such as "QEX14", for different types of errors. You can use these to handle specific errors in your application.
-
Python
-
Java
-
Rust
try:
with driver.transaction(DB_NAME, TransactionType.READ) as tx:
result = tx.query("match $x isa person;").resolve()
# Process result...
except TypeDBDriverException as e:
# check for query execution error
if "QEX" in str(e).upper():
print(f"Query execution error: {e}")
# check for TypeQL parsing error
elif "TQL" in str(e).upper():
print(f"TypeQL parsing error: {e}")
else:
print(f"Server error: {e}")
try (Transaction tx = driver.transaction(dbName, TransactionType.READ)) {
QueryAnswer result = tx.query("match $x isa person;").resolve();
// Process result...
} catch (TypeDBDriverException e) {
// check for query execution error
if (e.getMessage().toUpperCase().contains("QEX")) {
System.err.println("Query execution error: " + e.getMessage());
// check for TypeQL parsing error
} else if (e.getMessage().toUpperCase().contains("TQL")) {
System.err.println("TypeQL parsing error: " + e.getMessage());
} else {
System.err.println("Server error: " + e.getMessage());
}
}
let tx = driver.transaction(DB_NAME, TransactionType::Read).await?;
match tx.query("match $x isa person;").await {
Ok(result) => {
// Process result...
},
Err(e) => {
let error_msg = e.to_string().to_uppercase();
// check for query execution error
if error_msg.contains("QEX") {
eprintln!("Query execution error: {}", e);
// check for TypeQL parsing error
} else if error_msg.contains("TQL") {
eprintln!("TypeQL parsing error: {}", e);
} else {
eprintln!("Server error: {}", e);
}
}
}
Performance optimization
To optimize data loading, a common pattern is to asynchronously submit a batch of queries, resolve them all to catch any errors, and then commit:
def batch_load(driver, queries):
with driver.transaction(DB_NAME, TransactionType.WRITE) as tx:
promises = []
for query in queries:
promises.append(tx.query(query))
# resolve all the promises
for p in promises:
p.resolve() # may throw an exception, such as a syntax error
tx.commit()
If you know that all your queries are valid, the resolve() call can be omitted for even higher performance.
Debugging
Driver logging
TypeDB drivers support logging via environment variables. Set these before running your application:
| Variable | Description |
|---|---|
|
Simple log level ( |
|
Fine-grained control using the same syntax as |
Examples:
# Simple: Enable debug logging for all driver components
export TYPEDB_DRIVER_LOG_LEVEL=debug
python my_app.py
# Advanced: Fine-grained control per component
export TYPEDB_DRIVER_LOG=typedb_driver=debug,typedb_driver_clib=trace
python my_app.py
Enabling logging in your application
-
Python
-
Java
-
Rust
Logging is automatically initialized when the driver module is imported. If you want logging enabled before importing the driver (e.g., to capture import-time logs), you can explicitly initialize it first:
from typedb.native_driver_wrapper import init_logging
init_logging()
# Now import and use the driver
from typedb.driver import TypeDB
Logging is automatically initialized when driver classes are loaded. If you want logging enabled before creating a driver (e.g., to capture class-loading logs), you can explicitly initialize it first:
import com.typedb.driver.TypeDB;
// Explicitly initialize logging before driver creation
TypeDB.initLogging();
// Now create and use the driver
Driver driver = TypeDB.driver(address, credentials, options);
The Rust driver uses the tracing crate and does not use the TYPEDB_DRIVER_LOG* environment variables. Configure your own tracing subscriber using standard Rust logging conventions:
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
// Initialize tracing with environment filter
tracing_subscriber::registry()
.with(EnvFilter::from_default_env())
.with(fmt::layer())
.init();
// Driver logs will now be captured
let driver = TypeDBDriver::new(address, credential).await?;
Set RUST_LOG=typedb_driver=debug to enable debug-level driver logs.
Summary
Following these best practices will help you build robust TypeDB applications:
-
Use automatic resource management whenever possible
-
Reuse connections and handle errors gracefully
-
Choose appropriate transaction types and keep transactions short-lived
-
Batch operations for better performance
-
Secure your credentials and use encryption in production
-
Enable logging with environment variables when debugging issues