4.2. Client applications

This section describes how to manage database connections within an application.

4.2.1. The driver object

A Neo4j client application will require a driver object instance in order to provide access to the database. This object instance should be made available to all parts of the application that need to interact with Neo4j. In languages where thread safety is an issue, the driver can be considered thread-safe.

A note on lifecycle

Applications will typically construct a driver instance on startup and destroy it on exit. Destroying a driver instance will immediately shut down any connections previously opened via that driver; for drivers that contain a connection pool, the entire pool will be shut down.

To construct a driver instance, a connection URI and authentication information must be supplied. Additional configuration details can be supplied if required. All of these details are immutable for the lifetime of the driver. Therefore, if multiple configurations are required (such as when working with multiple database users) then multiple driver objects must be used.

An example of driver construction and destruction can be seen below:

Example 4.6. The driver lifecycle
public class DriverLifecycleExample : IDisposable
{
    public IDriver Driver { get; }

    public DriverLifecycleExample(string uri, string user, string password)
    {
        Driver = GraphDatabase.Driver(uri, AuthTokens.Basic(user, password));
    }

    public void Dispose()
    {
        Driver?.Dispose();
    }
}
public class DriverLifecycleExample implements AutoCloseable
{
    private final Driver driver;

    public DriverLifecycleExample( String uri, String user, String password )
    {
        driver = GraphDatabase.driver( uri, AuthTokens.basic( user, password ) );
    }

    @Override
    public void close() throws Exception
    {
        driver.close();
    }
}
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password));

driver.onCompleted = () => {
  console.log('Driver created');
};

driver.onError = error => {
  console.log(error);
};

const session = driver.session();
session.run('CREATE (i:Item)').then(() => {
  session.close();

  // ... on application exit:
  driver.close();
});
class DriverLifecycleExample:
    def __init__(self, uri, user, password):
        self._driver = GraphDatabase.driver(uri, auth=(user, password))

    def close(self):
        self._driver.close()

4.2.2. Connection URIs

A connection URI identifies a graph database and how to connect to it. The official Neo4j drivers currently support the following URI schemes and driver object types:

Table 4.2. Available URI schemes
URI Scheme Driver object type

bolt

Direct driver

bolt+routing

Routing driver

4.2.2.1. Direct drivers (bolt)

A Direct driver is created via a bolt URI, for example: bolt://localhost:7687. This kind of driver is used to maintain connections to a single database server and is typically used when working with a single Neo4j instance or when targeting a specific member of a cluster. Note that a Routing driver is preferable when working with a causal cluster.

4.2.2.2. Routing drivers (bolt+routing)

A Routing driver is created via a bolt+routing URI, for example: bolt+routing://graph.example.com:7687. The address in the URI must be that of a Core server. This kind of driver uses the Bolt Routing Protocol and works in tandem with the cluster to route transactions to available cluster members.

4.2.2.3. Routing drivers with routing context

Routing drivers with routing context are an available option when using drivers of version 1.3 or above together with a Neo4j Causal Cluster of version 3.2 or above. In such a setup, a Routing driver can include a preferred routing context via the query part of the bolt+routing URI.

In the standard Neo4j configuration, routing contexts are defined on the server side by means of server policies. Thus the driver communicates the routing context to the cluster in the form of a server policy. It then obtains refined routing information back from the cluster, based on the server policy.

The address in the URI of a Routing driver with routing context must be that of a Core server.

Example 4.7. Configure a Routing driver with routing context

This example will assume that Neo4j has been configured for server policies as described in Neo4j Operations Manual → Load balancing for multi-data center systems. In particular, a server policy called europe has been defined. Additionally, we have a server neo01.graph.example.com to which we wish to direct the driver.

This URI will use the server policy europe:

bolt+routing://neo01.graph.example.com?policy=europe

Server-side configuration to enable Routing drivers with routing context

A prerequisite for using a Routing driver with routing context is that the Neo4j database is operated on a Causal Cluster with the Multi-data center licensing option enabled. Additionally, the routing contexts must be defined within the cluster as routing policies. For details on how to configure multi-data center routing policies for a Causal Cluster, please refer to Operations Manual → Causal Clustering.

4.2.3. Authentication

Authentication details are provided as an auth token which contains the user names, passwords or other credentials required to access the database. Neo4j supports multiple authentication standards but uses basic authentication by default.

4.2.3.1. Basic authentication

The basic authentication scheme is backed by a password file stored within the server and requires applications to provide a user name and password. For this, use the basic auth helper:

Example 4.8. Basic authentication
public IDriver CreateDriverWithBasicAuth(string uri, string user, string password)
{
    return GraphDatabase.Driver(uri, AuthTokens.Basic(user, password));
}
public BasicAuthExample( String uri, String user, String password )
{
    driver = GraphDatabase.driver( uri, AuthTokens.basic( user, password ) );
}
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password));
def __init__(self, uri, user, password):
    self._driver = GraphDatabase.driver(uri, auth=(user, password))

The basic authentication scheme can also be used to authenticate against an LDAP server.

4.2.3.2. Kerberos authentication

The Kerberos authentication scheme provides a simple way to create a Kerberos authentication token with a base64 encoded server authentication ticket. The best way to create a Kerberos authentication token is shown below:

Example 4.9. Kerberos authentication
public IDriver CreateDriverWithKerberosAuth(string uri, string ticket)
{
    return GraphDatabase.Driver(uri, AuthTokens.Kerberos(ticket),
        new Config { EncryptionLevel = EncryptionLevel.None });
}
public KerberosAuthExample( String uri, String ticket )
{
    driver = GraphDatabase.driver( uri, AuthTokens.kerberos( ticket ) );
}
const driver = neo4j.driver(uri, neo4j.auth.kerberos(ticket));
def __init__(self, uri, ticket):
    self._driver = GraphDatabase.driver(uri, auth=kerberos_auth(ticket))

The Kerberos authentication token can only be understood by the server if the server has the Kerberos Add-on installed.

4.2.3.3. Custom authentication

For advanced deployments, where a custom security provider has been built, the custom authentication helper can be used.

Example 4.10. Custom authentication
public IDriver CreateDriverWithCustomizedAuth(string uri,
    string principal, string credentials, string realm, string scheme, Dictionary<string, object> parameters)
{
    return GraphDatabase.Driver(uri, AuthTokens.Custom(principal, credentials, realm, scheme, parameters),
        new Config {EncryptionLevel = EncryptionLevel.None});
}
public CustomAuthExample( String uri, String principal, String credentials, String realm, String scheme,
        Map<String,Object> parameters )
{
    driver = GraphDatabase.driver( uri, AuthTokens.custom( principal, credentials, realm, scheme, parameters ) );
}
const driver = neo4j.driver(uri, neo4j.auth.custom(principal, credentials, realm, scheme, parameters));
def __init__(self, uri, principal, credentials, realm, scheme, **parameters):
    self._driver = GraphDatabase.driver(uri, auth=custom_auth(principal, credentials, realm, scheme, **parameters))

4.2.4. Configuration

4.2.4.1. Encryption

TLS encryption is enabled for all connections by default. This can be disabled through configuration as follows:

Example 4.11. Unencrypted
public IDriver CreateDriverWithCustomizedSecurityStrategy(string uri, string user, string password)
{
    return GraphDatabase.Driver(uri, AuthTokens.Basic(user, password),
        new Config {EncryptionLevel = EncryptionLevel.None});
}
public ConfigUnencryptedExample( String uri, String user, String password )
{
    driver = GraphDatabase.driver( uri, AuthTokens.basic( user, password ),
            Config.build().withoutEncryption().toConfig() );
}
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password),
  {
    encrypted: 'ENCRYPTION_OFF'
  }
);
def __init__(self, uri, user, password):
    self._driver = GraphDatabase.driver(uri, auth=(user, password), encrypted=False)

The server can be modified to require encryption for all connections. Please see the Operations Manual → Configure Neo4j connectors for more information.

An attempt to connect to a server using an encryption setting not allowed by that server will result in a Service unavailable status.

4.2.4.2. Trust

During a TLS handshake, the server provides a certificate to the client application. The application can choose to accept or reject this certificate based on one of the following trust strategies:

Table 4.3. Trust strategies
Trust strategy Description

TRUST_ALL_CERTIFICATES (default)

Accept any certificate provided by the server

TRUST_CUSTOM_CA_SIGNED_CERTIFICATES

Accept any certificate that can be verified against a custom CA

TRUST_SYSTEM_CA_SIGNED_CERTIFICATES

Accept any certificate that can be verified against the system store

Example 4.12. Trust
public IDriver CreateDriverWithCustomizedTrustStrategy(string uri, string user, string password)
{
    return GraphDatabase.Driver(uri, AuthTokens.Basic(user, password),
        new Config {TrustStrategy = TrustStrategy.TrustAllCertificates});
}
public ConfigTrustExample( String uri, String user, String password )
{
    driver = GraphDatabase.driver( uri, AuthTokens.basic( user, password ),
            Config.build().withTrustStrategy( Config.TrustStrategy.trustAllCertificates() ).toConfig() );
}
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password),
  {
    encrypted: 'ENCRYPTION_ON',
    trust: 'TRUST_ALL_CERTIFICATES'
  }
);
def __init__(self, uri, user, password):
    self._driver = GraphDatabase.driver(uri, auth=(user, password), trust=TRUST_ALL_CERTIFICATES)

4.2.4.3. Connection pool management

The driver maintains a pool of connections. The pooled connections are reused by sessions and transactions to avoid the overhead added by establishing new connections for every query. The connection pool always starts up empty. New connections are created on demand by sessions and transactions. When a session or a transaction is done with its execution, the connection will be returned to the pool to be reused.

Application users can tune connection pool settings to configure the driver for different use cases based on client performance requirements and database resource consumption limits.

Detailed descriptions of connection pool settings available via driver configuration are listed below:

MaxConnectionLifetime
Pooled connections older than this threshold will be closed and removed from the pool. The actual removal happens during connection acquisition so that the new session returned is never backed by an old connection. Setting this option to a low value will cause a high connection churn and might result in a performance drop. It is recommended to pick a value smaller than the maximum lifetime exposed by the surrounding system infrastructure (such as operating system, router, load balancer, proxy and firewall). Negative values result in lifetime not being checked. Default value: 1h.
MaxConnectionPoolSize
This setting defines the maximum total number of connections allowed, per host, to be managed by the connection pool. In other words, for a direct driver, this sets the maximum number of connections towards a single database server. For a routing driver this sets the maximum amount of connections per cluster member. If a session or transaction tries to acquire a connection at a time when the pool size is at its full capacity, it must wait until a free connection is available in the pool or the request to acquire a new connection times out. The connection acquiring timeout is configured via ConnectionAcquisitionTimeout. Default value: This is different for different drivers, but is a number in the order of 100.
ConnectionAcquisitionTimeout
This setting limits the amount of time a session or transaction can spend waiting for a free connection to appear in the pool before throwing an exception. The exception thrown in this case is ClientException. Timeout only applies when connection pool is at its max capacity. Default value: 1m.
Example 4.13. Connection pool management
public IDriver CreateDriverWithCustomizedConnectionPool(string uri, string user, string password)
{
    return GraphDatabase.Driver(uri, AuthTokens.Basic(user, password),
        new Config
        {
            MaxConnectionLifetime = TimeSpan.FromMinutes(30),
            MaxConnectionPoolSize = 50,
            ConnectionAcquisitionTimeout = TimeSpan.FromMinutes(2)
        });
}
public ConfigConnectionPoolExample( String uri, String user, String password )
{
    driver = GraphDatabase.driver( uri, AuthTokens.basic( user, password ), Config.build()
            .withMaxConnectionLifetime( 30, TimeUnit.MINUTES )
            .withMaxConnectionPoolSize( 50 )
            .withConnectionAcquisitionTimeout( 2, TimeUnit.MINUTES )
            .toConfig() );
}
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password),
  {
    maxConnectionLifetime: 3 * 60 * 60 * 1000, // 3 hours
    maxConnectionPoolSize: 50,
    connectionAcquisitionTimeout: 2 * 60 * 1000 // 120 seconds
  }
);
def __init__(self, uri, user, password):
    self._driver = GraphDatabase.driver(uri, auth=(user, password),
                                        max_connection_lifetime=30 * 60, max_connection_pool_size=50,
                                        connection_acquisition_timeout=2 * 60)

4.2.4.4. Connection timeout

To configure the maximum time allowed to establish a connection, pass a duration value to the driver configuration. For example:

Example 4.14. Connection timeout
public IDriver CreateDriverWithCustomizedConnectionTimeout(string uri, string user, string password)
{
    return GraphDatabase.Driver(uri, AuthTokens.Basic(user, password),
        new Config {ConnectionTimeout = TimeSpan.FromSeconds(15)});
}
public ConfigConnectionTimeoutExample( String uri, String user, String password )
{
    driver = GraphDatabase.driver( uri, AuthTokens.basic( user, password ),
            Config.build().withConnectionTimeout( 15, SECONDS ).toConfig() );
}
This feature is not available in the JavaScript driver.
def __init__(self, uri, user, password):
    self._driver = GraphDatabase.driver(uri, auth=(user, password), connection_timeout=15)

4.2.4.5. Load balancing strategy

A routing driver contains a load balancer to route queries evenly among many cluster members. The built-in load balancer provides two strategies: least-connected and round-robin. The least-connected strategy in general gives a better performance as it takes query execution time and server load into consideration when distributing queries among the cluster members. Default value: least-connected.

Example 4.15. Load balancing strategy
public IDriver CreateDriverWithCustomizedLoadBalancingStrategy(string uri, string user, string password)
{
    return GraphDatabase.Driver(uri, AuthTokens.Basic(user, password),
        new Config
        {
            LoadBalancingStrategy = LoadBalancingStrategy.LeastConnected
        });
}
public ConfigLoadBalancingStrategyExample( String uri, String user, String password )
{
    driver = GraphDatabase.driver( uri, AuthTokens.basic( user, password ), Config.build()
            .withLoadBalancingStrategy( Config.LoadBalancingStrategy.LEAST_CONNECTED )
            .toConfig() );
}
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password),
  {
    loadBalancingStrategy: "least_connected"
  }
);
def __init__(self, uri, user, password):
    self._driver = GraphDatabase.driver(uri, auth=(user, password), load_balancing_strategy=LOAD_BALANCING_STRATEGY_LEAST_CONNECTED)

4.2.4.6. Max retry time

To configure retry behavior, supply a value for the maximum time in which to keep attempting retries of transaction functions. For example:

Example 4.16. Max retry time
public IDriver CreateDriverWithCustomizedMaxRetryTime(string uri, string user, string password)
{
    return GraphDatabase.Driver(uri, AuthTokens.Basic(user, password),
        new Config {MaxTransactionRetryTime = TimeSpan.FromSeconds(15)});
}
public ConfigMaxRetryTimeExample( String uri, String user, String password )
{
    driver = GraphDatabase.driver( uri, AuthTokens.basic( user, password ),
            Config.build().withMaxTransactionRetryTime( 15, SECONDS ).toConfig() );
}
const maxRetryTimeMs = 15 * 1000; // 15 seconds
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password),
  {
    maxTransactionRetryTime: maxRetryTimeMs
  }
);
def __init__(self, uri, user, password):
    self._driver = GraphDatabase.driver(uri, auth=(user, password), max_retry_time=15)

Note that the time specified here does not take into account the running time of the unit of work itself, merely a limit after which retries will no longer be attempted.

4.2.5. Service unavailable

A Service unavailable status will be signalled when the driver is no longer able to establish communication with the server, even after retries. Encountering this condition usually indicates a fundamental networking or database problem. Applications should be designed to cater for this eventuality.

Example 4.17. Service unavailable
public bool AddItem()
{
    try
    {
        using (var session = Driver.Session())
        {
            return session.WriteTransaction(
                tx =>
                {
                    tx.Run("CREATE (a:Item)");
                    return true;
                }
            );
        }
    }
    catch (ServiceUnavailableException)
    {
        return false;
    }
}
public boolean addItem()
{
    try ( Session session = driver.session() )
    {
        return session.writeTransaction( new TransactionWork<Boolean>()
        {
            @Override
            public Boolean execute( Transaction tx )
            {
                tx.run( "CREATE (a:Item)" );
                return true;
            }
        } );
    }
    catch ( ServiceUnavailableException ex )
    {
        return false;
    }
}
const driver = neo4j.driver(uri, neo4j.auth.basic(user, password), {maxTransactionRetryTime: 3000});
const session = driver.session();

const writeTxPromise = session.writeTransaction(tx => tx.run('CREATE (a:Item)'));

writeTxPromise.catch(error => {
  if (error.code === neo4j.error.SERVICE_UNAVAILABLE) {
    console.log('Unable to create node: ' + error.code);
  }
});
def add_item(self):
    try:
        with self._driver.session() as session:
            session.write_transaction(lambda tx: tx.run("CREATE (a:Item)"))
        return True
    except ServiceUnavailable:
        return False