Graph Databases: Neo4j Use Cases & Architecture

When and how to use graph databases in enterprise applications. Covers Neo4j fundamentals, Cypher queries, data modeling, performance tuning, and real-world use cases from fraud detection to recommendation engines.

Graph databases are ideal when data is highly interconnected and queries traverse relationships frequently. Neo4j is the dominant open-source graph database, and this page details how to use it effectively in real systems—when to choose it, how to model for it, how to deploy it, and how to tune it for performance and reliability.

Modeling Data as Graphs

When data is naturally relational—nodes connected by labeled edges—modeling in Neo4j becomes intuitive and efficient. Use CREATE with MERGE for idempotent setup of nodes and relationships.

MERGE (u:User {id: $userId})
  ON CREATE SET u.name = $name, u.email = $email
  ON MATCH SET u.last_seen = timestamp()

MERGE (p:Product {sku: $sku})
  ON CREATE SET p.name = $productName, p.price = $price

MERGE (u)-[:PURCHASED {on: $timestamp}]->(p)
RETURN u, p

The MERGE clause is essential: it ensures consistency across repeated operations and avoids duplicates. When a MERGE fails to create a node, the ON CREATE and ON MATCH clauses execute different logic. Use ON CREATE to populate default values, and ON MATCH to track changes.

Avoiding Over-Indexing

Neo4j uses indexes on labels and properties. Create them explicitly:

CREATE INDEX ON :User(email);
CREATE INDEX ON :Product(sku);
CREATE INDEX ON :User(name);

Indexing on :User(email) allows fast lookups, but indexing on :User(name) creates an additional index that may be redundant. The query planner chooses between them based on selectivity. A MATCH (u:User {name: 'Alice'}) RETURN u will use the name index, but a MATCH (u:User) WHERE u.name CONTAINS 'A' may fall back to a full scan.

Indexing on :User(email) is critical for MERGE (u:User {email: $email}), but if the email field is not unique, MERGE may still create duplicate users. Use CREATE UNIQUE for guaranteed uniqueness:

CREATE UNIQUE (u:User {email: $email})
  SET u.name = $name
RETURN u

The CREATE UNIQUE pattern ensures that exactly one User node exists per email, with the name updated if the user is already present.

Querying and Traversing Relationships

Graph queries in Neo4j express relationships directly. Use MATCH with patterns to traverse paths.

MATCH (u:User)-[:PURCHASED]->(p:Product)<-[:SUPPLIED_BY]-(s:Supplier)
WHERE u.name = 'Alice'
RETURN u.name, p.name, s.name
ORDER BY p.price DESC

The query returns users who bought products supplied by a supplier. The MATCH clause defines a graph pattern: u connected to p via PURCHASED, and p connected to s via SUPPLIED_BY.

Performance: Profile and Optimize Queries

Use EXPLAIN to analyze query execution:

EXPLAIN
MATCH (u:User)-[:PURCHASED]->(p:Product)
WHERE u.name = 'Alice'
RETURN p.name, p.price

The output shows execution steps: index seeks, scans, and join types. Use PROFILE to see actual runtime:

PROFILE
MATCH (u:User {name: 'Alice'})-[:PURCHASED]->(p:Product)
WHERE p.price > 100
RETURN p.name, p.price

The PROFILE output reveals which nodes are scanned, how many rows are returned at each step, and the time taken. A common failure mode is missing an index on Product.price, causing a full scan on every query.

Traversing with Variable-Length Paths

For recommendations or navigation, use variable-length paths:

MATCH (u:User {name: 'Alice'})-[:PURCHASED*1..3]->(p:Product)
RETURN p.name, COUNT(*) AS frequency
ORDER BY frequency DESC

This finds products bought directly, or through one or two intermediate steps (e.g., bought a book, then a pen). The *1..3 pattern matches paths of length 1 to 3.

Use WITH to pipe results through multiple steps:

MATCH (u:User {name: 'Alice'})
WITH u
MATCH (u)-[:PURCHASED]->(p:Product)
WITH u, p
MATCH (p)<-[:PURCHASED]-(other:User)
WHERE other <> u
RETURN other.name, COUNT(*) AS recommendations
ORDER BY recommendations DESC
LIMIT 10

The WITH clause enables complex multi-stage processing, and is essential for building recommendation engines.

Deploying and Scaling Neo4j

Deploy Neo4j as a service using systemctl or docker.


sudo systemctl start neo4j

# Check status
sudo systemctl status neo4j

# View logs
sudo journalctl -u neo4j -f

Configuration is in conf/neo4j.conf. Key settings:

# conf/neo4j.conf
dbms.connectors.default_listen_address=0.0.0.0
dbms.connector.bolt.listen_address=0.0.0.0:7687
dbms.connector.http.listen_address=0.0.0.0:7474

dbms.memory.pagecache.size=4G
dbms.memory.heap.initial_size=2G
dbms.memory.heap.max_size=4G

dbms.default_page_cache_size=4G
dbms.transaction.log.rotation.size=100M
dbms.transaction.log.rotation.frequency=10m

dbms.tx_log.checkpoint.interval=30m

The dbms.memory.pagecache.size controls how much memory is used for caching database pages. If the working set exceeds this size, performance degrades due to disk I/O.

Tuning Checkpoints and Transaction Logs

Checkpoints flush dirty pages from memory to disk. The dbms.tx_log.checkpoint.interval determines how often to trigger a checkpoint. The dbms.transaction.log.rotation.size sets the maximum size before rotating the transaction log file.

A common failure mode: a checkpoint takes longer than the interval, causing overlapping checkpoints. If a transaction log rotation occurs during a checkpoint, the system may lose data or delay recovery.

Use neo4j-admin for administrative tasks:

# Optimize database
neo4j-admin database online --name=neo4j

# Backup database
neo4j-admin backup --backup-dir=/backups/neo4j --name=neo4j

# Restore database
neo4j-admin restore --backup-dir=/backups/neo4j --name=neo4j

The neo4j-admin tool is essential for maintenance. It runs offline, so it can be used during deployment or during maintenance windows.

High Availability and Clustering

For production systems, use Neo4j Enterprise Edition with clustering.

# conf/neo4j.conf
dbms.mode=HA
dbms.connector.ha.listen_address=0.0.0.0:5000
dbms.connector.ha.advertised_address=cluster1:5000

dbms.cluster.discovery.type=static
dbms.cluster.discovery.static.members=cluster1:5000,cluster2:5000,cluster3:5000

The dbms.mode=HA enables high availability. Each node in the cluster runs a bolt connector on port 7687 and a ha connector on 5000.

The dbms.cluster.discovery.type=static means the cluster members are listed explicitly. A node fails to join if it cannot reach any of the static members.

Failover and Consistency

When a leader node fails, the cluster elects a new leader. Clients connect to any cluster node via bolt://cluster1:7687, and the driver handles routing.

The bolt protocol supports routing: clients send a BEGIN transaction to any node, and the server returns a ROUTE message with the current leader and all followers.

Use cypher with dbms.procedures.list to inspect cluster state:

CALL dbms.procedures.list()
YIELD name, signature
WHERE name CONTAINS 'cluster'
RETURN name, signature
ORDER BY name

The dbms.procedures.list function returns all available procedures, including those related to clustering.

Recovery and Data Consistency

After a node failure, the cluster recovers by replaying transaction logs. The recovery process is automatic but can be monitored:

# View cluster status
curl http://cluster1:7474/db/manage/server/cluster

# Example response
{
  "cluster": {
    "name": "neo4j-cluster",
    "status": "RUNNING",
    "members": [
      {
        "id": 1,
        "address": "cluster1:5000",
        "role": "LEADER",
        "status": "ONLINE",
        "lastUpdated": "2024-04-05T12:00:00Z"
      },
      {
        "id": 2,
        "address": "cluster2:5000",
        "role": "FOLLOWER",
        "status": "ONLINE",
        "lastUpdated": "2024-04-05T12:00:05Z"
      }
    ]
  }
}

The curl command returns a JSON object describing the cluster. The status field indicates whether the cluster is running, and each member shows its role and last update.

A common failure mode: a follower node fails to receive updates. The lastUpdated timestamp lags behind the leader, indicating network latency or disk I/O issues.

Integrating with Application Code

Use the Neo4j Java driver for high-throughput applications:

try (Driver driver = GraphDatabase.driver("bolt://cluster1:7687", AuthTokens.basic("neo4j", "password"))) {
  try (Session session = driver.session()) {
    Result result = session.run(
      "MATCH (u:User {name: $name})-[:PURCHASED]->(p:Product) " +
      "RETURN p.name, p.price ORDER BY p.price DESC",
      parameters("name", "Alice")
    );

    while (result.hasNext()) {
      Record record = result.next();
      String productName = record.get("p.name").asString();
      double price = record.get("p.price").asDouble();
      System.out.println(productName + " - " + price);
    }
  }
}

The GraphDatabase.driver creates a connection pool. The Session object manages transactions, and Result provides access to query output.

Managing Transactions

Use session.beginTransaction() to create a transaction:

try (Session session = driver.session()) {
  Transaction tx = session.beginTransaction();
  try {
    tx.run("CREATE (u:User {name: $name})", parameters("name", "Bob"));
    tx.run("MATCH (u:User {name: $name}) SET u.last_login = $time",
           parameters("name", "Bob", "time", System.currentTimeMillis()));
    tx.commit();
  } catch (Exception e) {
    tx.rollback();
    throw e;
  }
}

Transactions are atomic: either all statements succeed, or all are rolled back. A failure in one statement rolls back the entire transaction.

Error Handling and Connection Failures

A common error is java.lang.RuntimeException: Failed to connect to bolt://cluster1:7687 with Caused by: java.net.SocketTimeoutException: Read timed out. This indicates that the client waited longer than expected for a response.

Use dbms.connector.bolt.max_connection_lifetime and dbms.connector.bolt.idle_timeout to tune connection lifetimes:

dbms.connector.bolt.max_connection_lifetime=1h
dbms.connector.bolt.idle_timeout=30m

If connections are idle longer than idle_timeout, the server closes them. Clients must reconnect on next request. A failure occurs when a client sends a request to a closed connection.

Monitoring and Maintenance

Use neo4j-admin to monitor database health:

# Check database status
neo4j-admin db status --name=neo4j

# Output
Name: neo4j
Status: ONLINE
Version: 5.5
Last checkpoint: 2024-04-05T12:00:00Z
Last transaction ID: 123456

The db status command returns the current state of the database, including the last checkpoint and transaction ID.

Use neo4j-admin db dump to create a backup:

neo4j-admin db dump --name=neo4j --backup-dir=/backups/neo4j

The backup is a directory with data, logs, and metadata subdirectories. Restore it using neo4j-admin restore.

Common Pitfalls and Sharp Edges

The key insight: a graph database is not just a storage engine—it is a query engine. Queries that traverse relationships are not just faster, but fundamentally different in structure and intent. Design for traversals, not just lookups.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.