---
title: Use the Java client
description: "relate, the embedded Cypher graph database (relatedb.ai): Add the com.relate:relate jar to a Maven or Gradle build, open stores in each mode, bind typed parameters, run transactions, and handle exceptions."
type: how-to
surfaces:
  - java
status: stable
verified: 0.1.2
verified_by:
  - java
samples:
  - https://relatedb.ai/docs/guides/embed/java/samples/open.json
  - https://relatedb.ai/docs/guides/embed/java/samples/params.json
  - https://relatedb.ai/docs/guides/embed/java/samples/transaction.json
  - https://relatedb.ai/docs/guides/embed/java/samples/metadata.json
  - https://relatedb.ai/docs/guides/embed/java/samples/errors.json
  - https://relatedb.ai/docs/guides/embed/java/samples/verify.json
canonical_url: https://relatedb.ai/docs/guides/embed/java/
prerequisites:
  - get-started/java
related:
  - reference/api/java
  - get-started/java
  - reference/errors
  - concepts/transactions-and-concurrency
  - guides/storage/choose-a-store-layout
  - reference/project
  - concepts/problems-relate-solves#embedded-graph-in-an-app
last_updated: 2026-11-08
---

# Use the Java client

Add the com.relate:relate jar to a Maven or Gradle build, open stores in each mode, bind typed parameters, run transactions, and handle exceptions.

> Verified against relate 0.1.2. If your `relate --version` is newer, read https://relatedb.ai/docs/reference/changelog/#since-0.1.2

This guide shows you how to use the relate Java [client](https://relatedb.ai/docs/reference/glossary/#client) in an app: the Maven or Gradle dependency, how the jar loads its native engine, which store to open and how, which Java types values arrive as, how to group writes and retry a conflict, what a result reports about itself, and how to branch on an exception. The client runs the engine inside your Java virtual machine (JVM) through JNA (Java Native Access). Every program on this page writes to a store it creates, using the team graph from the [Java tutorial](https://relatedb.ai/docs/get-started/java/).

## Prerequisites

- Java 17 or later. The programs on this page ran on JDK 21.0.13 with JNA 5.14.0 and Jackson databind 2.17.2, the client's two dependencies.
- Maven or Gradle.
- macOS (arm64 or x86-64), Linux with glibc (arm64 or x86-64), or Windows (x86-64): the platforms the jar carries an engine for.

## Add the client

The artifact is `com.relate:relate` and the package is `com.relate`. The plain jar carries the engine for all five platforms, about 40 MB; in these blocks `<release>` is the release version:

```xml title="pom.xml"
<dependency>
  <groupId>com.relate</groupId>
  <artifactId>relate</artifactId>
  <version><release></version>
</dependency>
```

```kotlin title="build.gradle.kts"
dependencies { implementation("com.relate:relate:<release>") }
```

For a smaller deployable, depend on one classifier jar instead, which carries one platform's engine. Depend on the plain jar or one classifier jar, never both:

| Classifier       | Platform            |
| ---------------- | ------------------- |
| `osx-aarch_64`   | macOS arm64         |
| `osx-x86_64`     | macOS x86-64        |
| `linux-aarch_64` | Linux arm64, glibc  |
| `linux-x86_64`   | Linux x86-64, glibc |
| `windows-x86_64` | Windows x86-64      |

The names follow the vocabulary of the `os-maven-plugin` build extension for Maven and of `com.google.osdetector` for Gradle. With the extension, `<classifier>${os.detected.classifier}</classifier>` picks the build host's; in a Gradle build that applies `com.google.osdetector`, `"com.relate:relate:<release>:${osdetector.classifier}"` does. A container image built for another platform names its classifier literally. The jars are attached to each release on the releases page; resolving `com.relate:relate` from Maven Central is a launch-day target. [Project](https://relatedb.ai/docs/reference/project/#platforms) lists what each release publishes, and where.

## Load the native engine

The first use of `Database` in a JVM resolves the engine once:

1. When `RELATE_LIB` is set and not empty, its path is loaded. It is a development override for a library you built, not a deployment mechanism.
2. Otherwise the jar's engine for the running platform is extracted to a cache directory and loaded from there.
3. Otherwise the call throws `RelateException`, naming the platform and the classifier to add.

The system library path, `java.library.path` and `jna.library.path` are never searched. The extracted file is reused by every later JVM:

| Platform | Cache root                                                                                       |
| -------- | ------------------------------------------------------------------------------------------------ |
| macOS    | `~/Library/Caches/relate/native`                                                                 |
| Linux    | `$XDG_CACHE_HOME/relate/native` when `XDG_CACHE_HOME` is absolute, else `~/.cache/relate/native` |
| Windows  | `%LOCALAPPDATA%\relate\native`                                                                   |

On a read-only home directory or a `noexec` mount, pass `-Drelate.native.cache=<dir>` to move the cache. On Java 24 or later, pass `--enable-native-access=ALL-UNNAMED` to the JVM: JNA loads through JNI, and without the flag the JVM prints `WARNING: A restricted method in java.lang.System has been called` on first use.

## Open a store

`Database.open(path)` opens a store for writing and creates it when it is missing; `Database.openReadOnly(path)` loads an existing one once. The path decides the layout:

| Call                          | Opens                                                                                  | Notes                                                                                 |
| ----------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `Database.open(":memory:")`   | An in-memory store                                                                     | Gone when the database closes                                                         |
| `Database.open("./team")`     | A directory store                                                                      | Created when missing; another process may read it with `openReadOnly` while you write |
| `Database.open("./team.rl")`  | A single-file store                                                                    | Created when missing; opens only while no other process writes it                     |
| `Database.openReadOnly(path)` | An existing store, [read-only](https://relatedb.ai/docs/reference/glossary/#read-only) | Refuses writes and `checkpoint()`; a missing store is an error, never created         |
| `db.connect("system")`        | A connection in another namespace                                                      | `connect()` is `connect("neo4j")`; names match case-insensitively                     |

`Database` and `Database.Connection` are `AutoCloseable`. Close both, ideally with try-with-resources: until the database and its connections close, a file [store](https://relatedb.ai/docs/reference/glossary/#store) cannot be opened again in the same JVM. This program writes `team.rl`, closes it, and reads it back read-only:

```java run name=open
import com.relate.ClientException;
import com.relate.Database;
import java.nio.file.Files;
import java.nio.file.Path;

public class Open {
    static final String TEAM = """
        CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
               (grace:Person {name: 'Grace', role: 'engineer'}),
               (noor:Person {name: 'Noor', role: 'maintainer'}),
               (relate:Project {name: 'relate', language: 'Rust'}),
               (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
               (ada)-[:WORKS_ON {since: 2024}]->(relate),
               (grace)-[:WORKS_ON {since: 2023}]->(relate),
               (grace)-[:WORKS_ON {since: 2022}]->(tck),
               (noor)-[:MAINTAINS]->(relate),
               (ada)-[:KNOWS]->(grace)
        """;

    public static void main(String[] args) {
        try (Database db = Database.open("./team.rl");
             Database.Connection conn = db.connect()) {
            conn.query(TEAM);
        }
        System.out.println("team.rl exists: " + Files.exists(Path.of("team.rl")));

        try (Database db = Database.openReadOnly("./team.rl");
             Database.Connection conn = db.connect()) {
            var people = conn.query("MATCH (p:Person) RETURN count(p) AS people");
            System.out.println("people: " + people.rows().get(0).get(0));
            try {
                conn.query("CREATE (:Person {name: 'Lin'})");
            } catch (ClientException e) {
                System.out.println("refused: " + e.getGqlStatus() + " " + e.getCode());
            }
        }
    }
}
```

```text
team.rl exists: true
people: 3
refused: 42N18 Neo.ClientError.Statement.SemanticError
```

A connection serves one thread at a time; its methods are `synchronized`, so concurrent callers queue. For parallel work, open one connection per thread from the same `Database`.

## Bind parameters and read typed values

`conn.query(statement, params)` takes a `Map<String, ?>`, and each value binds as its natural Cypher type: `Integer` and `Long` as `INTEGER`, `LocalDate` as `DATE`, `VectorValue` as `VECTOR`, a `List` as `LIST`. A Java type outside the supported set throws `IllegalArgumentException` before the statement runs. Cells come back typed in the same way: nodes as the `Node` record, integers as `Long`, dates as `java.time` values.

```java run name=params
import com.relate.Database;
import com.relate.Node;
import com.relate.VectorValue;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;

public class Params {
    static final String TEAM = """
        CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
               (grace:Person {name: 'Grace', role: 'engineer'}),
               (noor:Person {name: 'Noor', role: 'maintainer'}),
               (relate:Project {name: 'relate', language: 'Rust'}),
               (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
               (ada)-[:WORKS_ON {since: 2024}]->(relate),
               (grace)-[:WORKS_ON {since: 2023}]->(relate),
               (grace)-[:WORKS_ON {since: 2022}]->(tck),
               (noor)-[:MAINTAINS]->(relate),
               (ada)-[:KNOWS]->(grace)
        """;

    public static void main(String[] args) {
        try (Database db = Database.open(":memory:");
             Database.Connection conn = db.connect()) {
            conn.query(TEAM);

            var team = conn.query("""
                MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project})
                RETURN p, w.since AS since ORDER BY since""",
                Map.of("project", "relate"));
            for (List<Object> row : team.rows()) {
                Node person = (Node) row.get(0);
                Long since = (Long) row.get(1);
                System.out.println(person.properties().get("name") + " " + person.labels() + " since " + since);
            }

            var noor = conn.query("""
                MATCH (p:Person {name: $name})
                SET p.joined = $joined, p.focus = $focus
                RETURN p.joined AS joined, p.focus AS focus, elementId(p) AS id""",
                Map.of("name", "Noor",
                       "joined", LocalDate.of(2025, 3, 1),
                       "focus", new VectorValue("FLOAT32", List.of(0.25, 0.5, 1.0))));
            List<Object> row = noor.rows().get(0);
            System.out.println(row.get(0).getClass().getSimpleName() + " " + row.get(0));
            System.out.println(row.get(1));

            var byId = conn.query("MATCH (p) WHERE elementId(p) = $id RETURN p.name AS name",
                Map.of("id", row.get(2)));
            System.out.println("by element id: " + byId.rows().get(0).get(0));
        }
    }
}
```

```text
Grace [Person] since 2023
Ada [Person] since 2024
LocalDate 2025-03-01
VectorValue[dtype=FLOAT32, coordinates=[0.25, 0.5, 1.0]]
by element id: Noor
```

An element id comes back spelled exactly as `elementId()` returns it, so it round-trips through a parameter. The [Java API](https://relatedb.ai/docs/reference/api/java/#values) has the type of every Cypher kind in both directions.

## Group writes in a transaction

Outside a transaction, each `query` commits on its own. `begin()` opens an explicit transaction on the connection; `commit()` applies its statements together and returns its [bookmark](https://relatedb.ai/docs/reference/glossary/#bookmark), `"relate:<n>"`, or `null` when it wrote nothing, and `rollback()` discards them. A [conflict](https://relatedb.ai/docs/reference/glossary/#conflict), another connection committing a change to the same node first, throws `TransientException` from `commit()`; it is the one exception worth retrying, and a failed commit ends the transaction. This program commits one change, rolls one back, and retries a unit of work that loses a conflict on its first attempt:

```java run name=transaction
import com.relate.ClientException;
import com.relate.Database;
import com.relate.RelateException;
import java.util.function.Consumer;

public class Transactions {
    static final String TEAM = """
        CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
               (grace:Person {name: 'Grace', role: 'engineer'}),
               (noor:Person {name: 'Noor', role: 'maintainer'}),
               (relate:Project {name: 'relate', language: 'Rust'}),
               (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
               (ada)-[:WORKS_ON {since: 2024}]->(relate),
               (grace)-[:WORKS_ON {since: 2023}]->(relate),
               (grace)-[:WORKS_ON {since: 2022}]->(tck),
               (noor)-[:MAINTAINS]->(relate),
               (ada)-[:KNOWS]->(grace)
        """;

    static String inTransaction(Database.Connection conn, Consumer<Integer> work) {
        for (int attempt = 1; ; attempt++) {
            conn.begin();
            try {
                work.accept(attempt);
                return conn.commit();
            } catch (RelateException e) {
                try {
                    conn.rollback();
                } catch (ClientException ended) {
                    // a failed commit has already ended the transaction
                }
                if (!e.isRetryable() || attempt == 3) {
                    throw e;
                }
                System.out.println("attempt " + attempt + ": " + e.getMessage());
            }
        }
    }

    public static void main(String[] args) {
        try (Database db = Database.open(":memory:");
             Database.Connection conn = db.connect();
             Database.Connection other = db.connect()) {
            conn.query(TEAM);

            conn.begin();
            conn.query("MATCH (n:Person {name: 'Noor'}), (t:Project {name: 'opencypher-tck'}) "
                + "CREATE (n)-[:WORKS_ON {since: 2025}]->(t)");
            conn.query("MATCH (n:Person {name: 'Noor'}) SET n.role = 'engineer'");
            System.out.println("committed: " + conn.commit());

            conn.begin();
            conn.query("MATCH (a:Person {name: 'Ada'}) DETACH DELETE a");
            conn.rollback();
            System.out.println("people: " + conn.query("MATCH (p:Person) RETURN count(p)").rows().get(0).get(0));

            String bookmark = inTransaction(conn, attempt -> {
                conn.query("MATCH (p:Project {name: 'relate'}) SET p.reviews = coalesce(p.reviews, 0) + 1");
                if (attempt == 1) {
                    other.query("MATCH (p:Project {name: 'relate'}) SET p.reviews = 10");
                }
            });
            var reviews = conn.query("MATCH (p:Project {name: 'relate'}) RETURN p.reviews").rows().get(0).get(0);
            System.out.println("committed: " + bookmark + ", reviews: " + reviews);
        }
    }
}
```

```text
committed: relate:2
people: 3
attempt 1: write-write conflict
committed: relate:6, reviews: 11
```

The retry re-runs the whole unit of work, reads included, because the losing transaction saw state that has since changed. Compare bookmarks by their number, never as strings: `"relate:10"` sorts before `"relate:9"`. An auto-commit write carries its bookmark on `result.getBookmark()`. [Transactions and concurrency](https://relatedb.ai/docs/concepts/transactions-and-concurrency/) explains what a transaction isolates.

## Read result metadata

`query` returns a `Database.Result` that holds every row, copied out of the engine before the call returns. Beside `columns()` and `rows()` it reports:

| Method               | Value                                                                                                                                      |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `getQueryType()`     | `"r"` (read), `"w"` (write, no columns), `"rw"` (write with columns) or `"s"` (schema)                                                     |
| `getNamespace()`     | The namespace the statement ran in: its `USE` target, else the connection's                                                                |
| `getBookmark()`      | An auto-commit write's bookmark, `relate:<n>`; the [Java API](https://relatedb.ai/docs/reference/api/java/#result) lists when it is `null` |
| `getNotifications()` | The planner's notifications, filled by `EXPLAIN` and `PROFILE` and empty on a plain run                                                    |
| `getPlan()`          | The plan document under `EXPLAIN` or `PROFILE`, else `null`                                                                                |

The Java `Result` has no mutation counters, column types, or timings. To learn how many nodes a write touched, return a count from the statement.

```java run name=metadata
import com.relate.Database;
import com.relate.Notification;

public class Metadata {
    static final String TEAM = """
        CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
               (grace:Person {name: 'Grace', role: 'engineer'}),
               (noor:Person {name: 'Noor', role: 'maintainer'}),
               (relate:Project {name: 'relate', language: 'Rust'}),
               (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
               (ada)-[:WORKS_ON {since: 2024}]->(relate),
               (grace)-[:WORKS_ON {since: 2023}]->(relate),
               (grace)-[:WORKS_ON {since: 2022}]->(tck),
               (noor)-[:MAINTAINS]->(relate),
               (ada)-[:KNOWS]->(grace)
        """;

    static void show(String label, Database.Result r) {
        System.out.println(label + ": type=" + r.getQueryType() + " namespace=" + r.getNamespace()
            + " bookmark=" + r.getBookmark() + " columns=" + r.columns());
    }

    public static void main(String[] args) {
        try (Database db = Database.open(":memory:");
             Database.Connection conn = db.connect()) {
            show("create", conn.query(TEAM));
            show("read", conn.query("MATCH (p:Person) RETURN p.name AS name"));
            show("update", conn.query("MATCH (p:Person {name: 'Ada'}) SET p.role = 'lead' RETURN p.role AS role"));
            show("index", conn.query("CREATE INDEX person_name FOR (p:Person) ON (p.name)"));
            show("routed", conn.query("USE system SHOW DATABASES YIELD name RETURN name"));

            var explained = conn.query("EXPLAIN MATCH (a:Person), (b:Project) RETURN a.name, b.name");
            System.out.println("plan root: " + explained.getPlan().root.operator);
            for (Notification n : explained.getNotifications()) {
                System.out.println(n.getGqlStatus() + " " + n.getSeverity() + " " + n.getTitle());
                System.out.println("remedy: " + n.getRemedy());
            }
        }
    }
}
```

```text
create: type=WRITE namespace=NEO4J bookmark=relate:1 columns=[]
read: type=READ namespace=NEO4J bookmark=null columns=[name]
update: type=READ_WRITE namespace=NEO4J bookmark=relate:2 columns=[role]
index: type=SCHEMA namespace=NEO4J bookmark=relate:3 columns=[]
routed: type=READ namespace=SYSTEM bookmark=null columns=[name]
plan root: ProduceResults
03N90 INFORMATION Cartesian product
remedy: Give the patterns a join anchor: reuse a node variable across them, or add a pattern that relates their endpoints; otherwise run them as separate queries.
```

`EXPLAIN` plans the statement without running it, and the two unconnected patterns earn a Cartesian product notification with its remedy. [Notifications](https://relatedb.ai/docs/reference/cypher/notifications/) lists every kind.

## Handle errors

Every failure the engine reports throws a subclass of `RelateException`, an unchecked exception. The class comes from the engine's classification, so code never parses message text:

| Class                | Thrown when                                                                                                                |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `TransientException` | `isRetryable()` is true: a write-write conflict, or the retryable workspace cap                                            |
| `ClientException`    | The code starts `Neo.ClientError.`: syntax, types, a missing parameter, an unknown namespace, a write on a read-only store |
| `DatabaseException`  | The code starts `Neo.DatabaseError.`: the engine's own failures, every failed open among them                              |
| `RelateException`    | Anything else, including the client's own failures (loading the engine, a closed handle), where `getCode()` is `null`      |

Each carries `getCode()` (the Neo4j status code), `getGqlStatus()`, `getStage()` (`COMPILE`, `RUNTIME` or `INTERNAL`) and `isRetryable()`:

```java run name=errors
import com.relate.ClientException;
import com.relate.Database;
import com.relate.RelateException;
import java.util.Map;

public class Errors {
    static final String TEAM = """
        CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
               (grace:Person {name: 'Grace', role: 'engineer'}),
               (noor:Person {name: 'Noor', role: 'maintainer'}),
               (relate:Project {name: 'relate', language: 'Rust'}),
               (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
               (ada)-[:WORKS_ON {since: 2024}]->(relate),
               (grace)-[:WORKS_ON {since: 2023}]->(relate),
               (grace)-[:WORKS_ON {since: 2022}]->(tck),
               (noor)-[:MAINTAINS]->(relate),
               (ada)-[:KNOWS]->(grace)
        """;

    public static void main(String[] args) {
        Database db = Database.open(":memory:");
        Database.Connection conn = db.connect();
        conn.query(TEAM);
        try {
            conn.query("MATCH (p:Person {name: $name}) RETURN p.role AS role");
        } catch (ClientException e) {
            System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
            System.out.println("code: " + e.getCode());
            System.out.println("gql_status: " + e.getGqlStatus() + ", stage: " + e.getStage()
                + ", retryable: " + e.isRetryable());
        }
        System.out.println(conn.query("MATCH (p:Person {name: $name}) RETURN p.role AS role",
            Map.of("name", "Noor")).rows().get(0).get(0));

        conn.close();
        db.close();
        try {
            conn.query("RETURN 1");
        } catch (RelateException e) {
            System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage() + ", code: " + e.getCode());
        }
    }
}
```

```text
ClientException: semantic error: ParameterMissing: Expected parameter(s): name
code: Neo.ClientError.Statement.ParameterMissing
gql_status: 42N02, stage: COMPILE, retryable: false
maintainer
RelateException: connection is closed, code: null
```

`close()` is idempotent, and a call after it throws instead of touching freed memory. After an `INTERNAL` failure the handle may be unusable: close it and open a new one. The [errors reference](https://relatedb.ai/docs/reference/errors/#engine-parameter-missing) lists every engine message with its cause and codes.

## Verify

Ask the engine which release the jar loaded:

```java run name=verify
import com.relate.Database;

public class Verify {
    public static void main(String[] args) {
        try (Database db = Database.open(":memory:");
             Database.Connection conn = db.connect()) {
            var result = conn.query("CALL dbms.components() YIELD name, versions "
                + "WHERE name = 'Relate' RETURN versions[0] AS version");
            System.out.println("relate " + result.rows().get(0).get(0));
        }
    }
}
```

```text
relate 0.1.2
```

A version means the jar found an engine for your platform, loaded it, and ran a statement through it.

## If it goes wrong

| Error                                                                                                                            | Cause                                                                                                                           | Remedy                                                                                      |
| -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `RelateException`: `relate: no bundled native library for platform <key> …`                                                      | The classpath has a classifier jar for another platform                                                                         | Depend on the plain jar, or add the classifier the message names                            |
| `RelateException`: `relate: unsupported platform os.name=<name>, os.arch=<arch>; …`                                              | No jar carries an engine for this operating system and architecture                                                             | Run on one of the five platforms in [Prerequisites](#prerequisites)                         |
| `RelateException`: `relate: cannot extract the bundled native library …`                                                         | The cache directory is not writable                                                                                             | Pass `-Drelate.native.cache=<writable directory>`                                           |
| `RelateException`: `incompatible librelate_ffi at <path> …`                                                                      | `RELATE_LIB` points at a library of another C application binary interface (C ABI) version; the client needs version 2          | Unset `RELATE_LIB` to load the jar's engine, or rebuild the library from the same release   |
| `DatabaseException`: `internal error` (`Neo.DatabaseError.General.UnknownError`, stage `INTERNAL`) from `open` or `openReadOnly` | Another process or an unclosed `Database` in this JVM holds the store for writing, or `openReadOnly` named a path with no store | Close the other writer, open a directory store read-only beside its writer, or fix the path |
| `ClientException`: `semantic error: WriteOnReadOnlyAccessDatabase: …` (`42N18`)                                                  | A write or `checkpoint()` on a database opened with `openReadOnly`                                                              | Open with `Database.open` for writes                                                        |

## Related

- [Java API](https://relatedb.ai/docs/reference/api/java/): every class, method, value mapping, and exception.
- [Embed relate in a Java app](https://relatedb.ai/docs/get-started/java/): the first program, step by step.
- [Errors](https://relatedb.ai/docs/reference/errors/): every engine error message, its codes, and its remedy.
- [Transactions and concurrency](https://relatedb.ai/docs/concepts/transactions-and-concurrency/): what a transaction isolates, and why a store has one writer.
- [Choose a store layout](https://relatedb.ai/docs/guides/storage/choose-a-store-layout/): in memory, a directory or one `.rl` file.
- [Project](https://relatedb.ai/docs/reference/project/): releases, platforms, and where each package is published.
- [Embedded graph in an app](https://relatedb.ai/docs/concepts/problems-relate-solves/#embedded-graph-in-an-app): the problem this client solves.

## Terms used on this page

- **client**: A language package that runs the relate engine inside a program's process: the Python, Node.js, Rust, Java, Go and Swift clients.
- **read-only**: An open mode (`--read-only`) that never writes, checkpoints or migrates the store; writes fail, and a directory store can be read beside its writer.
- **store**: A relate database: a directory, a single `.rl` file, or `:memory:`, opened with `--database`.
- **bookmark**: The marker a commit returns, `relate:<n>`, naming its place in the store's commit sequence; returned by the clients and over Bolt.
- **write-write conflict**: The retryable error a commit gets when another transaction committed a change to the same node or relationship after it began; exit code 5 on the CLI.
