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

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