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.

Security considerations

  • Never hardcode credentials: Use environment variables or secure configuration management

  • Use encryption: Always enable TLS network encryption for production or publicly accessible deployments

Debugging

Driver logging

TypeDB drivers support logging via environment variables. Set these before running your application:

Variable Description

TYPEDB_DRIVER_LOG_LEVEL

Simple log level (debug, info, warn, error, trace) applied to all driver components. This is the easiest way to enable logging.

TYPEDB_DRIVER_LOG

Fine-grained control using the same syntax as RUST_LOG. Allows setting different levels for different components. TYPEDB_DRIVER_LOG_LEVEL takes precedence if both are set.

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:

  1. Use automatic resource management whenever possible

  2. Reuse connections and handle errors gracefully

  3. Choose appropriate transaction types and keep transactions short-lived

  4. Batch operations for better performance

  5. Secure your credentials and use encryption in production

  6. Enable logging with environment variables when debugging issues