Skip to content

Neo4j

dev.rightsize.modules.Neo4jContainer — a single-node Neo4j Community container, queried over its HTTP Cypher transaction endpoint (/db/neo4j/tx/commit) — no bolt driver dependency needed, matching the house convention for HTTP-first modules (ClickHouse, Pinot). The bolt port (7687) is still exposed and its URI available via boltUrl for callers who do want a real driver.

Defaults

Default image neo4j:5-community
Exposed ports 7474 (HTTP — what the helpers use), 7687 (bolt, exposed but not wrapped by a helper)
Env NEO4J_AUTH=neo4j/rightsize-test
Memory limit withMemoryLimit(1024) — see below
Wait strategy Wait.forLogMessage(".*Started\\..*", 1).withStartupTimeout(Duration.ofSeconds(120))

Helpers

Member Returns
httpUrl: String The HTTP interface's base URI — Cypher transactions via POST {httpUrl}/db/neo4j/tx/commit
boltUrl: String The bolt interface's URI, for callers using a real bolt driver instead of the HTTP helpers
username: String The fixed admin username (neo4j — the image has no env var to change it)
password: String The configured admin password (default rightsize-test)
withPassword(password: String): Neo4jContainer Overrides NEO4J_AUTH's password half (the image requires ≥8 chars)

Call withPassword before start(). There's no withUsername — the username is fixed by the image at neo4j.

Example

package dev.rightsize.modules

import dev.rightsize.modules.Neo4jContainer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.util.Base64

class Neo4jContainerTest {
    private val http = HttpClient.newHttpClient()

    private fun basicAuth(user: String, pass: String): String =
        "Basic " + Base64.getEncoder().encodeToString("$user:$pass".toByteArray())

    @Test
    fun `CREATE then MATCH round-trips over the HTTP Cypher transaction endpoint`() {
        val neo4j = Neo4jContainer()
        neo4j.start()
        try {
            fun commit(statement: String): HttpResponse<String> = http.send(
                HttpRequest.newBuilder(URI("${neo4j.httpUrl}/db/neo4j/tx/commit"))
                    .header("Content-Type", "application/json")
                    .header("Authorization", basicAuth(neo4j.username, neo4j.password))
                    .POST(HttpRequest.BodyPublishers.ofString("""{"statements":[{"statement":"$statement"}]}"""))
                    .build(),
                HttpResponse.BodyHandlers.ofString(),
            )

            val create = commit("CREATE (n:Test {name: 'hello'}) RETURN n.name AS name")
            assertEquals(200, create.statusCode(), "CREATE failed: ${create.body()}")
            assertTrue(create.body().contains("\"errors\":[]"), "CREATE reported errors: ${create.body()}")

            val match = commit("MATCH (n:Test {name: 'hello'}) RETURN n.name AS name")
            assertEquals(200, match.statusCode(), "MATCH failed: ${match.body()}")
            assertTrue(match.body().contains("\"hello\""), "MATCH did not return the created node: ${match.body()}")
        } finally {
            neo4j.stop()
        }
    }
}

Backend notes

Started. is the exact log line, verified against a real boot. Captured verbatim from neo4j:5-community:

... INFO  Bolt enabled on 0.0.0.0:7687.
... INFO  HTTP enabled on 0.0.0.0:7474.
... INFO  Remote interface available at http://localhost:7474/
... INFO  Started.

Started. is logged only after both connectors are already listening, so it's both accurate and simpler than a two-port HTTP/bolt race — pinned as the wait strategy over forHttp.

The image refuses passwords under 8 charactersneo4j/neo4j is rejected at boot — so this module defaults to neo4j/rightsize-test, giving zero-configuration use of httpUrl plus basic auth. Call withPassword before start() to override it with your own (still ≥8-character) password.

Memory needed the ladder — but the failure mode is different from an OOM kill. At msb's default ~450 MB microVM RAM, the server logs ERROR Invalid memory configuration - exceeds physical memory. Check the configured values for server.memory.pagecache.size and server.memory.heap.max_size and shuts itself down cleanly (INFO Stopped.) rather than hanging or getting killed by the kernel — Neo4j's own memory-recommendation calculator sizes the page cache and heap off total visible RAM and refuses to start if the sums don't fit. A real docker boot with no memory cap sits at ~468 MiB RSS (docker stats), just over that default budget. Retried with -m 1024M: boots clean, the HTTP Cypher endpoint answers within the startup timeout. This module ships with withMemoryLimit(1024) for exactly this reason, the same number as Keycloak.