
Spring Data Neo4j
- 1.8k installs
- 318 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
How to integrate Neo4j graph databases into Spring Boot applications using entity mapping, repositories, and Cypher queries
About
Spring Data Neo4j provides patterns for integrating Neo4j graph databases into Spring Boot applications. Developers use it to define node entities with @Node annotation, configure imperative or reactive repositories, write custom Cypher queries with @Query, and manage relationships with @Relationship. The skill covers entity design with immutable fields, repository patterns for simple and complex queries, and testing strategies using embedded Neo4j databases via Neo4j Harness for integration tests.
- Define node entities with @Node and relationships with @Relationship annotations
- Create imperative (Neo4jRepository) or reactive (ReactiveNeo4jRepository) repositories with query derivation and custom
- Write parameterized Cypher queries using $paramName syntax to prevent injection and filter graph patterns
- Configure Spring Boot connection pooling, credentials, and Cypher-DSL dialect for Neo4j 5+ compatibility
- Test with embedded Neo4j databases using @DataNeo4jTest and Neo4j Harness with Cypher fixtures
Spring Data Neo4j by the numbers
- 1,808 all-time installs (skills.sh)
- +142 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #271 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
spring-data-neo4j capabilities & compatibility
- Capabilities
- define immutable node entities with @node and @p · configure imperative and reactive repositories w · write parameterized cypher queries with @query a · test with embedded neo4j databases and cypher fi · manage relationship traversal with direction.inc · handle transaction boundaries with @transactiona
- Use cases
- database · api development · testing
- Platforms
- macOS · Windows · Linux
- Runs
- Runs locally
- Pricing
- Free
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-data-neo4jAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 318 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
What it does
Integrate Neo4j graph database with Spring Boot via entity mapping, repositories, and Cypher queries
Who is it for?
Graph database backends, social networks, recommendation engines, knowledge graphs, and relationship-heavy domain models in Spring Boot
Skip if: Relational database integration (use Spring Data JPA), simple CRUD operations without graph traversal, applications requiring lazy-loaded relationships by default
When should I use this skill?
Building data access layer for Neo4j, defining entity relationships, writing graph traversal queries, setting up integration tests with embedded databases
What you get
Working Spring Boot integration with persisted graph entities, type-safe repositories, and custom Cypher queries tested against embedded Neo4j
- Node entity classes with @Node and @Relationship annotations
- Repository interfaces extending Neo4jRepository or ReactiveNeo4jRepository
- Custom Cypher queries with @Query and parameterized syntax
By the numbers
- Supports both imperative blocking and reactive non-blocking operations via separate repository interfaces
- Provides @DataNeo4jTest annotation for focused integration test slicing with embedded Neo4j
- Enables parameter injection in Cypher queries using $paramName syntax to prevent injection attacks
Files
Spring Data Neo4j Integration Patterns
Overview
Provides Spring Data Neo4j integration patterns for Spring Boot applications. Covers node entity mapping with @Node and @Relationship, repository configuration (imperative and reactive), custom Cypher queries with @Query, and integration testing with embedded Neo4j databases.
When to Use
Use this skill when working with:
- Graph databases and Neo4j integration in Spring Boot
- Node entities, relationships, and Cypher queries
- Spring Data Neo4j repositories (imperative or reactive)
- Neo4j testing with embedded databases
Instructions
1. Set Up Spring Data Neo4j
Add the dependency:
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>Gradle:
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'Configure connection in application.properties:
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secretConfigure Cypher-DSL dialect (recommended):
@Configuration
public class Neo4jConfig {
@Bean
Configuration cypherDslConfiguration() {
return Configuration.newConfig()
.withDialect(Dialect.NEO4J_5).build();
}
}Validation Checkpoint: Run MATCH (n) RETURN count(n) via cypher-shell to verify the connection works before proceeding.2. Define Node Entities
1. Use `@`Node annotation to mark entity classes 2. Choose ID strategy:
- Business key as
@Id (immutable, natural identifier) - Generated
@Id@GeneratedValue (Neo4j internal ID)
3. Define relationships with @Relationship annotation 4. Keep entities immutable with final fields 5. Use `@`Property for custom property names
Validation Checkpoint: If entity save fails, check for constraint violations—duplicate IDs violate uniqueness constraints.
3. Create Repositories
1. Extend repository interface:
Neo4jRepository<Entity, ID>for imperative operationsReactiveNeo4jRepository<Entity, ID>for reactive operations
2. Use query derivation for simple queries 3. Apply `@`Query annotation for complex Cypher queries 4. Use `$`paramName syntax for parameters
Validation Checkpoint: Test repository with findAll() first—if empty, verify the Neo4j instance is running and credentials are correct.4. Test Your Implementation
1. Use `@`DataNeo4jTest for repository testing with test slicing 2. Set up Neo4j Harness with embedded database and fixtures 3. Provide test data via withFixture() Cypher queries 4. Clean up test data between tests
Validation Checkpoint: If tests fail with "Connection refused", ensure the embedded Neo4j started successfully in @BeforeAll.Basic Entity Mapping
Node Entity with Business Key
@Node("Movie")
public class MovieEntity {
@Id
private final String title; // Business key as ID
@Property("tagline")
private final String description;
private final Integer year;
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING)
private List<Roles> actorsAndRoles = new ArrayList<>();
@Relationship(type = "DIRECTED", direction = Direction.INCOMING)
private List<PersonEntity> directors = new ArrayList<>();
public MovieEntity(String title, String description, Integer year) {
this.title = title;
this.description = description;
this.year = year;
}
}Node Entity with Generated ID
@Node("Movie")
public class MovieEntity {
@Id @GeneratedValue
private Long id;
private final String title;
@Property("tagline")
private final String description;
public MovieEntity(String title, String description) {
this.id = null; // Never set manually
this.title = title;
this.description = description;
}
// Wither method for immutability with generated IDs
public MovieEntity withId(Long id) {
if (this.id != null && this.id.equals(id)) {
return this;
} else {
MovieEntity newObject = new MovieEntity(this.title, this.description);
newObject.id = id;
return newObject;
}
}
}Repository Patterns
Basic Repository Interface
@Repository
public interface MovieRepository extends Neo4jRepository<MovieEntity, String> {
// Query derivation from method name
MovieEntity findOneByTitle(String title);
List<MovieEntity> findAllByYear(Integer year);
List<MovieEntity> findByYearBetween(Integer startYear, Integer endYear);
}Reactive Repository
@Repository
public interface MovieRepository extends ReactiveNeo4jRepository<MovieEntity, String> {
Mono<MovieEntity> findOneByTitle(String title);
Flux<MovieEntity> findAllByYear(Integer year);
}Imperative vs Reactive:
- Use
Neo4jRepositoryfor blocking, imperative operations - Use
ReactiveNeo4jRepositoryfor non-blocking, reactive operations - Do not mix imperative and reactive in the same application
- Reactive requires Neo4j 4+ on the database side
Custom Queries with @Query
@Repository
public interface AuthorRepository extends Neo4jRepository<Author, Long> {
@Query("MATCH (b:Book)-[:WRITTEN_BY]->(a:Author) " +
"WHERE a.name = $name AND b.year > $year " +
"RETURN b")
List<Book> findBooksAfterYear(@Param("name") String name,
@Param("year") Integer year);
@Query("MATCH (b:Book)-[:WRITTEN_BY]->(a:Author) " +
"WHERE a.name = $name " +
"RETURN b ORDER BY b.year DESC")
List<Book> findBooksByAuthorOrderByYearDesc(@Param("name") String name);
}Custom Query Best Practices:
- Use
$parameterNamefor parameter placeholders - Use
@Paramannotation when parameter name differs from method parameter - MATCH specifies node patterns and relationships
- WHERE filters results
- RETURN defines what to return
Testing Strategies
Neo4j Harness for Integration Testing
Test Configuration:
@DataNeo4jTest
class BookRepositoryIntegrationTest {
private static Neo4j embeddedServer;
@BeforeAll
static void initializeNeo4j() {
embeddedServer = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer() // No HTTP access needed
.withFixture(
"CREATE (b:Book {isbn: '978-0547928210', " +
"name: 'The Fellowship of the Ring', year: 1954})" +
"-[:WRITTEN_BY]->(a:Author {id: 1, name: 'J. R. R. Tolkien'}) " +
"CREATE (b2:Book {isbn: '978-0547928203', " +
"name: 'The Two Towers', year: 1956})" +
"-[:WRITTEN_BY]->(a)"
)
.build();
}
@AfterAll
static void stopNeo4j() {
embeddedServer.close();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", embeddedServer::boltURI);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", () -> "null");
}
@Autowired
private BookRepository bookRepository;
@Test
void givenBookExists_whenFindOneByTitle_thenBookIsReturned() {
Book book = bookRepository.findOneByTitle("The Fellowship of the Ring");
assertThat(book.getIsbn()).isEqualTo("978-0547928210");
}
}Examples
Example 1: Saving and Retrieving Entities
Input:
MovieEntity movie = new MovieEntity("The Matrix", "Welcome to the Real World", 1999);
movieRepository.save(movie);
MovieEntity found = movieRepository.findOneByTitle("The Matrix");Output:
MovieEntity{
title="The Matrix",
description="Welcome to the Real World",
year=1999,
actorsAndRoles=[],
directors=[]
}Example 2: Custom Cypher Query
Input:
List<Book> books = authorRepository.findBooksAfterYear("J.R.R. Tolkien", 1950);Output:
[
Book{isbn="978-0547928210", name="The Fellowship of the Ring", year=1954},
Book{isbn="978-0547928203", name="The Two Towers", year=1956},
Book{isbn="978-0547928227", name="The Return of the King", year=1957}
]Example 3: Relationship Traversal
Input:
@Query("MATCH (m:Movie)<-[:ACTED_IN]-(a:Person) " +
"WHERE m.title = $title RETURN a.name as actorName")
List<String> findActorsByMovieTitle(@Param("title") String title);
List<String> actors = movieRepository.findActorsByMovieTitle("The Matrix");Output:
["Keanu Reeves", "Laurence Fishburne", "Carrie-Anne Moss", "Hugo Weaving"]---
Progress from basic to advanced examples covering complete movie database, social network patterns, e-commerce product catalogs, custom queries, and reactive operations.
See examples for comprehensive code examples.
Best Practices
Entity Design
- Use immutable entities with final fields
- Choose between business keys (
@Id) or generated IDs (@Id@GeneratedValue) - Keep entities focused on graph structure, not business logic
- Use proper relationship directions (INCOMING, OUTGOING, UNDIRECTED)
Repository Design
- Extend
Neo4jRepositoryfor imperative orReactiveNeo4jRepositoryfor reactive - Use query derivation for simple queries
- Write custom
@Query for complex graph patterns - Don't mix imperative and reactive in same application
Configuration
- Always configure Cypher-DSL dialect explicitly
- Use environment-specific properties for credentials
- Never hardcode credentials in source code
- Configure connection pooling based on load
Testing
- Use Neo4j Harness for integration tests
- Provide test data via
withFixture()Cypher queries - Use
@DataNeo4jTestfor test slicing - Test both successful and edge-case scenarios
Architecture
- Use constructor injection exclusively
- Separate domain entities from DTOs
- Follow feature-based package structure
- Keep domain layer framework-agnostic
Security
- Use Spring Boot property overrides for credentials
- Configure proper authentication and authorization
- Validate input parameters in service layer
- Use parameterized queries to prevent Cypher injection
Constraints and Warnings
- Do not mix imperative and reactive repositories in the same application.
- Neo4j transactions are required for write operations; ensure
@Transactionalis properly configured. - Be cautious with deep relationship traversal as it can cause performance issues.
- Large result sets should be paginated to avoid memory problems.
- Cypher queries are case-sensitive; ensure consistent casing in property names.
- Immutable entities require proper wither methods for generated IDs.
- Relationships in Spring Data Neo4j are not lazy-loaded by default; consider projection for large graphs.
- The Neo4j Java driver is not compatible with reactive streams; use the reactive driver for reactive operations.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
Connection refused on localhost:7687 | Neo4j server not running | Start Neo4j or use embedded Neo4j for tests |
Authentication failed | Wrong credentials | Check spring.neo4j.authentication.username/password |
Entity not saved / MATCH returns nothing | Transaction not committed | Add @Transactional or verify auto-commit settings |
ConstraintViolationException on save | Duplicate @Id value | Ensure IDs are unique or use @GeneratedValue |
| Relationships missing in results | Wrong @Relationship direction | Check Direction.INCOMING/OUTGOING/UNDIRECTED |
@Query returns wrong data | Cypher parameter syntax | Use $paramName not $ {paramName} |
Test fails with @DataNeo4jTest | Embedded Neo4j not started | Ensure @BeforeAll starts Neo4j before tests |
References
For detailed documentation including complete API reference, Cypher query patterns, and configuration options:
- Annotations Reference
- Cypher Query Language
- Configuration Properties
- Repository Methods
- Projections and DTOs
- Transaction Management
- Performance Tuning
External Resources
Spring Data Neo4j - Examples
This document provides comprehensive, real-world examples of Spring Data Neo4j patterns and implementations.
Table of Contents
1. Complete Movie Database Example 2. Social Network Example 3. E-Commerce Product Catalog 4. Custom Query Examples 5. Reactive Neo4j Examples 6. Testing Examples
Complete Movie Database Example
Entity Classes
@Node("Movie")
public class Movie {
@Id
private final String imdbId;
private final String title;
@Property("tagline")
private final String description;
private final Integer releaseYear;
private final List<String> genres;
@Relationship(type = "ACTED_IN", direction = Direction.INCOMING)
private List<ActedIn> actors;
@Relationship(type = "DIRECTED", direction = Direction.INCOMING)
private List<Person> directors;
public Movie(String imdbId, String title, String description,
Integer releaseYear, List<String> genres) {
this.imdbId = imdbId;
this.title = title;
this.description = description;
this.releaseYear = releaseYear;
this.genres = genres;
this.actors = new ArrayList<>();
this.directors = new ArrayList<>();
}
// Getters
public String getImdbId() { return imdbId; }
public String getTitle() { return title; }
public String getDescription() { return description; }
public Integer getReleaseYear() { return releaseYear; }
public List<String> getGenres() { return genres; }
public List<ActedIn> getActors() { return actors; }
public List<Person> getDirectors() { return directors; }
}
@Node("Person")
public class Person {
@Id @GeneratedValue
private Long id;
private final String name;
private final Integer birthYear;
@Relationship(type = "ACTED_IN", direction = Direction.OUTGOING)
private List<ActedIn> actedIn;
@Relationship(type = "DIRECTED", direction = Direction.OUTGOING)
private List<Movie> directed;
public Person(String name, Integer birthYear) {
this.name = name;
this.birthYear = birthYear;
this.actedIn = new ArrayList<>();
this.directed = new ArrayList<>();
}
public Person withId(Long id) {
if (this.id != null && this.id.equals(id)) {
return this;
}
Person newPerson = new Person(this.name, this.birthYear);
newPerson.id = id;
return newPerson;
}
// Getters
public Long getId() { return id; }
public String getName() { return name; }
public Integer getBirthYear() { return birthYear; }
public List<ActedIn> getActedIn() { return actedIn; }
public List<Movie> getDirected() { return directed; }
}
@RelationshipProperties
public class ActedIn {
@Id @GeneratedValue
private Long id;
@TargetNode
private final Movie movie;
private final List<String> roles;
private final Integer screenTime; // in minutes
public ActedIn(Movie movie, List<String> roles, Integer screenTime) {
this.movie = movie;
this.roles = roles;
this.screenTime = screenTime;
}
// Getters
public Long getId() { return id; }
public Movie getMovie() { return movie; }
public List<String> getRoles() { return roles; }
public Integer getScreenTime() { return screenTime; }
}Repository Interfaces
@Repository
public interface MovieRepository extends Neo4jRepository<Movie, String> {
// Simple query derivation
Optional<Movie> findByTitle(String title);
List<Movie> findByReleaseYear(Integer year);
List<Movie> findByReleaseYearBetween(Integer startYear, Integer endYear);
List<Movie> findByGenresContaining(String genre);
// Custom queries
@Query("MATCH (m:Movie) WHERE m.title CONTAINS $keyword RETURN m")
List<Movie> searchByTitle(@Param("keyword") String keyword);
@Query("MATCH (m:Movie)-[:ACTED_IN]-(p:Person) " +
"WHERE p.name = $actorName " +
"RETURN m ORDER BY m.releaseYear DESC")
List<Movie> findMoviesByActor(@Param("actorName") String actorName);
@Query("MATCH (m:Movie)-[:DIRECTED]-(p:Person) " +
"WHERE p.name = $directorName " +
"RETURN m ORDER BY m.releaseYear")
List<Movie> findMoviesByDirector(@Param("directorName") String directorName);
@Query("MATCH (m:Movie) " +
"WHERE $genre IN m.genres AND m.releaseYear >= $minYear " +
"RETURN m ORDER BY m.releaseYear DESC")
List<Movie> findRecentMoviesByGenre(@Param("genre") String genre,
@Param("minYear") Integer minYear);
@Query("MATCH (m:Movie)<-[:ACTED_IN]-(p:Person) " +
"WHERE m.imdbId = $imdbId " +
"RETURN p.name AS name, p.birthYear AS birthYear")
List<PersonProjection> findActorsByMovie(@Param("imdbId") String imdbId);
}
@Repository
public interface PersonRepository extends Neo4jRepository<Person, Long> {
Optional<Person> findByName(String name);
List<Person> findByBirthYearBetween(Integer startYear, Integer endYear);
@Query("MATCH (p:Person)-[r:ACTED_IN]->(m:Movie) " +
"WHERE p.name = $name " +
"RETURN m, r " +
"ORDER BY m.releaseYear DESC")
List<Movie> findMoviesActedInByPerson(@Param("name") String name);
@Query("MATCH (p1:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person) " +
"WHERE p1.name = $actorName AND p1 <> p2 " +
"RETURN DISTINCT p2")
List<Person> findCoActors(@Param("actorName") String actorName);
@Query("MATCH (p:Person)-[:ACTED_IN]->(m:Movie) " +
"WHERE p.name = $name " +
"RETURN COUNT(m) AS movieCount")
Integer countMoviesForPerson(@Param("name") String name);
}
// Projection interface
public interface PersonProjection {
String getName();
Integer getBirthYear();
}Service Layer
@Service
public class MovieService {
private final MovieRepository movieRepository;
private final PersonRepository personRepository;
public MovieService(MovieRepository movieRepository,
PersonRepository personRepository) {
this.movieRepository = movieRepository;
this.personRepository = personRepository;
}
public MovieDTO getMovieByTitle(String title) {
Movie movie = movieRepository.findByTitle(title)
.orElseThrow(() -> new MovieNotFoundException(title));
return mapToDTO(movie);
}
public List<MovieDTO> searchMoviesByKeyword(String keyword) {
return movieRepository.searchByTitle(keyword).stream()
.map(this::mapToDTO)
.collect(Collectors.toList());
}
public List<MovieDTO> getMoviesByGenreAndYear(String genre, Integer minYear) {
return movieRepository.findRecentMoviesByGenre(genre, minYear).stream()
.map(this::mapToDTO)
.collect(Collectors.toList());
}
public MovieDTO createMovie(CreateMovieRequest request) {
Movie movie = new Movie(
request.imdbId(),
request.title(),
request.description(),
request.releaseYear(),
request.genres()
);
Movie saved = movieRepository.save(movie);
return mapToDTO(saved);
}
private MovieDTO mapToDTO(Movie movie) {
return new MovieDTO(
movie.getImdbId(),
movie.getTitle(),
movie.getDescription(),
movie.getReleaseYear(),
movie.getGenres(),
extractActorNames(movie.getActors()),
extractDirectorNames(movie.getDirectors())
);
}
private List<String> extractActorNames(List<ActedIn> actors) {
return actors.stream()
.map(ActedIn::getMovie)
.map(Movie::getTitle)
.collect(Collectors.toList());
}
private List<String> extractDirectorNames(List<Person> directors) {
return directors.stream()
.map(Person::getName)
.collect(Collectors.toList());
}
}DTOs
public record MovieDTO(
String imdbId,
String title,
String description,
Integer releaseYear,
List<String> genres,
List<String> actors,
List<String> directors
) {}
public record CreateMovieRequest(
String imdbId,
String title,
String description,
Integer releaseYear,
List<String> genres
) {
public CreateMovieRequest {
Objects.requireNonNull(imdbId, "IMDB ID is required");
Objects.requireNonNull(title, "Title is required");
if (releaseYear != null && releaseYear < 1888) {
throw new IllegalArgumentException("Invalid release year");
}
}
}Social Network Example
Entity Classes
@Node("User")
public class User {
@Id
private final String username;
private final String email;
private final String fullName;
private final LocalDateTime joinedAt;
@Relationship(type = "FOLLOWS", direction = Direction.OUTGOING)
private Set<User> following;
@Relationship(type = "FOLLOWS", direction = Direction.INCOMING)
private Set<User> followers;
@Relationship(type = "POSTED", direction = Direction.OUTGOING)
private List<Post> posts;
public User(String username, String email, String fullName) {
this.username = username;
this.email = email;
this.fullName = fullName;
this.joinedAt = LocalDateTime.now();
this.following = new HashSet<>();
this.followers = new HashSet<>();
this.posts = new ArrayList<>();
}
public void follow(User user) {
this.following.add(user);
}
public void unfollow(User user) {
this.following.remove(user);
}
// Getters
public String getUsername() { return username; }
public String getEmail() { return email; }
public String getFullName() { return fullName; }
public LocalDateTime getJoinedAt() { return joinedAt; }
public Set<User> getFollowing() { return following; }
public Set<User> getFollowers() { return followers; }
public List<Post> getPosts() { return posts; }
}
@Node("Post")
public class Post {
@Id @GeneratedValue
private Long id;
private final String content;
private final LocalDateTime createdAt;
private Integer likes;
@Relationship(type = "POSTED", direction = Direction.INCOMING)
private User author;
@Relationship(type = "TAGGED", direction = Direction.OUTGOING)
private List<Hashtag> hashtags;
public Post(String content) {
this.content = content;
this.createdAt = LocalDateTime.now();
this.likes = 0;
this.hashtags = new ArrayList<>();
}
public void incrementLikes() {
this.likes++;
}
// Getters omitted
}
@Node("Hashtag")
public class Hashtag {
@Id
private final String tag;
private Integer usageCount;
@Relationship(type = "TAGGED", direction = Direction.INCOMING)
private List<Post> posts;
public Hashtag(String tag) {
this.tag = tag;
this.usageCount = 0;
this.posts = new ArrayList<>();
}
public void incrementUsage() {
this.usageCount++;
}
// Getters omitted
}Repository with Advanced Queries
@Repository
public interface UserRepository extends Neo4jRepository<User, String> {
Optional<User> findByEmail(String email);
@Query("MATCH (u:User {username: $username})-[:FOLLOWS]->(following:User) " +
"RETURN following")
List<User> findFollowing(@Param("username") String username);
@Query("MATCH (u:User {username: $username})<-[:FOLLOWS]-(follower:User) " +
"RETURN follower")
List<User> findFollowers(@Param("username") String username);
@Query("MATCH (u1:User {username: $username1})-[:FOLLOWS]->(mutual:User)" +
"<-[:FOLLOWS]-(u2:User {username: $username2}) " +
"RETURN mutual")
List<User> findMutualFollowing(@Param("username1") String username1,
@Param("username2") String username2);
@Query("MATCH (u:User {username: $username})-[:FOLLOWS*2..3]->(suggested:User) " +
"WHERE NOT (u)-[:FOLLOWS]->(suggested) AND u <> suggested " +
"RETURN DISTINCT suggested " +
"LIMIT $limit")
List<User> findSuggestedUsers(@Param("username") String username,
@Param("limit") Integer limit);
@Query("MATCH (u:User {username: $username})-[:POSTED]->(p:Post) " +
"RETURN COUNT(p)")
Integer countPostsByUser(@Param("username") String username);
@Query("MATCH (u:User {username: $username})-[:FOLLOWS]->(following)-[:POSTED]->(p:Post) " +
"RETURN p ORDER BY p.createdAt DESC LIMIT $limit")
List<Post> getFeed(@Param("username") String username,
@Param("limit") Integer limit);
}
@Repository
public interface PostRepository extends Neo4jRepository<Post, Long> {
@Query("MATCH (p:Post)-[:TAGGED]->(h:Hashtag {tag: $tag}) " +
"RETURN p ORDER BY p.createdAt DESC LIMIT $limit")
List<Post> findByHashtag(@Param("tag") String tag,
@Param("limit") Integer limit);
@Query("MATCH (p:Post) " +
"WHERE p.createdAt >= $since " +
"RETURN p ORDER BY p.likes DESC LIMIT $limit")
List<Post> findTrendingPosts(@Param("since") LocalDateTime since,
@Param("limit") Integer limit);
}E-Commerce Product Catalog
Entity Classes
@Node("Product")
public class Product {
@Id
private final String sku;
private final String name;
private final String description;
private final BigDecimal price;
@Relationship(type = "BELONGS_TO", direction = Direction.OUTGOING)
private Category category;
@Relationship(type = "SIMILAR_TO", direction = Direction.UNDIRECTED)
private List<Product> similarProducts;
@Relationship(type = "PURCHASED_WITH", direction = Direction.UNDIRECTED)
private List<PurchasedWith> frequentlyBoughtTogether;
public Product(String sku, String name, String description, BigDecimal price) {
this.sku = sku;
this.name = name;
this.description = description;
this.price = price;
this.similarProducts = new ArrayList<>();
this.frequentlyBoughtTogether = new ArrayList<>();
}
// Getters omitted
}
@Node("Category")
public class Category {
@Id @GeneratedValue
private Long id;
private final String name;
@Relationship(type = "PARENT_CATEGORY", direction = Direction.OUTGOING)
private Category parent;
@Relationship(type = "PARENT_CATEGORY", direction = Direction.INCOMING)
private List<Category> subcategories;
public Category(String name) {
this.name = name;
this.subcategories = new ArrayList<>();
}
// Getters omitted
}
@RelationshipProperties
public class PurchasedWith {
@Id @GeneratedValue
private Long id;
@TargetNode
private final Product product;
private Integer purchaseCount;
public PurchasedWith(Product product) {
this.product = product;
this.purchaseCount = 1;
}
public void incrementCount() {
this.purchaseCount++;
}
// Getters omitted
}Repository with Recommendation Queries
@Repository
public interface ProductRepository extends Neo4jRepository<Product, String> {
List<Product> findByNameContaining(String keyword);
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
@Query("MATCH (p:Product)-[:BELONGS_TO]->(c:Category {name: $categoryName}) " +
"RETURN p ORDER BY p.price")
List<Product> findByCategoryName(@Param("categoryName") String categoryName);
@Query("MATCH (p:Product)-[:BELONGS_TO]->(c:Category)" +
"-[:PARENT_CATEGORY*0..]->(parent:Category {name: $parentCategory}) " +
"RETURN p")
List<Product> findByParentCategory(@Param("parentCategory") String parentCategory);
@Query("MATCH (p:Product {sku: $sku})-[:SIMILAR_TO]->(similar:Product) " +
"RETURN similar LIMIT $limit")
List<Product> findSimilarProducts(@Param("sku") String sku,
@Param("limit") Integer limit);
@Query("MATCH (p:Product {sku: $sku})-[r:PURCHASED_WITH]->(related:Product) " +
"RETURN related ORDER BY r.purchaseCount DESC LIMIT $limit")
List<Product> findFrequentlyBoughtTogether(@Param("sku") String sku,
@Param("limit") Integer limit);
@Query("MATCH (p1:Product {sku: $sku1})<-[:PURCHASED_WITH]-(p2:Product)" +
"-[:PURCHASED_WITH]->(recommended:Product) " +
"WHERE recommended.sku <> $sku1 " +
"RETURN recommended, COUNT(*) AS score " +
"ORDER BY score DESC LIMIT $limit")
List<Product> findRecommendedProducts(@Param("sku1") String sku1,
@Param("limit") Integer limit);
}Custom Query Examples
Pagination and Sorting
@Repository
public interface MovieRepository extends Neo4jRepository<Movie, String> {
// Using Pageable
Page<Movie> findByGenresContaining(String genre, Pageable pageable);
// Using Sort
List<Movie> findByReleaseYearBetween(Integer start, Integer end, Sort sort);
// Custom query with pagination
@Query("MATCH (m:Movie) WHERE $genre IN m.genres " +
"RETURN m ORDER BY m.releaseYear DESC SKIP $skip LIMIT $limit")
List<Movie> findByGenrePaginated(@Param("genre") String genre,
@Param("skip") Integer skip,
@Param("limit") Integer limit);
}
// Usage
public class MovieService {
public Page<MovieDTO> getMoviesByGenre(String genre, int page, int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("releaseYear").descending());
return movieRepository.findByGenresContaining(genre, pageable)
.map(this::mapToDTO);
}
}Aggregation Queries
@Repository
public interface StatisticsRepository extends Neo4jRepository<Movie, String> {
@Query("MATCH (m:Movie) WHERE m.releaseYear = $year " +
"RETURN COUNT(m) AS count")
Long countMoviesByYear(@Param("year") Integer year);
@Query("MATCH (m:Movie) " +
"RETURN m.releaseYear AS year, COUNT(m) AS count " +
"ORDER BY year DESC")
List<YearStatistics> getMovieCountByYear();
@Query("MATCH (p:Person)-[:ACTED_IN]->(m:Movie) " +
"RETURN p.name AS actor, COUNT(m) AS movieCount " +
"ORDER BY movieCount DESC LIMIT $limit")
List<ActorStatistics> getMostProlificActors(@Param("limit") Integer limit);
@Query("MATCH (m:Movie) " +
"RETURN AVG(m.releaseYear) AS averageYear, " +
"MIN(m.releaseYear) AS oldestYear, " +
"MAX(m.releaseYear) AS newestYear")
MovieYearStatistics getYearStatistics();
}
// Projection interfaces
public interface YearStatistics {
Integer getYear();
Long getCount();
}
public interface ActorStatistics {
String getActor();
Long getMovieCount();
}
public interface MovieYearStatistics {
Double getAverageYear();
Integer getOldestYear();
Integer getNewestYear();
}Reactive Neo4j Examples
Reactive Repository
@Repository
public interface ReactiveMovieRepository
extends ReactiveNeo4jRepository<Movie, String> {
Mono<Movie> findByTitle(String title);
Flux<Movie> findByReleaseYear(Integer year);
@Query("MATCH (m:Movie) WHERE m.title CONTAINS $keyword RETURN m")
Flux<Movie> searchByTitle(@Param("keyword") String keyword);
@Query("MATCH (m:Movie)-[:ACTED_IN]-(p:Person {name: $actorName}) " +
"RETURN m ORDER BY m.releaseYear DESC")
Flux<Movie> findMoviesByActor(@Param("actorName") String actorName);
}Reactive Service
@Service
public class ReactiveMovieService {
private final ReactiveMovieRepository movieRepository;
public ReactiveMovieService(ReactiveMovieRepository movieRepository) {
this.movieRepository = movieRepository;
}
public Mono<MovieDTO> getMovieByTitle(String title) {
return movieRepository.findByTitle(title)
.map(this::mapToDTO)
.switchIfEmpty(Mono.error(
new MovieNotFoundException("Movie not found: " + title)));
}
public Flux<MovieDTO> searchMovies(String keyword) {
return movieRepository.searchByTitle(keyword)
.map(this::mapToDTO);
}
public Mono<MovieDTO> createMovie(CreateMovieRequest request) {
Movie movie = new Movie(
request.imdbId(),
request.title(),
request.description(),
request.releaseYear(),
request.genres()
);
return movieRepository.save(movie)
.map(this::mapToDTO);
}
private MovieDTO mapToDTO(Movie movie) {
return new MovieDTO(
movie.getImdbId(),
movie.getTitle(),
movie.getDescription(),
movie.getReleaseYear(),
movie.getGenres(),
List.of(),
List.of()
);
}
}Reactive Controller
@RestController
@RequestMapping("/api/movies")
public class ReactiveMovieController {
private final ReactiveMovieService movieService;
public ReactiveMovieController(ReactiveMovieService movieService) {
this.movieService = movieService;
}
@GetMapping("/{title}")
public Mono<MovieDTO> getMovie(@PathVariable String title) {
return movieService.getMovieByTitle(title);
}
@GetMapping("/search")
public Flux<MovieDTO> searchMovies(@RequestParam String keyword) {
return movieService.searchMovies(keyword);
}
@PostMapping
public Mono<MovieDTO> createMovie(@RequestBody CreateMovieRequest request) {
return movieService.createMovie(request);
}
}Testing Examples
Integration Test with Neo4j Harness
@DataNeo4jTest
class MovieRepositoryIntegrationTest {
private static Neo4j embeddedNeo4j;
@BeforeAll
static void initializeNeo4j() {
embeddedNeo4j = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer()
.withFixture("""
CREATE (m1:Movie {
imdbId: 'tt0120737',
title: 'The Lord of the Rings: The Fellowship of the Ring',
tagline: 'One Ring to rule them all',
releaseYear: 2001,
genres: ['Adventure', 'Drama', 'Fantasy']
})
CREATE (m2:Movie {
imdbId: 'tt0167261',
title: 'The Lord of the Rings: The Two Towers',
tagline: 'The journey continues',
releaseYear: 2002,
genres: ['Adventure', 'Drama', 'Fantasy']
})
CREATE (p:Person {
name: 'Peter Jackson',
birthYear: 1961
})
CREATE (p)-[:DIRECTED]->(m1)
CREATE (p)-[:DIRECTED]->(m2)
""")
.build();
}
@AfterAll
static void stopNeo4j() {
embeddedNeo4j.close();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", embeddedNeo4j::boltURI);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", () -> "null");
}
@Autowired
private MovieRepository movieRepository;
@Test
void shouldFindMovieByTitle() {
Optional<Movie> movie = movieRepository.findByTitle(
"The Lord of the Rings: The Fellowship of the Ring");
assertThat(movie).isPresent();
assertThat(movie.get().getImdbId()).isEqualTo("tt0120737");
assertThat(movie.get().getReleaseYear()).isEqualTo(2001);
}
@Test
void shouldFindMoviesByYear() {
List<Movie> movies = movieRepository.findByReleaseYear(2001);
assertThat(movies).hasSize(1);
assertThat(movies.get(0).getTitle())
.contains("Fellowship of the Ring");
}
@Test
void shouldFindMoviesByGenre() {
List<Movie> movies = movieRepository.findByGenresContaining("Fantasy");
assertThat(movies).hasSize(2);
}
@Test
void shouldSearchMoviesByKeyword() {
List<Movie> movies = movieRepository.searchByTitle("Rings");
assertThat(movies).hasSize(2);
}
}Service Layer Test with Mocks
@ExtendWith(MockitoExtension.class)
class MovieServiceTest {
@Mock
private MovieRepository movieRepository;
@Mock
private PersonRepository personRepository;
@InjectMocks
private MovieService movieService;
@Test
void shouldGetMovieByTitle() {
// Given
String title = "The Matrix";
Movie movie = new Movie(
"tt0133093",
title,
"A computer hacker learns about the true nature of reality",
1999,
List.of("Action", "Sci-Fi")
);
when(movieRepository.findByTitle(title))
.thenReturn(Optional.of(movie));
// When
MovieDTO result = movieService.getMovieByTitle(title);
// Then
assertThat(result.title()).isEqualTo(title);
assertThat(result.releaseYear()).isEqualTo(1999);
verify(movieRepository).findByTitle(title);
}
@Test
void shouldThrowExceptionWhenMovieNotFound() {
// Given
String title = "Non-existent Movie";
when(movieRepository.findByTitle(title))
.thenReturn(Optional.empty());
// When/Then
assertThatThrownBy(() -> movieService.getMovieByTitle(title))
.isInstanceOf(MovieNotFoundException.class)
.hasMessageContaining(title);
}
@Test
void shouldCreateMovie() {
// Given
CreateMovieRequest request = new CreateMovieRequest(
"tt1234567",
"New Movie",
"A new movie description",
2024,
List.of("Action")
);
Movie movie = new Movie(
request.imdbId(),
request.title(),
request.description(),
request.releaseYear(),
request.genres()
);
when(movieRepository.save(any(Movie.class)))
.thenReturn(movie);
// When
MovieDTO result = movieService.createMovie(request);
// Then
assertThat(result.imdbId()).isEqualTo(request.imdbId());
assertThat(result.title()).isEqualTo(request.title());
verify(movieRepository).save(any(Movie.class));
}
}Reactive Test Example
@DataNeo4jTest
class ReactiveMovieRepositoryTest {
private static Neo4j embeddedNeo4j;
@BeforeAll
static void initializeNeo4j() {
embeddedNeo4j = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer()
.withFixture("CREATE (m:Movie {imdbId: 'tt0133093', " +
"title: 'The Matrix', releaseYear: 1999})")
.build();
}
@AfterAll
static void stopNeo4j() {
embeddedNeo4j.close();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", embeddedNeo4j::boltURI);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", () -> "null");
}
@Autowired
private ReactiveMovieRepository reactiveMovieRepository;
@Test
void shouldFindMovieByTitle() {
StepVerifier.create(reactiveMovieRepository.findByTitle("The Matrix"))
.assertNext(movie -> {
assertThat(movie.getImdbId()).isEqualTo("tt0133093");
assertThat(movie.getReleaseYear()).isEqualTo(1999);
})
.verifyComplete();
}
@Test
void shouldReturnEmptyWhenMovieNotFound() {
StepVerifier.create(reactiveMovieRepository.findByTitle("Non-existent"))
.verifyComplete();
}
@Test
void shouldFindMoviesByYear() {
StepVerifier.create(reactiveMovieRepository.findByReleaseYear(1999))
.expectNextCount(1)
.verifyComplete();
}
}These examples demonstrate real-world patterns for using Spring Data Neo4j, including entity modeling, repository design, service layer implementation, and comprehensive testing strategies.
Spring Data Neo4j - Reference Guide
This document provides detailed reference information for Spring Data Neo4j, including annotations, query language syntax, configuration options, and API documentation.
Table of Contents
1. Annotations Reference 2. Cypher Query Language 3. Configuration Properties 4. Repository Methods 5. Projections and DTOs 6. Transaction Management 7. Performance Tuning
Annotations Reference
Entity Annotations
@Node
Marks a class as a Neo4j node entity.
@Node // Label defaults to class name
@Node("CustomLabel") // Explicit label
@Node({"Label1", "Label2"}) // Multiple labels
public class MyEntity {
// ...
}Properties:
valueorlabels: String or String array for node labelsprimaryLabel: Specify which label is primary (when using multiple labels)
@Id
Marks a field as the entity identifier.
@Id
private String businessKey; // Custom business key
@Id @GeneratedValue
private Long id; // Auto-generated internal IDImportant:
- Required on every
@Node entity - Can be used with business keys or generated values
- Must be unique within the node type
@GeneratedValue
Configures ID generation strategy.
@Id @GeneratedValue
private Long id; // Uses Neo4j internal ID
@Id @GeneratedValue(generatorClass = UUIDStringGenerator.class)
private String uuid; // Custom UUID generator
@Id @GeneratedValue(generatorClass = MyCustomGenerator.class)
private String customId;Built-in Generators:
InternalIdGenerator(default for Long): Uses Neo4j's internal IDUUIDStringGenerator: Generates UUID strings
@Property
Maps a field to a different property name in Neo4j.
@Property("graph_property_name")
private String javaFieldName;When to use:
- Field name differs from graph property name
- Property names contain special characters
- Following different naming conventions
@Relationship
Defines relationships between nodes.
@Relationship(type = "RELATIONSHIP_TYPE", direction = Direction.OUTGOING)
private RelatedEntity related;
@Relationship(type = "RELATED_TO", direction = Direction.INCOMING)
private List<RelatedEntity> incoming;
@Relationship(type = "CONNECTED", direction = Direction.UNDIRECTED)
private Set<RelatedEntity> connections;Properties:
type(required): Relationship type in Neo4jdirection: OUTGOING, INCOMING, or UNDIRECTED- Default direction is OUTGOING if not specified
Direction Guidelines:
OUTGOING: This node → target nodeINCOMING: Target node → this nodeUNDIRECTED: Ignores direction when querying
@RelationshipProperties
Marks a class as relationship properties container.
@RelationshipProperties
public class ActedIn {
@Id @GeneratedValue
private Long id;
@TargetNode
private Movie movie;
private List<String> roles;
private Integer screenTime;
}Required Fields:
@Idfield (can be generated)@TargetNodefield pointing to target entity
Repository Annotations
@Query
Defines custom Cypher query for a repository method.
@Query("MATCH (n:Node) WHERE n.property = $param RETURN n")
List<Node> customQuery(@Param("param") String param);
@Query("MATCH (n:Node) WHERE n.id = $0 RETURN n")
Node findById(String id); // Positional parameterParameter Binding:
- Use
$paramNamefor named parameters with@Param - Use
$0,$1, etc. for positional parameters - SpEL expressions supported:
#{#entityName}
@Param
Binds method parameter to query parameter.
@Query("MATCH (n) WHERE n.name = $customName RETURN n")
List<Node> find(@Param("customName") String name);When required:
- Parameter name in query differs from method parameter
- Making intent explicit and clear
Configuration Annotations
@EnableNeo4jRepositories
Enables Neo4j repository support.
@Configuration
@EnableNeo4jRepositories(basePackages = "com.example.repositories")
public class Neo4jConfiguration {
// ...
}Properties:
basePackages: Packages to scan for repositoriesbasePackageClasses: Type-safe package specificationrepositoryImplementationPostfix: Custom implementation suffix (default: "Impl")
Note: Auto-enabled by Spring Boot starter, manual configuration rarely needed.
@DataNeo4jTest
Test slice annotation for Neo4j tests.
@DataNeo4jTest
class MyRepositoryTest {
@Autowired
private MyRepository repository;
}What it does:
- Configures test slice for Spring Data Neo4j
- Loads only Neo4j-related beans
- Configures embedded test database when available
- Enables transaction rollback for tests
Cypher Query Language
Basic Patterns
MATCH - Find Patterns
// Find all nodes with label
MATCH (n:Label) RETURN n
// Find node with property
MATCH (n:Label {property: 'value'}) RETURN n
// Find nodes with WHERE clause
MATCH (n:Label) WHERE n.property > 100 RETURN n
// Multiple labels
MATCH (n:Label1:Label2) RETURN nRelationship Patterns
// Outgoing relationship
MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b
// Incoming relationship
MATCH (a:Person)<-[:KNOWS]-(b:Person) RETURN a, b
// Undirected relationship
MATCH (a:Person)-[:KNOWS]-(b:Person) RETURN a, b
// Relationship with properties
MATCH (a)-[r:KNOWS {since: 2020}]->(b) RETURN a, r, b
// Variable length relationships
MATCH (a)-[:KNOWS*1..3]->(b) RETURN a, bCREATE - Create Patterns
// Create single node
CREATE (n:Person {name: 'John', age: 30})
// Create node and relationship
CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})
// Create relationship between existing nodes
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:KNOWS {since: 2020}]->(b)MERGE - Find or Create
// Find or create node
MERGE (n:Person {email: 'john@example.com'})
ON CREATE SET n.created = timestamp()
ON MATCH SET n.accessed = timestamp()
// Find or create relationship
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
MERGE (a)-[r:KNOWS]->(b)
ON CREATE SET r.since = 2020SET - Update Properties
// Set single property
MATCH (n:Person {name: 'John'})
SET n.age = 31
// Set multiple properties
MATCH (n:Person {name: 'John'})
SET n.age = 31, n.city = 'London'
// Set from map
MATCH (n:Person {name: 'John'})
SET n += {age: 31, city: 'London'}
// Add label
MATCH (n:Person {name: 'John'})
SET n:PremiumDELETE and REMOVE
// Delete node (must have no relationships)
MATCH (n:Person {name: 'John'})
DELETE n
// Delete node and relationships
MATCH (n:Person {name: 'John'})
DETACH DELETE n
// Delete relationship
MATCH (a)-[r:KNOWS]->(b)
WHERE a.name = 'Alice' AND b.name = 'Bob'
DELETE r
// Remove property
MATCH (n:Person {name: 'John'})
REMOVE n.age
// Remove label
MATCH (n:Person {name: 'John'})
REMOVE n:PremiumAdvanced Patterns
Collections and List Functions
// Collect results
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
RETURN p.name, collect(m.title) AS movies
// Unwind collection
UNWIND [1, 2, 3] AS number
RETURN number
// List comprehension
MATCH (p:Person)
RETURN [x IN p.skills WHERE x STARTS WITH 'Java'] AS javaSkills
// Size of collection
MATCH (p:Person)
RETURN p.name, size(p.skills) AS skillCountAggregation Functions
// Count
MATCH (p:Person) RETURN count(p)
// Sum
MATCH (p:Product) RETURN sum(p.price)
// Average
MATCH (p:Product) RETURN avg(p.price)
// Min/Max
MATCH (p:Product) RETURN min(p.price), max(p.price)
// Group by with aggregation
MATCH (p:Person)-[:LIVES_IN]->(c:City)
RETURN c.name, count(p) AS population
ORDER BY population DESCConditional Logic
// CASE expression
MATCH (p:Person)
RETURN p.name,
CASE
WHEN p.age < 18 THEN 'Minor'
WHEN p.age < 65 THEN 'Adult'
ELSE 'Senior'
END AS category
// COALESCE - first non-null value
MATCH (p:Person)
RETURN coalesce(p.nickname, p.name) AS displayNamePattern Comprehension
// Pattern comprehension
MATCH (p:Person)
RETURN p.name,
[(p)-[:KNOWS]->(friend) | friend.name] AS friends
// With filtering
MATCH (p:Person)
RETURN p.name,
[(p)-[:KNOWS]->(friend) WHERE friend.age > 30 | friend.name] AS olderFriendsQuery Optimization
Using Indexes
// Create index (admin query, not in @Query)
CREATE INDEX person_name FOR (n:Person) ON (n.name)
// Composite index
CREATE INDEX person_name_age FOR (n:Person) ON (n.name, n.age)
// Use index hint
MATCH (p:Person)
USING INDEX p:Person(name)
WHERE p.name = 'John'
RETURN pPROFILE and EXPLAIN
// Analyze query performance
PROFILE
MATCH (p:Person)-[:KNOWS*1..3]->(friend)
WHERE p.name = 'Alice'
RETURN friend.name
// Dry run without execution
EXPLAIN
MATCH (p:Person)-[:KNOWS]->(friend)
RETURN p, friendLimiting Results
// Limit results
MATCH (n:Person) RETURN n LIMIT 10
// Skip and limit (pagination)
MATCH (n:Person)
RETURN n
ORDER BY n.name
SKIP 20 LIMIT 10Configuration Properties
Connection Properties
# Neo4j URI
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.uri=neo4j://localhost:7687
spring.neo4j.uri=neo4j+s://production.server:7687
# Authentication
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secret
spring.neo4j.authentication.realm=native
spring.neo4j.authentication.kerberos-ticket=...
# Connection pool
spring.neo4j.pool.max-connection-pool-size=50
spring.neo4j.pool.idle-time-before-connection-test=PT30S
spring.neo4j.pool.max-connection-lifetime=PT1H
spring.neo4j.pool.connection-acquisition-timeout=PT60S
spring.neo4j.pool.metrics-enabled=trueAdvanced Configuration
# Logging
spring.neo4j.logging.level=WARN
spring.neo4j.logging.log-leaked-sessions=true
# Connection timeout
spring.neo4j.connection-timeout=PT30S
# Max transaction retry time
spring.neo4j.max-transaction-retry-time=PT30S
# Encrypted connection
spring.neo4j.security.encrypted=true
spring.neo4j.security.trust-strategy=TRUST_ALL_CERTIFICATES
spring.neo4j.security.hostname-verification-enabled=trueNeo4j Driver Configuration Bean
@Configuration
public class Neo4jConfiguration {
@Bean
org.neo4j.driver.Config neo4jDriverConfig() {
return org.neo4j.driver.Config.builder()
.withMaxConnectionPoolSize(50)
.withConnectionAcquisitionTimeout(60, TimeUnit.SECONDS)
.withConnectionLivenessCheckTimeout(30, TimeUnit.SECONDS)
.withMaxConnectionLifetime(1, TimeUnit.HOURS)
.withLogging(Logging.slf4j())
.withEncryption()
.build();
}
@Bean
Configuration cypherDslConfiguration() {
return Configuration.newConfig()
.withDialect(Dialect.NEO4J_5)
.build();
}
}Repository Methods
Query Derivation Keywords
| Keyword | Cypher Equivalent |
|---|---|
findBy | MATCH ... RETURN |
existsBy | MATCH ... RETURN count(*) > 0 |
countBy | MATCH ... RETURN count(*) |
deleteBy | MATCH ... DETACH DELETE |
And | AND |
Or | OR |
Between | >= $lower AND <= $upper |
LessThan | < |
LessThanEqual | <= |
GreaterThan | > |
GreaterThanEqual | >= |
Before | < (for dates) |
After | > (for dates) |
IsNull | IS NULL |
IsNotNull | IS NOT NULL |
Like | =~ '.*pattern.*' |
NotLike | NOT =~ '.*pattern.*' |
StartingWith | STARTS WITH |
EndingWith | ENDS WITH |
Containing | CONTAINS |
In | IN |
NotIn | NOT IN |
True | = true |
False | = false |
OrderBy...Asc | ORDER BY ... ASC |
OrderBy...Desc | ORDER BY ... DESC |
Method Return Types
| Return Type | Description |
|---|---|
Entity | Single result or null |
Optional<Entity> | Single result wrapped in Optional |
List<Entity> | Multiple results |
Stream<Entity> | Results as Java Stream |
Page<Entity> | Paginated results |
Slice<Entity> | Slice of results |
Mono<Entity> | Reactive single result |
Flux<Entity> | Reactive stream of results |
boolean | Existence check |
long | Count query |
Examples
public interface UserRepository extends Neo4jRepository<User, String> {
// Simple query derivation
Optional<User> findByEmail(String email);
List<User> findByAgeGreaterThan(Integer age);
List<User> findByAgeBetween(Integer minAge, Integer maxAge);
List<User> findByNameStartingWith(String prefix);
// Boolean queries
boolean existsByEmail(String email);
// Count queries
long countByAgeGreaterThan(Integer age);
// Delete queries
long deleteByAgeLessThan(Integer age);
// Sorting
List<User> findByAgeGreaterThanOrderByNameAsc(Integer age);
// Pagination
Page<User> findByAgeGreaterThan(Integer age, Pageable pageable);
// Stream
Stream<User> findByAgeBetween(Integer min, Integer max);
// Multiple conditions
List<User> findByNameAndAge(String name, Integer age);
List<User> findByNameOrEmail(String name, String email);
// Null checks
List<User> findByNicknameIsNull();
List<User> findByNicknameIsNotNull();
// Collection queries
List<User> findByRolesContaining(String role);
List<User> findByIdIn(Collection<String> ids);
}Projections and DTOs
Interface-based Projections
// Closed projection - only declared properties
public interface UserSummary {
String getUsername();
String getEmail();
}
// Open projection - with SpEL
public interface UserWithFullName {
@Value("#{target.firstName + ' ' + target.lastName}")
String getFullName();
}
// Nested projection
public interface UserWithPosts {
String getUsername();
List<PostSummary> getPosts();
interface PostSummary {
String getTitle();
LocalDateTime getCreatedAt();
}
}
// Usage
public interface UserRepository extends Neo4jRepository<User, String> {
List<UserSummary> findAllBy();
Optional<UserWithFullName> findByUsername(String username);
}Class-based DTOs
public record UserDTO(
String username,
String email,
LocalDateTime joinedAt
) {}
// Repository usage
public interface UserRepository extends Neo4jRepository<User, String> {
List<UserDTO> findAllBy();
}Dynamic Projections
public interface UserRepository extends Neo4jRepository<User, String> {
<T> T findByUsername(String username, Class<T> type);
}
// Usage
UserSummary summary = repository.findByUsername("john", UserSummary.class);
UserDTO dto = repository.findByUsername("john", UserDTO.class);
User full = repository.findByUsername("john", User.class);Transaction Management
Declarative Transactions
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public User createUser(CreateUserRequest request) {
User user = new User(request.username(), request.email());
return userRepository.save(user);
}
@Transactional(readOnly = true)
public Optional<User> getUser(String username) {
return userRepository.findByUsername(username);
}
@Transactional(
propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.READ_COMMITTED,
timeout = 30
)
public void complexOperation() {
// Multiple repository calls in single transaction
// ...
}
}Programmatic Transactions
@Service
public class TransactionalService {
private final Neo4jTransactionManager transactionManager;
public void executeInTransaction() {
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.execute(status -> {
try {
// Your transactional code here
return someResult;
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
});
}
}Reactive Transactions
@Service
public class ReactiveUserService {
private final ReactiveUserRepository repository;
private final ReactiveNeo4jTransactionManager transactionManager;
public Mono<User> createUser(CreateUserRequest request) {
return transactionManager.getReactiveTransaction()
.flatMap(status -> {
User user = new User(request.username(), request.email());
return repository.save(user)
.doOnError(e -> status.setRollbackOnly());
});
}
}Performance Tuning
Index Creation
Create indexes on frequently queried properties:
// Single property index
CREATE INDEX user_email FOR (u:User) ON (u.email);
// Composite index
CREATE INDEX user_name_age FOR (u:User) ON (u.name, u.age);
// Full-text index
CREATE FULLTEXT INDEX user_search FOR (u:User) ON EACH [u.name, u.bio];
// Show indexes
SHOW INDEXES;
// Drop index
DROP INDEX user_email;Query Optimization Tips
1. Use specific labels:
// Good
MATCH (u:User {email: $email}) RETURN u
// Bad
MATCH (n {email: $email}) RETURN n2. Filter early:
// Good
MATCH (u:User)
WHERE u.age > 18
MATCH (u)-[:POSTED]->(p:Post)
RETURN p
// Bad
MATCH (u:User)-[:POSTED]->(p:Post)
WHERE u.age > 18
RETURN p3. Use projections to fetch only needed data:
// Good
List<UserSummary> findAllBy();
// Bad (when you only need summary)
List<User> findAll();4. Limit result sets:
// Use pagination
Page<User> findAll(Pageable pageable);
// Or explicit limits
@Query("MATCH (u:User) RETURN u LIMIT $limit")
List<User> findTopUsers(@Param("limit") int limit);Connection Pooling
@Bean
org.neo4j.driver.Config driverConfig() {
return org.neo4j.driver.Config.builder()
.withMaxConnectionPoolSize(50)
.withConnectionAcquisitionTimeout(60, TimeUnit.SECONDS)
.withIdleTimeBeforeConnectionTest(30, TimeUnit.SECONDS)
.build();
}Batch Operations
// Save in batches
@Service
public class BatchService {
private final UserRepository repository;
public void saveUsersInBatches(List<User> users) {
int batchSize = 1000;
for (int i = 0; i < users.size(); i += batchSize) {
int end = Math.min(i + batchSize, users.size());
List<User> batch = users.subList(i, end);
repository.saveAll(batch);
}
}
}Monitoring and Metrics
# Enable driver metrics
spring.neo4j.pool.metrics-enabled=true
# Log slow queries (if using Neo4j Enterprise)
# Set in neo4j.conf:
# dbms.logs.query.enabled=true
# dbms.logs.query.threshold=1sAdditional Resources
Related skills
Forks & variants (1)
Spring Data Neo4j has 1 known copy in the catalog totaling 22 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 22 installs
How it compares
Pick spring-data-neo4j for Spring Boot graph ORM patterns; use raw Neo4j driver skills when Spring Data abstractions are not in the stack.
FAQ
Should I use business keys or generated IDs as @Id?
Use business keys (@Id without @GeneratedValue) for immutable, natural identifiers. Use @Id @GeneratedValue for system-generated IDs, but implement wither methods for immutability.
Can I mix imperative and reactive repositories in the same application?
No. Do not mix imperative (Neo4jRepository) and reactive (ReactiveNeo4jRepository) in the same application. Choose one consistently.
Why are my relationships not appearing in query results?
Check the @Relationship direction (INCOMING, OUTGOING, UNDIRECTED) and verify the Cypher query MATCH pattern includes the relationship traversal explicitly.
Is Spring Data Neo4j safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.