Drivers in clustered TypeDB

TypeDB gRPC drivers have been updated to connect to TypeDB clusters and automatically operate across multiple server replicas. The API is experimental and may change between releases. You’ll need an alpha version of TypeDB Driver. These are published alongside the mainstream versions, starting with 3.7.0. To access the releases, visit the TypeDB Driver GitHub page and look for the most recent Pre-release item containing the alpha suffix (e.g., TypeDB Driver 3.7.0-alpha-3).

Alternatively, the TypeDB HTTP endpoint and drivers are unchanged and can be used the usual way by explicitly choosing a specific replica to send requests to.

What’s different in a clustered deployment?

In a single-server deployment, the data is stored on one single machine, and a client can access it only through this one server address.

In a clustered deployment, this information is replicated between a cluster of servers using the Raft consensus algorithm. The users can perform operations across multiple replicas:

  • a primary replica (serves strongly consistent operations),

  • one or more secondary replicas (can serve eventually consistent idempotent operations and be used for failover).

Cluster-enabled drivers add:

  • Flexible connection addresses (single, multiple, or translated).

  • Replica discovery (learning other replicas from the server).

  • Automatic failover (retrying operations against other replicas).

  • Server routing for inspection operations (automatic or targeted).

  • Cluster inspection (servers, primary server, server version).

Connecting to a cluster

Cluster-enabled drivers accept three address formats displayed below.

Single address

Use this when you have one stable endpoint (for example, a load balancer, or a known replica address):

  • Python

  • Java

  • Rust

driver = TypeDB.driver("host1:port1", credentials, driver_options)
Driver driver = TypeDB.driver("host1:port1", credentials, driverOptions)
let driver = TypeDBDriver::new(
    Addresses::try_from_address_str("host1:port1").unwrap(),
    credentials,
    driver_options,
)
.await
.unwrap();

Even if your cluster has multiple nodes, if the driver successfully connects to this address, it will automatically find its peers and establish all the needed connections.

Multiple addresses

To increase the chances of connecting to a functioning node, provide multiple addresses. This is helpful in situations when one of the replicas is down, and there is no way to retrieve the information about its peers.

  • Python

  • Java

  • Rust

driver = TypeDB.driver(
    ["host1:port1", "host2:port2", "host3:port3"],
    credentials,
    driver_options
)
Driver driver = TypeDB.driver(
        Set.of("host1:port1", "host2:port2", "host3:port3"),
        credentials,
        driverOptions
);
let driver = TypeDBDriver::new(
    Addresses::try_from_addresses_str(["host1:port1", "host2:port2", "host3:port3"]).unwrap(),
    credentials,
    driver_options,
)
.await
.unwrap();

Address translation

When replicas advertise private addresses internally (e.g. Docker/Kubernetes/VPC), while the users are outside of the cluster network and are provided with a dynamic public addresses unsuitable for server configuration, use address translation.

In this mode, you provide a mapping of public → private addresses, and the driver can translate replica addresses returned by the server into addresses reachable from your environment:

  • Python

  • Java

  • Rust

translation = {
    "public-1.domain:1729": "10.0.0.11:1729",
    "public-2.domain:1729": "10.0.0.12:1729",
}

driver = TypeDB.driver(translation, credentials, driver_options)
Map<String, String> translation = Map.of(
        "public-1.domain:1729", "10.0.0.11:1729",
        "public-2.domain:1729", "10.0.0.12:1729"
);

Driver driver = TypeDB.driver(translation, credentials, driverOptions);
let translation = HashMap::from([
    ("public-1.domain:1729", "10.0.0.11:1729"),
    ("public-2.domain:1729", "10.0.0.12:1729"),
]);
let addresses = Addresses::try_from_translation_str(translation)?;

let driver = TypeDBDriver::new(
    addresses,
    credentials,
    driver_options,
).await?;

DriverOptions for cluster connections

Cluster-enabled drivers extend DriverOptions with failover and timeout controls.

DriverTlsConfig

TLS configuration has been refactored to avoid ambiguity and protect your data. Now, to construct an options object, it is required to provide a DriverTlsConfig with one of the three modes:

  • disabled TLS (data, including passwords, is sent as plaintext): DriverTlsConfig.disabled()

  • enabled TLS, system’s native root CA is used: DriverTlsConfig.enabled_with_native_root_ca()

  • enabled TLS, custom native root CA is used (provide your own file): DriverTlsConfig.enabled_with_root_ca("path/to/ca-certificate.pem")

primary_failover_retries

Sets the number of times the driver retries finding and re-routing to the primary server on connection failures. This value is used both for polling during leader election (up to N+1 attempts with a 2-second sleep between each) and for re-executing a failed request on a newly discovered primary. Defaults to 1.

request_timeout_millis

Sets the maximum time (in milliseconds) to wait for a response to a unary RPC request. This applies to operations like database creation, user management, and initial transaction opening. It does NOT apply to operations within transactions (queries, commits). Defaults to 2 hours (7200000 milliseconds).

Example

  • Python

  • Java

  • Rust

driver_options = DriverOptions(
    DriverTlsConfig.enabled_with_native_root_ca(),
    primary_failover_retries=1,
    request_timeout_millis=60_000,
)

driver = TypeDB.driver(["host1:port1", "host2:port2"], credentials, driver_options)
DriverOptions driverOptions = new DriverOptions(DriverTlsConfig.enabledWithNativeRootCA())
        .primaryFailoverRetries(1)
        .requestTimeoutMillis(60_000);

Driver driver = TypeDB.driver(
        Set.of("host1:port1", "host2:port2"),
        credentials,
        driverOptions
);
let driver_options = DriverOptions::new(DriverTlsConfig::enabled_with_native_root_ca())
    .primary_failover_retries(1)
    .request_timeout(Duration::from_secs(60));

let driver = TypeDBDriver::new(
    Addresses::try_from_addresses_str(["host1:port1", "host2:port2"]).unwrap(),
    credentials,
    driver_options,
)
.await?;

Server routing

Cluster-enabled drivers introduce ServerRouting to control which server handles an inspection operation.

Most driver operations (database management, user management, transactions) are routed to the most suitable server automatically, and failover is handled transparently. ServerRouting is available on inspection methods to optionally target a specific server. This mechanism will be extended with the evolution of clustered TypeDB.

Variants

Auto

The driver selects the server automatically (typically the primary in a cluster). This is the default.

Direct

Routes the operation to a specific server by address. Useful for debugging, testing, and investigating replica-local state.

Where you can use server routing

Server routing is available on the following driver methods:

  • servers() / primaryServer() — retrieve cluster server information

  • serverVersion() — retrieve the TypeDB version from a specific server

Example

  • Python

  • Java

  • Rust

# Default routing (automatic)
driver.servers()
driver.primary_server()
driver.server_version()

# Explicit automatic routing
driver.servers(ServerRouting.Auto())

# Target a specific server
driver.servers(ServerRouting.Direct("host2:port2"))
driver.server_version(ServerRouting.Direct("host2:port2"))
// Default routing (automatic)
driver.servers();
driver.primaryServer();
driver.serverVersion();

// Explicit automatic routing
driver.servers(new ServerRouting.Auto());

// Target a specific server
driver.servers(new ServerRouting.Direct("host2:port2"));
driver.serverVersion(new ServerRouting.Direct("host2:port2"));
// Default routing (automatic)
driver.servers().await?;
driver.primary_server().await?;
driver.server_version().await?;

// Explicit automatic routing
driver.servers_with_routing(ServerRouting::Auto).await?;

// Target a specific server
driver.servers_with_routing(
    ServerRouting::Direct { address: "host2:port2".parse().unwrap() }
).await?;
driver.server_version_with_routing(
    ServerRouting::Direct { address: "host2:port2".parse().unwrap() }
).await?;

If you connect to a non-clustered (single-node) TypeDB server, server routing has no effect: all operations go to the single server.

Inspect your cluster

Drivers have access to information about the cluster and its servers.

Server properties

Each server exposes the following properties:

Property Description

id

The unique identifier of this server in the cluster.

address

The network address this server is available at.

role

The replication role of this server: Primary, Candidate, or Secondary. May be absent for non-clustered servers.

isPrimary

Whether this server is the current primary (leader) of the cluster.

term

The Raft protocol term of this server. Useful for debugging elections and failover. May be absent for non-clustered servers.

Get all servers

  • Python

  • Java

  • Rust

servers = driver.servers()
for server in servers:
    print(f"Server {server.id}: {server.address}, role={server.role}, term={server.term}")
Set<? extends Server> servers = driver.servers();
for (Server server : servers) {
    System.out.printf("Server %d: %s, role=%s, term=%s%n",
        server.getID(), server.getAddress(), server.getRole(), server.getTerm());
}
let servers = driver.servers().await?;
for server in &servers {
    if let Some(address) = server.address() {
        println!("Server at {address}");
    }
}

Get primary server

  • Python

  • Java

  • Rust

primary = driver.primary_server()
if primary is not None:
    print(f"Primary: {primary.address}")
Optional<? extends Server> primary = driver.primaryServer();
primary.ifPresent(p -> System.out.println("Primary: " + p.getAddress()));
if let Some(primary) = driver.primary_server().await? {
    println!("Primary: {}", primary.address());
}

Get server version

  • Python

  • Java

  • Rust

version = driver.server_version()
print(f"Distribution: {version.distribution}, Version: {version.version}")
ServerVersion version = driver.serverVersion();
System.out.println("Distribution: " + version.getDistribution() + ", Version: " + version.getVersion());
let version = driver.server_version().await?;
println!("Distribution: {}, Version: {}", version.distribution(), version.version());

Quickstart

  • Python

  • Java

  • Rust

from typedb.driver import (
    TypeDB, Credentials, DriverOptions, DriverTlsConfig,
    TransactionType, ServerRouting
)

DATABASE_NAME = "clustered-test"

def test_clustered_typedb():
    driver = TypeDB.driver(
        # Try automatic replica discovery by connecting to only a single server!
        # Use a list of addresses to provide multiple addresses instead.
        ADDRESS,
        Credentials(USERNAME, PASSWORD),
        DriverOptions(DriverTlsConfig.enabled_with_native_root_ca()),
    )

    servers = driver.servers()
    addresses = [server.address for server in servers]
    print(f"Servers known to the driver: {addresses}")

    primary = driver.primary_server()
    if primary is not None:
        print(f"Primary server: {primary.address}")

    version = driver.server_version()
    print(f"Server version: {version.distribution} {version.version}")

    if not driver.databases.contains(DATABASE_NAME):
        driver.databases.create(DATABASE_NAME)

    database = driver.databases.get(DATABASE_NAME)
    print(f"Database exists: {database.name}")

    # Schema transactions are always routed to the primary
    with driver.transaction(DATABASE_NAME, TransactionType.SCHEMA) as tx:
        tx.query("define entity person;").resolve()
        tx.commit()

    with driver.transaction(DATABASE_NAME, TransactionType.WRITE) as tx:
        tx.query("insert $p1 isa person; $p2 isa person;").resolve()
        tx.commit()

    # Read transactions are routed automatically
    with driver.transaction(DATABASE_NAME, TransactionType.READ) as tx:
        answer = tx.query("match $p isa person;").resolve()
        rows = list(answer.as_concept_rows())
        print(f"Persons found: {len(rows)}")

    driver.close()
    print("Done!")


if __name__ == "__main__":
    test_clustered_typedb()
import com.typedb.driver.TypeDB;
import com.typedb.driver.api.Credentials;
import com.typedb.driver.api.Driver;
import com.typedb.driver.api.DriverOptions;
import com.typedb.driver.api.DriverTlsConfig;
import com.typedb.driver.api.Transaction;
import com.typedb.driver.api.QueryAnswer;
import com.typedb.driver.api.ServerRouting;
import com.typedb.driver.api.answer.ConceptRow;
import com.typedb.driver.api.server.Server;
import com.typedb.driver.api.server.ServerVersion;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public class ClusteredTypeDBExample {
    private static final String DATABASE_NAME = "clustered-test";

    public static void main(String[] args) {
        try (Driver driver = TypeDB.driver(
                // Try automatic replica discovery by connecting to only a single server!
                // Use Set.of() with multiple addresses to provide multiple addresses instead.
                ADDRESS,
                new Credentials(USERNAME, PASSWORD),
                new DriverOptions(DriverTlsConfig.enabledWithNativeRootCA())
        )) {
            Set<? extends Server> servers = driver.servers();
            List<String> addresses = servers.stream()
                    .map(Server::getAddress)
                    .collect(Collectors.toList());
            System.out.println("Servers known to the driver: " + addresses);

            Optional<? extends Server> primary = driver.primaryServer();
            primary.ifPresent(p -> System.out.println("Primary server: " + p.getAddress()));

            ServerVersion version = driver.serverVersion();
            System.out.println("Server version: " + version.getDistribution() + " " + version.getVersion());

            if (!driver.databases().contains(DATABASE_NAME)) {
                driver.databases().create(DATABASE_NAME);
            }

            String databaseName = driver.databases().get(DATABASE_NAME).name();
            System.out.println("Database exists: " + databaseName);

            // Schema transactions are always routed to the primary
            try (Transaction tx = driver.transaction(DATABASE_NAME, Transaction.Type.SCHEMA)) {
                tx.query("define entity person;").resolve();
                tx.commit();
            }

            try (Transaction tx = driver.transaction(DATABASE_NAME, Transaction.Type.WRITE)) {
                tx.query("insert $p1 isa person; $p2 isa person;").resolve();
                tx.commit();
            }

            // Read transactions are routed automatically
            try (Transaction tx = driver.transaction(DATABASE_NAME, Transaction.Type.READ)) {
                QueryAnswer answer = tx.query("match $p isa person;").resolve();
                List<ConceptRow> rows = answer.asConceptRows().stream().collect(Collectors.toList());
                System.out.println("Persons found: " + rows.size());
            }

            System.out.println("Done!");
        }
    }
}
use typedb_driver::{
    Addresses, Credentials, DriverOptions, DriverTlsConfig,
    TransactionType, TypeDBDriver, Server, ServerRouting,
};

fn test_clustered_typedb() {
    async_std::task::block_on(async {
        let driver = TypeDBDriver::new(
            // Try automatic replica discovery by connecting to only a single server!
            // Use Addresses::try_from_addresses_str() to provide multiple addresses instead.
            Addresses::try_from_address_str(ADDRESS).unwrap(),
            Credentials::new(USERNAME, PASSWORD),
            DriverOptions::new(DriverTlsConfig::enabled_with_native_root_ca()),
        )
        .await
        .expect("Error while setting up the driver");

        let servers = driver.servers().await.expect("Expected servers retrieval");
        let addresses: Vec<_> = servers.iter()
            .filter_map(|server| server.address().map(|a| a.to_string()))
            .collect();
        println!("Servers known to the driver: {addresses:?}");

        if let Some(primary) = driver.primary_server().await.expect("Expected primary check") {
            println!("Primary server: {}", primary.address());
        }

        let version = driver.server_version().await.expect("Expected version retrieval");
        println!("Server version: {} {}", version.distribution(), version.version());

        const DATABASE_NAME: &str = "clustered-test";

        if !driver.databases().contains(DATABASE_NAME).await.expect("Expected database check") {
            driver.databases().create(DATABASE_NAME).await.expect("Expected database creation");
        }

        let database = driver.databases().get(DATABASE_NAME).await.expect("Expected database retrieval");
        println!("Database exists: {}", database.name());

        // Schema transactions are always routed to the primary
        let transaction = driver
            .transaction(DATABASE_NAME, TransactionType::Schema)
            .await
            .expect("Expected schema transaction");
        transaction.query("define entity person;").await.expect("Expected schema query");
        transaction.commit().await.expect("Expected schema tx commit");

        let transaction = driver
            .transaction(DATABASE_NAME, TransactionType::Write)
            .await
            .expect("Expected write transaction");
        transaction.query("insert $p1 isa person; $p2 isa person;").await.expect("Expected insert query");
        transaction.commit().await.expect("Expected write tx commit");

        // Read transactions are routed automatically
        let transaction = driver
            .transaction(DATABASE_NAME, TransactionType::Read)
            .await
            .expect("Expected read transaction");
        let answer = transaction.query("match $p isa person;").await.expect("Expected read query");
        let rows: Vec<_> = answer.into_rows().try_collect().await.unwrap();
        println!("Persons found: {}", rows.len());

        println!("Done!");
    });
}

Next steps