Developer-kit spring-data-neo4j
Provides Spring Data Neo4j integration patterns for Spring Boot applications. Use when you need to work with a graph database, Neo4j nodes and relationships, Cypher queries, or Spring Data Neo4j. Creates node entities with @Node annotation, defines relationships with @Relationship, writes Cypher queries using @Query, configures imperative and reactive Neo4j repositories, implements graph traversal patterns, and sets up testing with embedded databases.
git clone https://github.com/giuseppe-trisciuoglio/developer-kit
T=$(mktemp -d) && git clone --depth=1 https://github.com/giuseppe-trisciuoglio/developer-kit "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/developer-kit-java/skills/spring-data-neo4j" ~/.claude/skills/giuseppe-trisciuoglio-developer-kit-spring-data-neo4j && rm -rf "$T"
plugins/developer-kit-java/skills/spring-data-neo4j/SKILL.mdSpring 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=secret
Configure Cypher-DSL dialect (recommended):
@Configuration public class Neo4jConfig { @Bean Configuration cypherDslConfiguration() { return Configuration.newConfig() .withDialect(Dialect.NEO4J_5).build(); } }
Validation Checkpoint: Run
via cypher-shell to verify the connection works before proceeding.MATCH (n) RETURN count(n)
2. Define Node Entities
- Use
Node annotation to mark entity classes@ - Choose ID strategy:
- Business key as
Id (immutable, natural identifier)@ - Generated
Id@
GeneratedValue (Neo4j internal ID)@
- Business key as
- Define relationships with
Relationship annotation@ - Keep entities immutable with final fields
- Use
Property for custom property names@
Validation Checkpoint: If entity save fails, check for constraint violations—duplicate IDs violate uniqueness constraints.
3. Create Repositories
- Extend repository interface:
for imperative operationsNeo4jRepository<Entity, ID>
for reactive operationsReactiveNeo4jRepository<Entity, ID>
- Use query derivation for simple queries
- Apply
Query annotation for complex Cypher queries@ - Use
paramName syntax for parameters$
Validation Checkpoint: Test repository with
first—if empty, verify the Neo4j instance is running and credentials are correct.findAll()
4. Test Your Implementation
- Use
DataNeo4jTest for repository testing with test slicing@ - Set up Neo4j Harness with embedded database and fixtures
- Provide test data via
Cypher querieswithFixture() - 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
for blocking, imperative operationsNeo4jRepository - Use
for non-blocking, reactive operationsReactiveNeo4jRepository - 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
for parameter placeholders$parameterName - Use
annotation when parameter name differs from method parameter@Param - 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
for imperative orNeo4jRepository
for reactiveReactiveNeo4jRepository - 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
Cypher querieswithFixture() - Use
for test slicing@DataNeo4jTest - 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
is properly configured.@Transactional - 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 |
|---|---|---|
on localhost:7687 | Neo4j server not running | Start Neo4j or use embedded Neo4j for tests |
| Wrong credentials | Check |
Entity not saved / returns nothing | Transaction not committed | Add or verify auto-commit settings |
on save | Duplicate value | Ensure IDs are unique or use |
| Relationships missing in results | Wrong direction | Check |
returns wrong data | Cypher parameter syntax | Use not |
Test fails with | Embedded Neo4j not started | Ensure 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